diff --git a/.gitignore b/.gitignore index 3a83c2f..5a8eb7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,27 @@ -# See https://www.dartlang.org/guides/libraries/private-files - -# Files and directories created by pub -.dart_tool/ -.packages -build/ -# If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock - -# Directory created by dartdoc -# If you don't generate documentation locally you can remove this line. -doc/api/ - -# dotenv environment variables file -.env* - -# Avoid committing generated Javascript files: -*.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. -*.js_ -*.js.deps -*.js.map - -.flutter-plugins -.flutter-plugins-dependencies +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +# If you're building an application, you may want to check-in your pubspec.lock +pubspec.lock + +# Directory created by dartdoc +# If you don't generate documentation locally you can remove this line. +doc/api/ + +# dotenv environment variables file +.env* + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map + +.flutter-plugins +.flutter-plugins-dependencies diff --git a/LICENSE b/LICENSE index 0ad25db..ada1a81 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +1,661 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 1e41448..897543b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# reFilc+ ✨ - -A collection of features only accessible for reFilc+ subscribers. +# reFilc+ ✨ + +A collection of features only accessible for reFilc+ subscribers. diff --git a/analysis_options.yaml b/analysis_options.yaml index fd16f92..16f5f56 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,28 +1,28 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/api/auth.dart b/lib/api/auth.dart index 030c795..7544f8f 100644 --- a/lib/api/auth.dart +++ b/lib/api/auth.dart @@ -1,201 +1,209 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:developer'; -import 'dart:io'; - -import 'package:refilc/api/client.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:flutter/foundation.dart'; -import 'package:refilc_plus/models/premium_result.dart'; -// import 'package:flutter/foundation.dart'; -// import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:uni_links/uni_links.dart'; -import 'package:http/http.dart' as http; -// import 'package:home_widget/home_widget.dart'; - -class PremiumAuth { - final SettingsProvider _settings; - StreamSubscription? _sub; - - PremiumAuth({required SettingsProvider settings}) : _settings = settings; - - // initAuth() { - // try { - // _sub ??= uriLinkStream.listen( - // (Uri? uri) { - // if (uri != null) { - // final accessToken = uri.queryParameters['access_token']; - // if (accessToken != null) { - // finishAuth(accessToken); - // } - // } - // }, - // onError: (err) { - // log("ERROR: initAuth: $err"); - // }, - // ); - - // launchUrl( - // Uri.parse(FilcAPI.plusAuthLogin), - // mode: LaunchMode.externalApplication, - // ); - // } catch (err, sta) { - // log("ERROR: initAuth: $err\n$sta"); - // } - // } - - initAuth({required String product}) { - try { - _sub ??= uriLinkStream.listen( - (Uri? uri) { - if (uri != null) { - final sessionId = uri.queryParameters['session_id']; - if (sessionId != null) { - finishAuth(sessionId); - } - } - }, - onError: (err) { - log("ERROR: initAuth: $err"); - }, - ); - - launchUrl( - Uri.parse( - "${FilcAPI.payment}/stripe-create-checkout?product=$product&rf_uinid=${_settings.xFilcId}"), - mode: LaunchMode.externalApplication, - ); - } catch (err, sta) { - log("ERROR: initAuth: $err\n$sta"); - } - } - - // Future finishAuth(String accessToken) async { - // try { - // // final res = await http.get(Uri.parse( - // // "${FilcAPI.plusScopes}?access_token=${Uri.encodeComponent(accessToken)}")); - // // final scopes = - // // ((jsonDecode(res.body) as Map)["scopes"] as List).cast(); - // // log("[INFO] Premium auth finish: ${scopes.join(',')}"); - // await _settings.update(premiumAccessToken: accessToken); - // final result = await refreshAuth(); - // // if (Platform.isAndroid) updateWidget(); - // return result; - // } catch (err, sta) { - // log("[ERROR] reFilc+ auth failed: $err\n$sta"); - // } - - // await _settings.update(premiumAccessToken: "", premiumScopes: []); - // // if (Platform.isAndroid) updateWidget(); - // return false; - // } - - Future finishAuth(String sessionId) async { - try { - // final res = await http.get(Uri.parse( - // "${FilcAPI.plusScopes}?access_token=${Uri.encodeComponent(accessToken)}")); - // final scopes = - // ((jsonDecode(res.body) as Map)["scopes"] as List).cast(); - // log("[INFO] Premium auth finish: ${scopes.join(',')}"); - await _settings.update(plusSessionId: sessionId); - final result = await refreshAuth(); - // if (Platform.isAndroid) updateWidget(); - return result; - } catch (err, sta) { - log("[ERROR] reFilc+ auth failed: $err\n$sta"); - } - - await _settings.update(plusSessionId: "", premiumScopes: ["refilc.plus.*"]); - // if (Platform.isAndroid) updateWidget(); - return false; - } - - // Future updateWidget() async { - // try { - // return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable'); - // } on PlatformException catch (exception) { - // if (kDebugMode) { - // print('Error Updating Widget After Auth. $exception'); - // } - // } - // return false; - // } - - Future refreshAuth( - {bool removePremium = false, bool reactivate = false}) async { - if (!removePremium) { - if (_settings.plusSessionId == "" && !reactivate) { - await _settings.update(premiumScopes: ["refilc.plus.*"], premiumLogin: ""); - return false; - } - - // skip reFilc+ check when disconnected - try { - final status = await InternetAddress.lookup('api.refilc.hu'); - if (status.isEmpty) return false; - } on SocketException catch (_) { - return false; - } - - for (int tries = 0; tries < 3; tries++) { - try { - if (kDebugMode) { - print(FilcAPI.plusActivation); - print(_settings.plusSessionId); - print(_settings.xFilcId); - } - - final res = await http.post(Uri.parse(FilcAPI.plusActivation), body: { - "session_id": _settings.plusSessionId, - "rf_uinid": _settings.xFilcId, - }); - - if (kDebugMode) print(res.body); - - if (res.body == "") throw "empty body"; - // if (res.body == "Unauthorized") { - // throw "User is not autchenticated to Github!"; - // } - // if (res.body == "empty_sponsors") { - // throw "This user isn't sponsoring anyone currently!"; - // } - if (res.body == "expired_subscription") { - throw "This user isn't a subscriber anymore!"; - } - if (res.body == "no_subscription") { - throw "This user isn't a subscriber!"; - } - if (res.body == "unknown_device") { - throw "This device is not recognized, please contact support!"; - } - - final premium = PremiumResult.fromJson(jsonDecode(res.body) as Map); - - // successful activation of reFilc+ - log("[INFO] reFilc+ activated: ${premium.scopes.join(',')}"); - await _settings.update( - plusSessionId: premium.sessionId, - premiumScopes: premium.scopes, - premiumLogin: premium.login, - ); - return true; - } catch (err, sta) { - // error while activating reFilc+ - log("[ERROR] reFilc+ activation failed: $err\n$sta"); - } - - await Future.delayed(const Duration(seconds: 1)); - } - } - - // activation of reFilc+ failed - await _settings.update( - premiumAccessToken: "", - premiumScopes: ["refilc.plus.*"], - premiumLogin: "", - plusSessionId: "", - ); - return false; - } -} +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:refilc/api/client.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:flutter/foundation.dart'; +import 'package:refilc_plus/models/premium_result.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/services.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:uni_links/uni_links.dart'; +import 'package:http/http.dart' as http; +// import 'package:home_widget/home_widget.dart'; + +class PremiumAuth { + final SettingsProvider _settings; + StreamSubscription? _sub; + + PremiumAuth({required SettingsProvider settings}) : _settings = settings; + + // initAuth() { + // try { + // _sub ??= uriLinkStream.listen( + // (Uri? uri) { + // if (uri != null) { + // final accessToken = uri.queryParameters['access_token']; + // if (accessToken != null) { + // finishAuth(accessToken); + // } + // } + // }, + // onError: (err) { + // log("ERROR: initAuth: $err"); + // }, + // ); + + // launchUrl( + // Uri.parse(FilcAPI.plusAuthLogin), + // mode: LaunchMode.externalApplication, + // ); + // } catch (err, sta) { + // log("ERROR: initAuth: $err\n$sta"); + // } + // } + + initAuth({required String product, required String paymentProvider}) { + try { + _sub ??= uriLinkStream.listen( + (Uri? uri) { + if (uri != null) { + final sessionId = uri.queryParameters['session_id']; + if (sessionId != null) { + finishAuth(sessionId); + } + } + }, + onError: (err) { + log("ERROR: initAuth: $err"); + }, + ); + + String url = "https://refilcapp.hu"; + if (paymentProvider == "stripe") { + url = + "${FilcAPI.payment}/stripe-create-checkout?product=$product&rf_uinid=${_settings.xFilcId}"; + } else if (paymentProvider == "paypal") { + url = + "https://refilcapp.hu/payment/paypal/mobile-checkout?product=$product&device_id=${_settings.xFilcId}"; + } + + launchUrl( + Uri.parse(url), + mode: LaunchMode.externalApplication, + ); + } catch (err, sta) { + log("ERROR: initAuth: $err\n$sta"); + } + } + + // Future finishAuth(String accessToken) async { + // try { + // // final res = await http.get(Uri.parse( + // // "${FilcAPI.plusScopes}?access_token=${Uri.encodeComponent(accessToken)}")); + // // final scopes = + // // ((jsonDecode(res.body) as Map)["scopes"] as List).cast(); + // // log("[INFO] Premium auth finish: ${scopes.join(',')}"); + // await _settings.update(premiumAccessToken: accessToken); + // final result = await refreshAuth(); + // // if (Platform.isAndroid) updateWidget(); + // return result; + // } catch (err, sta) { + // log("[ERROR] reFilc+ auth failed: $err\n$sta"); + // } + + // await _settings.update(premiumAccessToken: "", premiumScopes: []); + // // if (Platform.isAndroid) updateWidget(); + // return false; + // } + + Future finishAuth(String sessionId) async { + try { + // final res = await http.get(Uri.parse( + // "${FilcAPI.plusScopes}?access_token=${Uri.encodeComponent(accessToken)}")); + // final scopes = + // ((jsonDecode(res.body) as Map)["scopes"] as List).cast(); + // log("[INFO] Premium auth finish: ${scopes.join(',')}"); + await _settings.update(plusSessionId: sessionId); + final result = await refreshAuth(); + // if (Platform.isAndroid) updateWidget(); + return result; + } catch (err, sta) { + log("[ERROR] reFilc+ auth failed: $err\n$sta"); + } + + await _settings.update(plusSessionId: "", premiumScopes: []); + // if (Platform.isAndroid) updateWidget(); + return false; + } + + // Future updateWidget() async { + // try { + // return HomeWidget.updateWidget(name: 'widget_timetable.WidgetTimetable'); + // } on PlatformException catch (exception) { + // if (kDebugMode) { + // print('Error Updating Widget After Auth. $exception'); + // } + // } + // return false; + // } + + Future refreshAuth( + {bool removePremium = false, bool reactivate = false}) async { + if (!removePremium) { + if (_settings.plusSessionId == "" && !reactivate) { + await _settings.update(premiumScopes: [], premiumLogin: ""); + return false; + } + + // skip reFilc+ check when disconnected + try { + final status = await InternetAddress.lookup('api.refilc.hu'); + if (status.isEmpty) return false; + } on SocketException catch (_) { + return false; + } + + for (int tries = 0; tries < 3; tries++) { + try { + if (kDebugMode) { + print(FilcAPI.plusActivation); + print(_settings.plusSessionId); + print(_settings.xFilcId); + } + + final res = await http.post(Uri.parse(FilcAPI.plusActivation), body: { + "session_id": _settings.plusSessionId, + "rf_uinid": _settings.xFilcId, + }); + + if (kDebugMode) print(res.body); + + if (res.body == "") throw "empty body"; + // if (res.body == "Unauthorized") { + // throw "User is not autchenticated to Github!"; + // } + // if (res.body == "empty_sponsors") { + // throw "This user isn't sponsoring anyone currently!"; + // } + if (res.body == "expired_subscription") { + throw "This user isn't a subscriber anymore!"; + } + if (res.body == "no_subscription") { + throw "This user isn't a subscriber!"; + } + if (res.body == "unknown_device") { + throw "This device is not recognized, please contact support!"; + } + + final premium = PremiumResult.fromJson(jsonDecode(res.body) as Map); + + // successful activation of reFilc+ + log("[INFO] reFilc+ activated: ${premium.scopes.join(',')}"); + await _settings.update( + plusSessionId: premium.sessionId, + premiumScopes: premium.scopes, + premiumLogin: premium.login, + ); + return true; + } catch (err, sta) { + // error while activating reFilc+ + log("[ERROR] reFilc+ activation failed: $err\n$sta"); + } + + await Future.delayed(const Duration(seconds: 1)); + } + } + + // activation of reFilc+ failed + await _settings.update( + premiumAccessToken: "", + premiumScopes: [], + premiumLogin: "", + plusSessionId: "", + ); + return false; + } +} diff --git a/lib/helpers/app_icon_helper.dart b/lib/helpers/app_icon_helper.dart index a4e8ced..7624146 100644 --- a/lib/helpers/app_icon_helper.dart +++ b/lib/helpers/app_icon_helper.dart @@ -1,60 +1,60 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_dynamic_icon_plus/flutter_dynamic_icon_plus.dart'; - -class AppIconHelper { - // static const _channel = MethodChannel('app_icon'); - - static Future setAppIcon(String iconName) async { - try { - if (await FlutterDynamicIconPlus.supportsAlternateIcons) { - // await FlutterDynamicIconPlus.setAlternateIconName(iconName: "icon_new"); - if (kDebugMode) { - print("successfully changed app icon"); - } - return; - } - } on PlatformException { - if (kDebugMode) { - print("failed to change icon"); - } - } catch (e) { - // ha nem megy hat nem megy - } - // if (Platform.isIOS) { - // // change icon on ios - // try { - // if (await FlutterDynamicIcon.supportsAlternateIcons) { - // await _channel.invokeMethod('changeIcon', iconName); - // // await FlutterDynamicIcon.setApplicationIconBadgeNumber(0); we don't need this rn, but we will - // await FlutterDynamicIcon.setAlternateIconName(iconName); - // } - // } on PlatformException catch (e) { - // if (kDebugMode) { - // print('Failed to change app icon: ${e.message}'); - // } - // } catch (e) { - // if (kDebugMode) { - // print('Ha nem megy, hat nem megy'); - // } - // } - // } else if (Platform.isAndroid) { - // // change icon on android - // // ignore: no_leading_underscores_for_local_identifiers - // final _androidDynamicIconPlugin = AndroidDynamicIcon(); - // await _androidDynamicIconPlugin.changeIcon( - // bundleId: 'hu.refilc.naplo', - // isNewIcon: iconName != 'refilc_default' ? true : false, - // iconName: iconName != 'refilc_default' ? iconName : '', - // iconNames: [ - // 'refilc_default', - // 'refilc_overcomplicated', - // 'refilc_concept', - // 'refilc_pride', - // ], - // ); - // } else { - // // ha nem megy hát nem megy - // } - } -} +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dynamic_icon_plus/flutter_dynamic_icon_plus.dart'; + +class AppIconHelper { + // static const _channel = MethodChannel('app_icon'); + + static Future setAppIcon(String iconName) async { + try { + if (await FlutterDynamicIconPlus.supportsAlternateIcons) { + // await FlutterDynamicIconPlus.setAlternateIconName(iconName: "icon_new"); + if (kDebugMode) { + print("successfully changed app icon"); + } + return; + } + } on PlatformException { + if (kDebugMode) { + print("failed to change icon"); + } + } catch (e) { + // ha nem megy hat nem megy + } + // if (Platform.isIOS) { + // // change icon on ios + // try { + // if (await FlutterDynamicIcon.supportsAlternateIcons) { + // await _channel.invokeMethod('changeIcon', iconName); + // // await FlutterDynamicIcon.setApplicationIconBadgeNumber(0); we don't need this rn, but we will + // await FlutterDynamicIcon.setAlternateIconName(iconName); + // } + // } on PlatformException catch (e) { + // if (kDebugMode) { + // print('Failed to change app icon: ${e.message}'); + // } + // } catch (e) { + // if (kDebugMode) { + // print('Ha nem megy, hat nem megy'); + // } + // } + // } else if (Platform.isAndroid) { + // // change icon on android + // // ignore: no_leading_underscores_for_local_identifiers + // final _androidDynamicIconPlugin = AndroidDynamicIcon(); + // await _androidDynamicIconPlugin.changeIcon( + // bundleId: 'hu.refilc.naplo', + // isNewIcon: iconName != 'refilc_default' ? true : false, + // iconName: iconName != 'refilc_default' ? iconName : '', + // iconNames: [ + // 'refilc_default', + // 'refilc_overcomplicated', + // 'refilc_concept', + // 'refilc_pride', + // ], + // ); + // } else { + // // ha nem megy hát nem megy + // } + } +} diff --git a/lib/models/premium_result.dart b/lib/models/premium_result.dart index 613fc7d..e676395 100644 --- a/lib/models/premium_result.dart +++ b/lib/models/premium_result.dart @@ -1,19 +1,19 @@ -class PremiumResult { - final String sessionId; - final List scopes; - final String login; - - PremiumResult({ - required this.sessionId, - required this.scopes, - required this.login, - }); - - factory PremiumResult.fromJson(Map json) { - return PremiumResult( - sessionId: json["session_id"] ?? "", - scopes: (json["scopes"] ?? []).cast(), - login: json["customer_id"] ?? "", - ); - } -} +class PremiumResult { + final String sessionId; + final List scopes; + final String login; + + PremiumResult({ + required this.sessionId, + required this.scopes, + required this.login, + }); + + factory PremiumResult.fromJson(Map json) { + return PremiumResult( + sessionId: json["session_id"] ?? "", + scopes: (json["scopes"] ?? []).cast(), + login: json["customer_id"] ?? "", + ); + } +} diff --git a/lib/models/premium_scopes.dart b/lib/models/premium_scopes.dart index 7d9b906..b9fc1bf 100644 --- a/lib/models/premium_scopes.dart +++ b/lib/models/premium_scopes.dart @@ -1,51 +1,51 @@ -class PremiumScopes { - // everything - static const all = "refilc.plus.*"; - - // idk where it will be but i need it - // static const renameTeachers = "refilc.plus.RENAME_TEACHERS"; - // static const goalPlanner = "refilc.plus.GOAL_PLANNER"; - // static const changeAppIcon = "refilc.plus.CHANGE_APP_ICON"; - - // tier 1 (Kupak) (reFilc+) - static const maxTwoAccounts = "refilc.plus.MAX_TWO_ACCOUNTS"; - static const earlyAccess = "refilc.plus.EARLY_ACCESS"; - static const totalGradeCalculator = "refilc.plus.TOTAL_GRADE_CALCULATOR"; - static const welcomeMessage = "refilc.plus.WELCOME_MESSAGE"; - static const unlimitedSelfNotes = "refilc.plus.UNLIMITED_SELF_NOTES"; - static const customGradeRarities = "refilc.plus.CUSTOM_GRADE_RARITIES"; - static const gradeExporting = "refilc.plus.GRADE_EXPORTING"; - // tier scope - // static const tierCap = "refilc.plus.tier.CAP"; - - // tier 2 (Tinta) (reFilc+ Gold) - static const noAccountLimit = "refilc.plus.NO_ACCOUNT_LIMIT"; - static const appIconChange = "refilc.plus.APP_ICON_CHANGE"; - static const liveActivityColor = "refilc.plus.LIVE_ACTIVITY_COLOR"; - static const customFont = "refilc.plus.CUSTOM_FONT"; - static const timetableNotes = "refilc.plus.TIMETABLE_NOTES"; - static const unlimitedGoalPlanner = "refilc.plus.UNLIMITED_GOAL_PLANNER"; - static const calendarSync = "refilc.plus.CALENDAR_SYNC"; - // tier scope - // static const tierInk = "refilc.plus.tier.INK"; - - // tier 3 (Szivacs) - // cancelled - // tier scope - static const tierSponge = "refilc.plus.tier.SPONGE"; - - // uncategorized - - // old scopes - static const nickname = "refilc.plus.NICKNAME"; - static const gradeStats = "refilc.plus.GRADE_STATS"; - static const customColors = "refilc.plus.CUSTOM_COLORS"; - static const customIcons = "refilc.plus.CUSTOM_ICONS"; - static const renameSubjects = "refilc.plus.RENAME_SUBJECTS"; - static const timetableWidget = "refilc.plus.TIMETALBE_WIDGET"; - static const fsTimetable = "refilc.plus.FS_TIMETABLE"; - - // new new tier scopes - static const tierBasic = "refilc.plus.tier.BASIC"; - static const tierGold = "refilc.plus.tier.GOLD"; -} +class PremiumScopes { + // everything + static const all = "refilc.plus.*"; + + // idk where it will be but i need it + // static const renameTeachers = "refilc.plus.RENAME_TEACHERS"; + // static const goalPlanner = "refilc.plus.GOAL_PLANNER"; + // static const changeAppIcon = "refilc.plus.CHANGE_APP_ICON"; + + // tier 1 (Kupak) (reFilc+) + static const maxTwoAccounts = "refilc.plus.MAX_TWO_ACCOUNTS"; + static const earlyAccess = "refilc.plus.EARLY_ACCESS"; + static const totalGradeCalculator = "refilc.plus.TOTAL_GRADE_CALCULATOR"; + static const welcomeMessage = "refilc.plus.WELCOME_MESSAGE"; + static const unlimitedSelfNotes = "refilc.plus.UNLIMITED_SELF_NOTES"; + static const customGradeRarities = "refilc.plus.CUSTOM_GRADE_RARITIES"; + static const gradeExporting = "refilc.plus.GRADE_EXPORTING"; + // tier scope + // static const tierCap = "refilc.plus.tier.CAP"; + + // tier 2 (Tinta) (reFilc+ Gold) + static const noAccountLimit = "refilc.plus.NO_ACCOUNT_LIMIT"; + static const appIconChange = "refilc.plus.APP_ICON_CHANGE"; + static const liveActivityColor = "refilc.plus.LIVE_ACTIVITY_COLOR"; + static const customFont = "refilc.plus.CUSTOM_FONT"; + static const timetableNotes = "refilc.plus.TIMETABLE_NOTES"; + static const unlimitedGoalPlanner = "refilc.plus.UNLIMITED_GOAL_PLANNER"; + static const calendarSync = "refilc.plus.CALENDAR_SYNC"; + // tier scope + // static const tierInk = "refilc.plus.tier.INK"; + + // tier 3 (Szivacs) + // cancelled + // tier scope + static const tierSponge = "refilc.plus.tier.SPONGE"; + + // uncategorized + + // old scopes + static const nickname = "refilc.plus.NICKNAME"; + static const gradeStats = "refilc.plus.GRADE_STATS"; + static const customColors = "refilc.plus.CUSTOM_COLORS"; + static const customIcons = "refilc.plus.CUSTOM_ICONS"; + static const renameSubjects = "refilc.plus.RENAME_SUBJECTS"; + static const timetableWidget = "refilc.plus.TIMETALBE_WIDGET"; + static const fsTimetable = "refilc.plus.FS_TIMETABLE"; + + // new new tier scopes + static const tierBasic = "refilc.plus.tier.BASIC"; + static const tierGold = "refilc.plus.tier.GOLD"; +} diff --git a/lib/providers/goal_provider.dart b/lib/providers/goal_provider.dart index 1ce7c3d..5a852e3 100644 --- a/lib/providers/goal_provider.dart +++ b/lib/providers/goal_provider.dart @@ -1,68 +1,68 @@ -import 'package:refilc/api/providers/database_provider.dart'; -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc_kreta_api/models/subject.dart'; -import 'package:refilc_kreta_api/providers/grade_provider.dart'; -import 'package:flutter/widgets.dart'; - -class GoalProvider extends ChangeNotifier { - final DatabaseProvider _db; - final UserProvider _user; - - late bool _done = false; - late GradeSubject? _doneSubject; - - bool get hasDoneGoals => _done; - GradeSubject? get doneSubject => _doneSubject; - - GoalProvider({ - required DatabaseProvider database, - required UserProvider user, - }) : _db = database, - _user = user; - - Future fetchDone({required GradeProvider gradeProvider}) async { - var goalAvgs = await _db.userQuery.subjectGoalAverages(userId: _user.id!); - var beforeAvgs = await _db.userQuery.subjectGoalBefores(userId: _user.id!); - - List subjects = gradeProvider.grades - .map((e) => e.subject) - .toSet() - .toList() - ..sort((a, b) => a.name.compareTo(b.name)); - - goalAvgs.forEach((k, v) { - if (beforeAvgs[k] == v) { - _done = true; - _doneSubject = subjects.where((e) => e.id == k).toList()[0]; - - notifyListeners(); - } - }); - } - - void lock() { - _done = false; - _doneSubject = null; - } - - Future clearGoal(GradeSubject subject) async { - final goalPlans = await _db.userQuery.subjectGoalPlans(userId: _user.id!); - final goalAvgs = await _db.userQuery.subjectGoalAverages(userId: _user.id!); - final goalBeforeGrades = - await _db.userQuery.subjectGoalBefores(userId: _user.id!); - final goalPinDates = - await _db.userQuery.subjectGoalPinDates(userId: _user.id!); - - goalPlans.remove(subject.id); - goalAvgs.remove(subject.id); - goalBeforeGrades.remove(subject.id); - goalPinDates.remove(subject.id); - - await _db.userStore.storeSubjectGoalPlans(goalPlans, userId: _user.id!); - await _db.userStore.storeSubjectGoalAverages(goalAvgs, userId: _user.id!); - await _db.userStore - .storeSubjectGoalBefores(goalBeforeGrades, userId: _user.id!); - await _db.userStore - .storeSubjectGoalPinDates(goalPinDates, userId: _user.id!); - } -} +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc_kreta_api/models/subject.dart'; +import 'package:refilc_kreta_api/providers/grade_provider.dart'; +import 'package:flutter/widgets.dart'; + +class GoalProvider extends ChangeNotifier { + final DatabaseProvider _db; + final UserProvider _user; + + late bool _done = false; + late GradeSubject? _doneSubject; + + bool get hasDoneGoals => _done; + GradeSubject? get doneSubject => _doneSubject; + + GoalProvider({ + required DatabaseProvider database, + required UserProvider user, + }) : _db = database, + _user = user; + + Future fetchDone({required GradeProvider gradeProvider}) async { + var goalAvgs = await _db.userQuery.subjectGoalAverages(userId: _user.id!); + var beforeAvgs = await _db.userQuery.subjectGoalBefores(userId: _user.id!); + + List subjects = gradeProvider.grades + .map((e) => e.subject) + .toSet() + .toList() + ..sort((a, b) => a.name.compareTo(b.name)); + + goalAvgs.forEach((k, v) { + if (beforeAvgs[k] == v) { + _done = true; + _doneSubject = subjects.where((e) => e.id == k).toList()[0]; + + notifyListeners(); + } + }); + } + + void lock() { + _done = false; + _doneSubject = null; + } + + Future clearGoal(GradeSubject subject) async { + final goalPlans = await _db.userQuery.subjectGoalPlans(userId: _user.id!); + final goalAvgs = await _db.userQuery.subjectGoalAverages(userId: _user.id!); + final goalBeforeGrades = + await _db.userQuery.subjectGoalBefores(userId: _user.id!); + final goalPinDates = + await _db.userQuery.subjectGoalPinDates(userId: _user.id!); + + goalPlans.remove(subject.id); + goalAvgs.remove(subject.id); + goalBeforeGrades.remove(subject.id); + goalPinDates.remove(subject.id); + + await _db.userStore.storeSubjectGoalPlans(goalPlans, userId: _user.id!); + await _db.userStore.storeSubjectGoalAverages(goalAvgs, userId: _user.id!); + await _db.userStore + .storeSubjectGoalBefores(goalBeforeGrades, userId: _user.id!); + await _db.userStore + .storeSubjectGoalPinDates(goalPinDates, userId: _user.id!); + } +} diff --git a/lib/providers/plus_provider.dart b/lib/providers/plus_provider.dart index 6e83095..bdcf4b8 100644 --- a/lib/providers/plus_provider.dart +++ b/lib/providers/plus_provider.dart @@ -1,31 +1,31 @@ -import 'package:refilc/models/settings.dart'; -import 'package:refilc_plus/api/auth.dart'; -import 'package:refilc_plus/models/premium_scopes.dart'; -import 'package:flutter/widgets.dart'; - -class PlusProvider extends ChangeNotifier { - final SettingsProvider _settings; - List get scopes => _settings.premiumScopes; - // bool hasScope(String scope) => false; - bool hasScope(String scope) => - scopes.contains(scope) || scopes.contains(PremiumScopes.all); - String get accessToken => _settings.premiumAccessToken; - String get login => _settings.premiumLogin; - bool get hasPremium => - _settings.plusSessionId != "" && _settings.premiumScopes.isNotEmpty; - - late final PremiumAuth _auth; - PremiumAuth get auth => _auth; - - PlusProvider({required SettingsProvider settings}) : _settings = settings { - _auth = PremiumAuth(settings: _settings); - _settings.addListener(() { - notifyListeners(); - }); - } - - Future activate({bool removePremium = false}) async { - await _auth.refreshAuth(removePremium: removePremium); - notifyListeners(); - } -} +import 'package:refilc/models/settings.dart'; +import 'package:refilc_plus/api/auth.dart'; +import 'package:refilc_plus/models/premium_scopes.dart'; +import 'package:flutter/widgets.dart'; + +class PlusProvider extends ChangeNotifier { + final SettingsProvider _settings; + List get scopes => _settings.premiumScopes; + // bool hasScope(String scope) => false; + bool hasScope(String scope) => + scopes.contains(scope) || scopes.contains(PremiumScopes.all); + String get accessToken => _settings.premiumAccessToken; + String get login => _settings.premiumLogin; + bool get hasPremium => + _settings.plusSessionId != "" && _settings.premiumScopes.isNotEmpty; + + late final PremiumAuth _auth; + PremiumAuth get auth => _auth; + + PlusProvider({required SettingsProvider settings}) : _settings = settings { + _auth = PremiumAuth(settings: _settings); + _settings.addListener(() { + notifyListeners(); + }); + } + + Future activate({bool removePremium = false}) async { + await _auth.refreshAuth(removePremium: removePremium); + notifyListeners(); + } +} diff --git a/lib/ui/mobile/goal_planner/goal_complete_modal.dart b/lib/ui/mobile/goal_planner/goal_complete_modal.dart index 8af6bf7..3d15d63 100644 --- a/lib/ui/mobile/goal_planner/goal_complete_modal.dart +++ b/lib/ui/mobile/goal_planner/goal_complete_modal.dart @@ -1,253 +1,253 @@ -import 'package:refilc/api/providers/database_provider.dart'; -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_kreta_api/models/subject.dart'; -import 'package:refilc_mobile_ui/common/average_display.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_state_screen.i18n.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class GoalCompleteModal extends StatelessWidget { - const GoalCompleteModal( - this.subject, { - super.key, - required this.user, - required this.database, - required this.goalAverage, - required this.beforeAverage, - required this.averageDifference, - }); - - final UserProvider user; - final DatabaseProvider database; - final GradeSubject subject; - - final double goalAverage; - final double beforeAverage; - final double averageDifference; - - @override - Widget build(BuildContext context) { - return Dialog( - elevation: 0, - backgroundColor: Colors.transparent, - child: Container( - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(20.0), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: double.infinity, - decoration: BoxDecoration( - image: const DecorationImage( - image: AssetImage('assets/images/static_confetti.png'), - fit: BoxFit.fitWidth, - alignment: Alignment.topCenter, - ), - color: Colors.white, - borderRadius: BorderRadius.circular(10.0), - ), - padding: const EdgeInsets.all(6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - goalAverage.toStringAsFixed(1), - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 64.0, - fontWeight: FontWeight.w800, - ), - ), - // const SizedBox(width: 10.0), - // Icon( - // SubjectIcon.resolveVariant( - // subject: subject, context: context), - // color: Colors.white, - // size: 64.0, - // ), - ], - ), - ), - const SizedBox(height: 10.0), - Text( - 'congrats_title'.i18n, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 27.0, - fontWeight: FontWeight.w700, - height: 1.2, - color: AppColors.of(context).text, - ), - ), - Text( - 'goal_reached'.i18n.fill(['20']), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.w500, - height: 1.1, - color: AppColors.of(context).text, - ), - ), - const SizedBox(height: 18.0), - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'started_at'.i18n, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 17.0, - fontWeight: FontWeight.w500, - color: AppColors.of(context).text, - ), - ), - const SizedBox(width: 5.0), - AverageDisplay( - average: beforeAverage, - ), - ], - ), - Text( - 'improved_by'.i18n.fill([ - '${averageDifference.toStringAsFixed(2)}%', - ]), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 17.0, - fontWeight: FontWeight.w500, - color: AppColors.of(context).text, - ), - ), - ], - ), - const SizedBox(height: 20.0), - Column( - children: [ - GestureDetector( - onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Hamarosan...")), - ); - }, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - gradient: const LinearGradient( - colors: [ - Color(0xFFCAECFA), - Color(0xFFF4D9EE), - Color(0xFFF3EFDA), - ], - stops: [0.0, 0.53, 1.0], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'detailed_stats'.i18n, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w700, - color: Color(0xFF691A9B), - ), - ), - ), - ), - ), - const SizedBox(height: 10.0), - GestureDetector( - onTap: () { - Navigator.of(context).pop(); - }, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - color: const Color.fromARGB(38, 131, 131, 131), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - 'later'.i18n, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w700, - color: AppColors.of(context).text, - ), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ); - - // return Padding( - // padding: const EdgeInsets.symmetric(vertical: 100.0, horizontal: 32.0), - // child: Material( - // borderRadius: BorderRadius.circular(12.0), - // child: Padding( - // padding: const EdgeInsets.all(12.0), - // child: Column( - // children: [ - // // content or idk - // ], - // ), - // ), - // ), - // ); - } - - static Future show( - GradeSubject subject, { - required BuildContext context, - }) async { - UserProvider user = Provider.of(context, listen: false); - DatabaseProvider db = Provider.of(context, listen: false); - - var goalAvgRes = await db.userQuery.subjectGoalAverages(userId: user.id!); - var beforeAvgRes = await db.userQuery.subjectGoalBefores(userId: user.id!); - - //DateTime goalPinDate = DateTime.parse((await db.userQuery.subjectGoalPinDates(userId: user.id!))[widget.subject.id]!); - - String? goalAvgStr = goalAvgRes[subject.id]; - String? beforeAvgStr = beforeAvgRes[subject.id]; - double goalAvg = double.parse(goalAvgStr ?? '0.0'); - double beforeAvg = double.parse(beforeAvgStr ?? '0.0'); - - double avgDifference = ((goalAvg - beforeAvg) / beforeAvg.abs()) * 100; - - return showDialog( - // ignore: use_build_context_synchronously - context: context, - builder: (context) => GoalCompleteModal( - subject, - user: user, - database: db, - goalAverage: goalAvg, - beforeAverage: beforeAvg, - averageDifference: avgDifference, - ), - barrierDismissible: false, - ); - } -} +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_kreta_api/models/subject.dart'; +import 'package:refilc_mobile_ui/common/average_display.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_state_screen.i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class GoalCompleteModal extends StatelessWidget { + const GoalCompleteModal( + this.subject, { + super.key, + required this.user, + required this.database, + required this.goalAverage, + required this.beforeAverage, + required this.averageDifference, + }); + + final UserProvider user; + final DatabaseProvider database; + final GradeSubject subject; + + final double goalAverage; + final double beforeAverage; + final double averageDifference; + + @override + Widget build(BuildContext context) { + return Dialog( + elevation: 0, + backgroundColor: Colors.transparent, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(20.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/images/static_confetti.png'), + fit: BoxFit.fitWidth, + alignment: Alignment.topCenter, + ), + color: Colors.white, + borderRadius: BorderRadius.circular(10.0), + ), + padding: const EdgeInsets.all(6.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + goalAverage.toStringAsFixed(1), + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 64.0, + fontWeight: FontWeight.w800, + ), + ), + // const SizedBox(width: 10.0), + // Icon( + // SubjectIcon.resolveVariant( + // subject: subject, context: context), + // color: Colors.white, + // size: 64.0, + // ), + ], + ), + ), + const SizedBox(height: 10.0), + Text( + 'congrats_title'.i18n, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 27.0, + fontWeight: FontWeight.w700, + height: 1.2, + color: AppColors.of(context).text, + ), + ), + Text( + 'goal_reached'.i18n.fill(['20']), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w500, + height: 1.1, + color: AppColors.of(context).text, + ), + ), + const SizedBox(height: 18.0), + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'started_at'.i18n, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 17.0, + fontWeight: FontWeight.w500, + color: AppColors.of(context).text, + ), + ), + const SizedBox(width: 5.0), + AverageDisplay( + average: beforeAverage, + ), + ], + ), + Text( + 'improved_by'.i18n.fill([ + '${averageDifference.toStringAsFixed(2)}%', + ]), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 17.0, + fontWeight: FontWeight.w500, + color: AppColors.of(context).text, + ), + ), + ], + ), + const SizedBox(height: 20.0), + Column( + children: [ + GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Hamarosan...")), + ); + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + gradient: const LinearGradient( + colors: [ + Color(0xFFCAECFA), + Color(0xFFF4D9EE), + Color(0xFFF3EFDA), + ], + stops: [0.0, 0.53, 1.0], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'detailed_stats'.i18n, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w700, + color: Color(0xFF691A9B), + ), + ), + ), + ), + ), + const SizedBox(height: 10.0), + GestureDetector( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + color: const Color.fromARGB(38, 131, 131, 131), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'later'.i18n, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w700, + color: AppColors.of(context).text, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + + // return Padding( + // padding: const EdgeInsets.symmetric(vertical: 100.0, horizontal: 32.0), + // child: Material( + // borderRadius: BorderRadius.circular(12.0), + // child: Padding( + // padding: const EdgeInsets.all(12.0), + // child: Column( + // children: [ + // // content or idk + // ], + // ), + // ), + // ), + // ); + } + + static Future show( + GradeSubject subject, { + required BuildContext context, + }) async { + UserProvider user = Provider.of(context, listen: false); + DatabaseProvider db = Provider.of(context, listen: false); + + var goalAvgRes = await db.userQuery.subjectGoalAverages(userId: user.id!); + var beforeAvgRes = await db.userQuery.subjectGoalBefores(userId: user.id!); + + //DateTime goalPinDate = DateTime.parse((await db.userQuery.subjectGoalPinDates(userId: user.id!))[widget.subject.id]!); + + String? goalAvgStr = goalAvgRes[subject.id]; + String? beforeAvgStr = beforeAvgRes[subject.id]; + double goalAvg = double.parse(goalAvgStr ?? '0.0'); + double beforeAvg = double.parse(beforeAvgStr ?? '0.0'); + + double avgDifference = ((goalAvg - beforeAvg) / beforeAvg.abs()) * 100; + + return showDialog( + // ignore: use_build_context_synchronously + context: context, + builder: (context) => GoalCompleteModal( + subject, + user: user, + database: db, + goalAverage: goalAvg, + beforeAverage: beforeAvg, + averageDifference: avgDifference, + ), + barrierDismissible: false, + ); + } +} diff --git a/lib/ui/mobile/goal_planner/goal_input.dart b/lib/ui/mobile/goal_planner/goal_input.dart index b38bce0..1bf3cdb 100644 --- a/lib/ui/mobile/goal_planner/goal_input.dart +++ b/lib/ui/mobile/goal_planner/goal_input.dart @@ -1,204 +1,204 @@ -import 'package:refilc/models/settings.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -class GoalInput extends StatelessWidget { - const GoalInput( - {super.key, - required this.currentAverage, - required this.value, - required this.onChanged}); - - final double currentAverage; - final double value; - final void Function(double value) onChanged; - - void offsetToValue(Offset offset, Size size) { - double v = ((offset.dx / size.width * 4 + 1) * 10).round() / 10; - v = v.clamp(1.5, 5); - v = v.clamp(((currentAverage * 10).round() / 10), 5); - setValue(v); - } - - void setValue(double v) { - if (v != value) { - HapticFeedback.lightImpact(); - } - onChanged(v); - } - - @override - Widget build(BuildContext context) { - SettingsProvider settings = Provider.of(context); - - List presets = [2, 3, 4, 5]; - presets = presets.where((e) => gradeToAvg(e) > currentAverage).toList(); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - LayoutBuilder(builder: (context, size) { - return GestureDetector( - onTapDown: (details) { - offsetToValue(details.localPosition, size.biggest); - }, - onHorizontalDragUpdate: (details) { - offsetToValue(details.localPosition, size.biggest); - }, - child: SizedBox( - height: 32.0, - width: double.infinity, - child: Padding( - padding: const EdgeInsets.only(right: 20.0), - child: CustomPaint( - painter: GoalSliderPainter( - value: (value - 1) / 4, - settings: settings, - goalValue: value), - ), - ), - ), - ); - }), - // const SizedBox(height: 12.0), - // Row( - // mainAxisAlignment: MainAxisAlignment.center, - // children: presets.map((e) { - // final pv = (value * 10).round() / 10; - // final selected = gradeToAvg(e) == pv; - // return Padding( - // padding: const EdgeInsets.symmetric(horizontal: 12.0), - // child: Container( - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(99.0), - // color: - // gradeColor(e, settings).withOpacity(selected ? 1.0 : 0.2), - // border: Border.all(color: gradeColor(e, settings), width: 4), - // ), - // child: Material( - // type: MaterialType.transparency, - // child: InkWell( - // borderRadius: BorderRadius.circular(99.0), - // onTap: () => setValue(gradeToAvg(e)), - // child: Padding( - // padding: const EdgeInsets.symmetric( - // vertical: 2.0, horizontal: 24.0), - // child: Text( - // e.toString(), - // style: TextStyle( - // color: - // selected ? Colors.white : gradeColor(e, settings), - // fontWeight: FontWeight.bold, - // fontSize: 24.0, - // ), - // ), - // ), - // ), - // ), - // ), - // ); - // }).toList(), - // ) - ], - ); - } -} - -class GoalSliderPainter extends CustomPainter { - final double value; - final SettingsProvider settings; - final double goalValue; - - GoalSliderPainter( - {required this.value, required this.settings, required this.goalValue}); - - @override - void paint(Canvas canvas, Size size) { - final radius = size.height / 2; - const cpadding = 4; - final rect = Rect.fromLTWH(0, 0, size.width + radius, size.height); - // final vrect = Rect.fromLTWH(0, 0, size.width * value + radius, size.height); - canvas.drawRRect( - RRect.fromRectAndRadius( - rect, - const Radius.circular(99.0), - ), - Paint()..color = Colors.black.withOpacity(.1), - ); - canvas.drawRRect( - RRect.fromRectAndRadius( - rect, - const Radius.circular(99.0), - ), - Paint() - ..shader = LinearGradient(colors: [ - settings.gradeColors[0], - settings.gradeColors[1], - settings.gradeColors[2], - settings.gradeColors[3], - settings.gradeColors[4], - ]).createShader(rect), - ); - - double w = size.width + radius; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH( - (w - (w * 0.986)) / 2, - (size.height - (size.height * 0.85)) / 2, - w * 0.986, - size.height * 0.85), - const Radius.circular(99.0), - ), - Paint()..color = Colors.white.withOpacity(.8), - ); - - canvas.drawOval( - Rect.fromCircle( - center: Offset(size.width * value, size.height / 2), - radius: radius - cpadding), - Paint()..color = Colors.white, - ); - canvas.drawOval( - Rect.fromCircle( - center: Offset(size.width * value, size.height / 2), - radius: (radius - cpadding) * 0.8), - Paint()..color = gradeColor(goalValue.round(), settings), - ); - - for (int i = 1; i < 4; i++) { - canvas.drawOval( - Rect.fromCircle( - center: Offset(size.width / 4 * i, size.height / 2), radius: 4), - Paint()..color = Colors.white.withOpacity(.6), - ); - } - } - - @override - bool shouldRepaint(GoalSliderPainter oldDelegate) { - return oldDelegate.value != value; - } -} - -double gradeToAvg(int grade) { - return grade - 0.5; -} - -Color gradeColor(int grade, SettingsProvider settings) { - // return [ - // const Color(0xffFF3B30), - // const Color(0xffFF9F0A), - // const Color(0xffFFD60A), - // const Color(0xff34C759), - // const Color(0xff247665), - // ].elementAt(grade.clamp(1, 5) - 1); - return [ - settings.gradeColors[0], - settings.gradeColors[1], - settings.gradeColors[2], - settings.gradeColors[3], - settings.gradeColors[4], - ].elementAt(grade.clamp(1, 5) - 1); -} +import 'package:refilc/models/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class GoalInput extends StatelessWidget { + const GoalInput( + {super.key, + required this.currentAverage, + required this.value, + required this.onChanged}); + + final double currentAverage; + final double value; + final void Function(double value) onChanged; + + void offsetToValue(Offset offset, Size size) { + double v = ((offset.dx / size.width * 4 + 1) * 10).round() / 10; + v = v.clamp(1.5, 5); + v = v.clamp(((currentAverage * 10).round() / 10), 5); + setValue(v); + } + + void setValue(double v) { + if (v != value) { + HapticFeedback.lightImpact(); + } + onChanged(v); + } + + @override + Widget build(BuildContext context) { + SettingsProvider settings = Provider.of(context); + + List presets = [2, 3, 4, 5]; + presets = presets.where((e) => gradeToAvg(e) > currentAverage).toList(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + LayoutBuilder(builder: (context, size) { + return GestureDetector( + onTapDown: (details) { + offsetToValue(details.localPosition, size.biggest); + }, + onHorizontalDragUpdate: (details) { + offsetToValue(details.localPosition, size.biggest); + }, + child: SizedBox( + height: 32.0, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.only(right: 20.0), + child: CustomPaint( + painter: GoalSliderPainter( + value: (value - 1) / 4, + settings: settings, + goalValue: value), + ), + ), + ), + ); + }), + // const SizedBox(height: 12.0), + // Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: presets.map((e) { + // final pv = (value * 10).round() / 10; + // final selected = gradeToAvg(e) == pv; + // return Padding( + // padding: const EdgeInsets.symmetric(horizontal: 12.0), + // child: Container( + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(99.0), + // color: + // gradeColor(e, settings).withOpacity(selected ? 1.0 : 0.2), + // border: Border.all(color: gradeColor(e, settings), width: 4), + // ), + // child: Material( + // type: MaterialType.transparency, + // child: InkWell( + // borderRadius: BorderRadius.circular(99.0), + // onTap: () => setValue(gradeToAvg(e)), + // child: Padding( + // padding: const EdgeInsets.symmetric( + // vertical: 2.0, horizontal: 24.0), + // child: Text( + // e.toString(), + // style: TextStyle( + // color: + // selected ? Colors.white : gradeColor(e, settings), + // fontWeight: FontWeight.bold, + // fontSize: 24.0, + // ), + // ), + // ), + // ), + // ), + // ), + // ); + // }).toList(), + // ) + ], + ); + } +} + +class GoalSliderPainter extends CustomPainter { + final double value; + final SettingsProvider settings; + final double goalValue; + + GoalSliderPainter( + {required this.value, required this.settings, required this.goalValue}); + + @override + void paint(Canvas canvas, Size size) { + final radius = size.height / 2; + const cpadding = 4; + final rect = Rect.fromLTWH(0, 0, size.width + radius, size.height); + // final vrect = Rect.fromLTWH(0, 0, size.width * value + radius, size.height); + canvas.drawRRect( + RRect.fromRectAndRadius( + rect, + const Radius.circular(99.0), + ), + Paint()..color = Colors.black.withOpacity(.1), + ); + canvas.drawRRect( + RRect.fromRectAndRadius( + rect, + const Radius.circular(99.0), + ), + Paint() + ..shader = LinearGradient(colors: [ + settings.gradeColors[0], + settings.gradeColors[1], + settings.gradeColors[2], + settings.gradeColors[3], + settings.gradeColors[4], + ]).createShader(rect), + ); + + double w = size.width + radius; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH( + (w - (w * 0.986)) / 2, + (size.height - (size.height * 0.85)) / 2, + w * 0.986, + size.height * 0.85), + const Radius.circular(99.0), + ), + Paint()..color = Colors.white.withOpacity(.8), + ); + + canvas.drawOval( + Rect.fromCircle( + center: Offset(size.width * value, size.height / 2), + radius: radius - cpadding), + Paint()..color = Colors.white, + ); + canvas.drawOval( + Rect.fromCircle( + center: Offset(size.width * value, size.height / 2), + radius: (radius - cpadding) * 0.8), + Paint()..color = gradeColor(goalValue.round(), settings), + ); + + for (int i = 1; i < 4; i++) { + canvas.drawOval( + Rect.fromCircle( + center: Offset(size.width / 4 * i, size.height / 2), radius: 4), + Paint()..color = Colors.white.withOpacity(.6), + ); + } + } + + @override + bool shouldRepaint(GoalSliderPainter oldDelegate) { + return oldDelegate.value != value; + } +} + +double gradeToAvg(int grade) { + return grade - 0.5; +} + +Color gradeColor(int grade, SettingsProvider settings) { + // return [ + // const Color(0xffFF3B30), + // const Color(0xffFF9F0A), + // const Color(0xffFFD60A), + // const Color(0xff34C759), + // const Color(0xff247665), + // ].elementAt(grade.clamp(1, 5) - 1); + return [ + settings.gradeColors[0], + settings.gradeColors[1], + settings.gradeColors[2], + settings.gradeColors[3], + settings.gradeColors[4], + ].elementAt(grade.clamp(1, 5) - 1); +} diff --git a/lib/ui/mobile/goal_planner/goal_planner.dart b/lib/ui/mobile/goal_planner/goal_planner.dart index 1a649e2..907a8c6 100644 --- a/lib/ui/mobile/goal_planner/goal_planner.dart +++ b/lib/ui/mobile/goal_planner/goal_planner.dart @@ -1,191 +1,191 @@ -/* - * Maintainer: DarK - * Translated from C version - * Minimal Working Fixed @ 2022.12.25 - * ##Please do NOT modify if you don't know whats going on## - * - * Issue: #59 - * - * Future changes / ideas: - * - `best` should be configurable - */ -import 'dart:math'; -import 'package:refilc_kreta_api/models/category.dart'; -import 'package:refilc_kreta_api/models/grade.dart'; -import 'package:refilc_kreta_api/models/subject.dart'; -import 'package:refilc_kreta_api/models/teacher.dart'; -import 'package:flutter/foundation.dart' show listEquals; - -/// Generate list of grades that achieve the wanted goal. -/// After generating possible options, it (when doing so would NOT result in empty list) filters with two criteria: -/// - Plan should not contain more than 15 grades -/// - Plan should not contain only one type of grade -/// -/// **Usage**: -/// -/// ```dart -/// List GoalPlanner(double goal, List grades).solve().plan -/// ``` -class GoalPlanner { - final double goal; - final List grades; - List plans = []; - GoalPlanner(this.goal, this.grades); - - bool _allowed(int grade) => grade > goal; - - void _generate(Generator g) { - // Exit condition 1: Generator has working plan. - if (g.currentAvg.avg >= goal) { - plans.add(Plan(g.plan)); - return; - } - // Exit condition 2: Generator plan will never work. - if (!_allowed(g.gradeToAdd)) { - return; - } - - for (int i = g.max; i >= 0; i--) { - int newGradeToAdd = g.gradeToAdd - 1; - List newPlan = - GoalPlannerHelper._addToList(g.plan, g.gradeToAdd, i); - - Avg newAvg = GoalPlannerHelper._addToAvg(g.currentAvg, g.gradeToAdd, i); - int newN = GoalPlannerHelper.howManyNeeded( - newGradeToAdd, - grades + - newPlan - .map((e) => Grade( - id: '', - date: DateTime(0), - value: GradeValue(e, '', '', 100), - teacher: Teacher.fromString(''), - description: '', - form: '', - groupId: '', - type: GradeType.midYear, - subject: GradeSubject.fromJson({}), - mode: Category.fromJson({}), - seenDate: DateTime(0), - writeDate: DateTime(0), - )) - .toList(), - goal); - - _generate(Generator(newGradeToAdd, newN, newAvg, newPlan)); - } - } - - List solve() { - _generate( - Generator( - 5, - GoalPlannerHelper.howManyNeeded( - 5, - grades, - goal, - ), - Avg(GoalPlannerHelper.averageEvals(grades), - GoalPlannerHelper.weightSum(grades)), - [], - ), - ); - - // Calculate Statistics - for (var e in plans) { - e.sum = e.plan.fold(0, (int a, b) => a + b); - e.avg = e.sum / e.plan.length; - e.sigma = sqrt( - e.plan.map((i) => pow(i - e.avg, 2)).fold(0, (num a, b) => a + b) / - e.plan.length); - } - - // filter without aggression - if (plans.where((e) => e.plan.length < 30).isNotEmpty) { - plans.removeWhere((e) => !(e.plan.length < 30)); - } - if (plans.where((e) => e.sigma > 1).isNotEmpty) { - plans.removeWhere((e) => !(e.sigma > 1)); - } - - return plans; - } -} - -class Avg { - final double avg; - final double n; - - Avg(this.avg, this.n); -} - -class Generator { - final int gradeToAdd; - final int max; - final Avg currentAvg; - final List plan; - - Generator(this.gradeToAdd, this.max, this.currentAvg, this.plan); -} - -class Plan { - final List plan; - int sum = 0; - double avg = 0; - int med = 0; // currently - int mod = 0; // unused - double sigma = 0; - - Plan(this.plan); - - String get dbString { - var finalString = ''; - for (var i in plan) { - finalString += "$i,"; - } - return finalString; - } - - @override - bool operator ==(other) => other is Plan && listEquals(plan, other.plan); - - @override - int get hashCode => Object.hashAll(plan); -} - -class GoalPlannerHelper { - static Avg _addToAvg(Avg base, int grade, int n) => - Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n); - - static List _addToList(List l, T e, int n) { - if (n == 0) return l; - List tmp = l; - for (int i = 0; i < n; i++) { - tmp = tmp + [e]; - } - return tmp; - } - - static int howManyNeeded(int grade, List base, double goal) { - double avg = averageEvals(base); - double wsum = weightSum(base); - if (avg >= goal) return 0; - if (grade * 1.0 == goal) return -1; - int candidate = (wsum * (avg - goal) / (goal - grade)).floor(); - return (candidate * grade + avg * wsum) / (candidate + wsum) < goal - ? candidate + 1 - : candidate; - } - - static double averageEvals(List grades, {bool finalAvg = false}) { - double average = grades - .map((e) => e.value.value * e.value.weight / 100.0) - .fold(0.0, (double a, double b) => a + b) / - weightSum(grades, finalAvg: finalAvg); - return average.isNaN ? 0.0 : average; - } - - static double weightSum(List grades, {bool finalAvg = false}) => grades - .map((e) => finalAvg ? 1 : e.value.weight / 100) - .fold(0, (a, b) => a + b); -} +/* + * Maintainer: DarK + * Translated from C version + * Minimal Working Fixed @ 2022.12.25 + * ##Please do NOT modify if you don't know whats going on## + * + * Issue: #59 + * + * Future changes / ideas: + * - `best` should be configurable + */ +import 'dart:math'; +import 'package:refilc_kreta_api/models/category.dart'; +import 'package:refilc_kreta_api/models/grade.dart'; +import 'package:refilc_kreta_api/models/subject.dart'; +import 'package:refilc_kreta_api/models/teacher.dart'; +import 'package:flutter/foundation.dart' show listEquals; + +/// Generate list of grades that achieve the wanted goal. +/// After generating possible options, it (when doing so would NOT result in empty list) filters with two criteria: +/// - Plan should not contain more than 15 grades +/// - Plan should not contain only one type of grade +/// +/// **Usage**: +/// +/// ```dart +/// List GoalPlanner(double goal, List grades).solve().plan +/// ``` +class GoalPlanner { + final double goal; + final List grades; + List plans = []; + GoalPlanner(this.goal, this.grades); + + bool _allowed(int grade) => grade > goal; + + void _generate(Generator g) { + // Exit condition 1: Generator has working plan. + if (g.currentAvg.avg >= goal) { + plans.add(Plan(g.plan)); + return; + } + // Exit condition 2: Generator plan will never work. + if (!_allowed(g.gradeToAdd)) { + return; + } + + for (int i = g.max; i >= 0; i--) { + int newGradeToAdd = g.gradeToAdd - 1; + List newPlan = + GoalPlannerHelper._addToList(g.plan, g.gradeToAdd, i); + + Avg newAvg = GoalPlannerHelper._addToAvg(g.currentAvg, g.gradeToAdd, i); + int newN = GoalPlannerHelper.howManyNeeded( + newGradeToAdd, + grades + + newPlan + .map((e) => Grade( + id: '', + date: DateTime(0), + value: GradeValue(e, '', '', 100), + teacher: Teacher.fromString(''), + description: '', + form: '', + groupId: '', + type: GradeType.midYear, + subject: GradeSubject.fromJson({}), + mode: Category.fromJson({}), + seenDate: DateTime(0), + writeDate: DateTime(0), + )) + .toList(), + goal); + + _generate(Generator(newGradeToAdd, newN, newAvg, newPlan)); + } + } + + List solve() { + _generate( + Generator( + 5, + GoalPlannerHelper.howManyNeeded( + 5, + grades, + goal, + ), + Avg(GoalPlannerHelper.averageEvals(grades), + GoalPlannerHelper.weightSum(grades)), + [], + ), + ); + + // Calculate Statistics + for (var e in plans) { + e.sum = e.plan.fold(0, (int a, b) => a + b); + e.avg = e.sum / e.plan.length; + e.sigma = sqrt( + e.plan.map((i) => pow(i - e.avg, 2)).fold(0, (num a, b) => a + b) / + e.plan.length); + } + + // filter without aggression + if (plans.where((e) => e.plan.length < 30).isNotEmpty) { + plans.removeWhere((e) => !(e.plan.length < 30)); + } + if (plans.where((e) => e.sigma > 1).isNotEmpty) { + plans.removeWhere((e) => !(e.sigma > 1)); + } + + return plans; + } +} + +class Avg { + final double avg; + final double n; + + Avg(this.avg, this.n); +} + +class Generator { + final int gradeToAdd; + final int max; + final Avg currentAvg; + final List plan; + + Generator(this.gradeToAdd, this.max, this.currentAvg, this.plan); +} + +class Plan { + final List plan; + int sum = 0; + double avg = 0; + int med = 0; // currently + int mod = 0; // unused + double sigma = 0; + + Plan(this.plan); + + String get dbString { + var finalString = ''; + for (var i in plan) { + finalString += "$i,"; + } + return finalString; + } + + @override + bool operator ==(other) => other is Plan && listEquals(plan, other.plan); + + @override + int get hashCode => Object.hashAll(plan); +} + +class GoalPlannerHelper { + static Avg _addToAvg(Avg base, int grade, int n) => + Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n); + + static List _addToList(List l, T e, int n) { + if (n == 0) return l; + List tmp = l; + for (int i = 0; i < n; i++) { + tmp = tmp + [e]; + } + return tmp; + } + + static int howManyNeeded(int grade, List base, double goal) { + double avg = averageEvals(base); + double wsum = weightSum(base); + if (avg >= goal) return 0; + if (grade * 1.0 == goal) return -1; + int candidate = (wsum * (avg - goal) / (goal - grade)).floor(); + return (candidate * grade + avg * wsum) / (candidate + wsum) < goal + ? candidate + 1 + : candidate; + } + + static double averageEvals(List grades, {bool finalAvg = false}) { + double average = grades + .map((e) => e.value.value * e.value.weight / 100.0) + .fold(0.0, (double a, double b) => a + b) / + weightSum(grades, finalAvg: finalAvg); + return average.isNaN ? 0.0 : average; + } + + static double weightSum(List grades, {bool finalAvg = false}) => grades + .map((e) => finalAvg ? 1 : e.value.weight / 100) + .fold(0, (a, b) => a + b); +} diff --git a/lib/ui/mobile/goal_planner/goal_planner_screen.dart b/lib/ui/mobile/goal_planner/goal_planner_screen.dart index 16a71a2..e089628 100644 --- a/lib/ui/mobile/goal_planner/goal_planner_screen.dart +++ b/lib/ui/mobile/goal_planner/goal_planner_screen.dart @@ -1,453 +1,453 @@ -import 'package:refilc/api/providers/database_provider.dart'; -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/helpers/average_helper.dart'; -import 'package:refilc/helpers/subject.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:refilc_kreta_api/models/grade.dart'; -import 'package:refilc_kreta_api/models/group_average.dart'; -import 'package:refilc_kreta_api/models/subject.dart'; -import 'package:refilc_kreta_api/providers/grade_provider.dart'; -import 'package:refilc_mobile_ui/common/average_display.dart'; -import 'package:refilc_mobile_ui/common/round_border_icon.dart'; -import 'package:refilc_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart'; -import 'package:refilc_plus/models/premium_scopes.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_input.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_track_popup.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/route_option.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -enum PlanResult { - available, // There are possible solutions - unreachable, // The solutions are too hard don't even try - unsolvable, // There are no solutions - reached, // Goal already reached -} - -class GoalPlannerScreen extends StatefulWidget { - final GradeSubject subject; - - const GoalPlannerScreen({super.key, required this.subject}); - - @override - State createState() => _GoalPlannerScreenState(); -} - -class _GoalPlannerScreenState extends State { - late GradeProvider gradeProvider; - late GradeCalculatorProvider calculatorProvider; - late SettingsProvider settingsProvider; - late DatabaseProvider dbProvider; - late UserProvider user; - - bool gradeCalcMode = false; - - List getSubjectGrades(GradeSubject subject) => !gradeCalcMode - ? gradeProvider.grades.where((e) => e.subject == subject).toList() - : calculatorProvider.grades.where((e) => e.subject == subject).toList(); - - double goalValue = 4.0; - List grades = []; - - Plan? recommended; - Plan? fastest; - Plan? selectedRoute; - List otherPlans = []; - - @override - void initState() { - super.initState(); - user = Provider.of(context, listen: false); - dbProvider = Provider.of(context, listen: false); - } - - Future> fetchGoalPlans() async { - return await dbProvider.userQuery.subjectGoalPlans(userId: user.id!); - } - - Future> fetchGoalAverages() async { - return await dbProvider.userQuery.subjectGoalAverages(userId: user.id!); - } - - // haha bees lol - Future> fetchGoalBees() async { - return await dbProvider.userQuery.subjectGoalBefores(userId: user.id!); - } - - Future> fetchGoalPinDates() async { - return await dbProvider.userQuery.subjectGoalPinDates(userId: user.id!); - } - - PlanResult getResult() { - final currentAvg = GoalPlannerHelper.averageEvals(grades); - - recommended = null; - fastest = null; - otherPlans = []; - - if (currentAvg >= goalValue) return PlanResult.reached; - - final planner = GoalPlanner(goalValue, grades); - final plans = planner.solve(); - - plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3) - .abs() - .compareTo(b.avg - (2 * goalValue + 5) / 3)); - - try { - final singleSolution = plans.every((e) => e.sigma == 0); - recommended = - plans.where((e) => singleSolution ? true : e.sigma > 0).first; - plans.removeWhere((e) => e == recommended); - } catch (_) {} - - plans.sort((a, b) => a.plan.length.compareTo(b.plan.length)); - - try { - fastest = plans.removeAt(0); - } catch (_) {} - - // print((recommended?.plan.length ?? 0).toString() + '-kuki'); - // print((fastest?.plan.length ?? 0).toString() + '--asd'); - - if ((((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0)) >= - 5) && - fastest != null) { - recommended = fastest; - } - - if (recommended == null) { - recommended = null; - fastest = null; - otherPlans = []; - selectedRoute = null; - return PlanResult.unsolvable; - } - - // print(recommended!.plan.length.toString() + '--------'); - - if (recommended!.plan.length > 20) { - recommended = null; - fastest = null; - otherPlans = []; - selectedRoute = null; - return PlanResult.unreachable; - } - - otherPlans = List.from(plans); - - // only save 2 items if not plus member - if (!Provider.of(context) - .hasScope(PremiumScopes.unlimitedGoalPlanner)) { - if (otherPlans.length > 2) { - otherPlans.removeRange(2, otherPlans.length - 1); - } - } - - return PlanResult.available; - } - - void getGrades() { - grades = getSubjectGrades(widget.subject).toList(); - } - - @override - Widget build(BuildContext context) { - gradeProvider = Provider.of(context); - calculatorProvider = Provider.of(context); - settingsProvider = Provider.of(context); - - getGrades(); - - final currentAvg = GoalPlannerHelper.averageEvals(grades); - - final result = getResult(); - - List subjectGrades = getSubjectGrades(widget.subject); - - double avg = AverageHelper.averageEvals(subjectGrades); - - var nullavg = GroupAverage(average: 0.0, subject: widget.subject, uid: "0"); - double groupAverage = gradeProvider.groupAverages - .firstWhere((e) => e.subject == widget.subject, orElse: () => nullavg) - .average; - - return Scaffold( - body: SafeArea( - child: ListView( - padding: const EdgeInsets.only( - top: 5.0, - bottom: 220.0, - right: 15.0, - left: 2.0, - ), - children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // const BackButton(), - // Padding( - // padding: const EdgeInsets.only(right: 15.0), - // child: Row( - // children: [ - // Text( - // 'goal_planner_title'.i18n, - // style: const TextStyle( - // fontWeight: FontWeight.w500, fontSize: 18.0), - // ), - // const SizedBox( - // width: 5, - // ), - // const BetaChip(), - // ], - // ), - // ), - // ], - // ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - children: [ - const BackButton(), - BackButton( - color: Colors.red, - onPressed: () => - GoalTrackPopup.show(context, subject: widget.subject), - ), - RoundBorderIcon( - icon: Icon( - SubjectIcon.resolveVariant( - context: context, - subject: widget.subject, - ), - size: 18, - weight: 1.5, - ), - ), - const SizedBox( - width: 5.0, - ), - Text( - (widget.subject.isRenamed - ? widget.subject.renamedTo - : widget.subject.name) ?? - 'goal_planner_title'.i18n, - style: const TextStyle( - fontSize: 20.0, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - Row( - children: [ - if (groupAverage != 0) - AverageDisplay(average: groupAverage, border: true), - const SizedBox(width: 6.0), - AverageDisplay(average: avg), - ], - ), - ], - ), - const SizedBox(height: 12.0), - Padding( - padding: const EdgeInsets.only(left: 22.0, right: 22.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "set_a_goal".i18n, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0, - ), - ), - const SizedBox(height: 4.0), - Text( - goalValue.toString(), - style: TextStyle( - fontWeight: FontWeight.w900, - fontSize: 48.0, - color: gradeColor(goalValue.round(), settingsProvider), - ), - ), - // Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Text( - // "select_subject".i18n, - // style: const TextStyle( - // fontWeight: FontWeight.bold, - // fontSize: 20.0, - // ), - // ), - // const SizedBox(height: 4.0), - // Column( - // children: [ - // Icon( - // SubjectIcon.resolveVariant( - // context: context, - // subject: widget.subject, - // ), - // size: 48.0, - // ), - // Text( - // (widget.subject.isRenamed - // ? widget.subject.renamedTo - // : widget.subject.name) ?? - // '', - // style: const TextStyle( - // fontSize: 17.0, - // fontWeight: FontWeight.w500, - // ), - // ) - // ], - // ) - // ], - // ) - const SizedBox(height: 24.0), - Text( - "pick_route".i18n, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20.0, - ), - ), - const SizedBox(height: 12.0), - if (recommended != null) - RouteOption( - plan: recommended!, - mark: RouteMark.recommended, - selected: selectedRoute == recommended!, - onSelected: () => setState(() { - selectedRoute = recommended; - }), - ), - if (fastest != null && fastest != recommended) - RouteOption( - plan: fastest!, - mark: RouteMark.fastest, - selected: selectedRoute == fastest!, - onSelected: () => setState(() { - selectedRoute = fastest; - }), - ), - ...otherPlans.map((e) => RouteOption( - plan: e, - selected: selectedRoute == e, - onSelected: () => setState(() { - selectedRoute = e; - }), - )), - if (result != PlanResult.available) Text(result.name.i18n), - ], - ), - ), - ], - ), - ), - bottomSheet: MediaQuery.removePadding( - context: context, - removeBottom: false, - removeTop: true, - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Container( - padding: const EdgeInsets.only(top: 24.0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: - const BorderRadius.vertical(top: Radius.circular(24.0)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(.1), - blurRadius: 8.0, - ) - ]), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - GoalInput( - value: goalValue, - currentAverage: currentAvg, - onChanged: (v) => setState(() { - selectedRoute = null; - goalValue = v; - }), - ), - const SizedBox(height: 24.0), - SizedBox( - width: double.infinity, - child: RawMaterialButton( - onPressed: () async { - if (selectedRoute == null) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text('${"pick_route".i18n}...'))); - } - - final goalPlans = await fetchGoalPlans(); - final goalAvgs = await fetchGoalAverages(); - final goalBeforeGrades = await fetchGoalBees(); - final goalPinDates = await fetchGoalPinDates(); - - goalPlans[widget.subject.id] = - selectedRoute!.dbString; - goalAvgs[widget.subject.id] = - goalValue.toStringAsFixed(2); - goalBeforeGrades[widget.subject.id] = - avg.toStringAsFixed(2); - goalPinDates[widget.subject.id] = - DateTime.now().toIso8601String(); - // goalPlans[widget.subject.id] = '1,2,3,4,5,'; - // goalAvgs[widget.subject.id] = '3.69'; - // goalBeforeGrades[widget.subject.id] = '3.69'; - // goalPinDates[widget.subject.id] = - // DateTime.now().toIso8601String(); - - await dbProvider.userStore.storeSubjectGoalPlans( - goalPlans, - userId: user.id!); - await dbProvider.userStore.storeSubjectGoalAverages( - goalAvgs, - userId: user.id!); - await dbProvider.userStore.storeSubjectGoalBefores( - goalBeforeGrades, - userId: user.id!); - await dbProvider.userStore.storeSubjectGoalPinDates( - goalPinDates, - userId: user.id!); - - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - }, - fillColor: Theme.of(context).colorScheme.secondary, - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - "track_it".i18n, - style: const TextStyle( - color: Colors.white, - fontSize: 20.0, - fontWeight: FontWeight.w600, - ), - ), - ), - ) - ], - ), - ), - ), - ), - ), - ), - ); - } -} +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/helpers/average_helper.dart'; +import 'package:refilc/helpers/subject.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc_kreta_api/models/grade.dart'; +import 'package:refilc_kreta_api/models/group_average.dart'; +import 'package:refilc_kreta_api/models/subject.dart'; +import 'package:refilc_kreta_api/providers/grade_provider.dart'; +import 'package:refilc_mobile_ui/common/average_display.dart'; +import 'package:refilc_mobile_ui/common/round_border_icon.dart'; +import 'package:refilc_mobile_ui/pages/grades/calculator/grade_calculator_provider.dart'; +import 'package:refilc_plus/models/premium_scopes.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_input.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_track_popup.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/route_option.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +enum PlanResult { + available, // There are possible solutions + unreachable, // The solutions are too hard don't even try + unsolvable, // There are no solutions + reached, // Goal already reached +} + +class GoalPlannerScreen extends StatefulWidget { + final GradeSubject subject; + + const GoalPlannerScreen({super.key, required this.subject}); + + @override + State createState() => _GoalPlannerScreenState(); +} + +class _GoalPlannerScreenState extends State { + late GradeProvider gradeProvider; + late GradeCalculatorProvider calculatorProvider; + late SettingsProvider settingsProvider; + late DatabaseProvider dbProvider; + late UserProvider user; + + bool gradeCalcMode = false; + + List getSubjectGrades(GradeSubject subject) => !gradeCalcMode + ? gradeProvider.grades.where((e) => e.subject == subject).toList() + : calculatorProvider.grades.where((e) => e.subject == subject).toList(); + + double goalValue = 4.0; + List grades = []; + + Plan? recommended; + Plan? fastest; + Plan? selectedRoute; + List otherPlans = []; + + @override + void initState() { + super.initState(); + user = Provider.of(context, listen: false); + dbProvider = Provider.of(context, listen: false); + } + + Future> fetchGoalPlans() async { + return await dbProvider.userQuery.subjectGoalPlans(userId: user.id!); + } + + Future> fetchGoalAverages() async { + return await dbProvider.userQuery.subjectGoalAverages(userId: user.id!); + } + + // haha bees lol + Future> fetchGoalBees() async { + return await dbProvider.userQuery.subjectGoalBefores(userId: user.id!); + } + + Future> fetchGoalPinDates() async { + return await dbProvider.userQuery.subjectGoalPinDates(userId: user.id!); + } + + PlanResult getResult() { + final currentAvg = GoalPlannerHelper.averageEvals(grades); + + recommended = null; + fastest = null; + otherPlans = []; + + if (currentAvg >= goalValue) return PlanResult.reached; + + final planner = GoalPlanner(goalValue, grades); + final plans = planner.solve(); + + plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3) + .abs() + .compareTo(b.avg - (2 * goalValue + 5) / 3)); + + try { + final singleSolution = plans.every((e) => e.sigma == 0); + recommended = + plans.where((e) => singleSolution ? true : e.sigma > 0).first; + plans.removeWhere((e) => e == recommended); + } catch (_) {} + + plans.sort((a, b) => a.plan.length.compareTo(b.plan.length)); + + try { + fastest = plans.removeAt(0); + } catch (_) {} + + // print((recommended?.plan.length ?? 0).toString() + '-kuki'); + // print((fastest?.plan.length ?? 0).toString() + '--asd'); + + if ((((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0)) >= + 5) && + fastest != null) { + recommended = fastest; + } + + if (recommended == null) { + recommended = null; + fastest = null; + otherPlans = []; + selectedRoute = null; + return PlanResult.unsolvable; + } + + // print(recommended!.plan.length.toString() + '--------'); + + if (recommended!.plan.length > 20) { + recommended = null; + fastest = null; + otherPlans = []; + selectedRoute = null; + return PlanResult.unreachable; + } + + otherPlans = List.from(plans); + + // only save 2 items if not plus member + if (!Provider.of(context) + .hasScope(PremiumScopes.unlimitedGoalPlanner)) { + if (otherPlans.length > 2) { + otherPlans.removeRange(2, otherPlans.length - 1); + } + } + + return PlanResult.available; + } + + void getGrades() { + grades = getSubjectGrades(widget.subject).toList(); + } + + @override + Widget build(BuildContext context) { + gradeProvider = Provider.of(context); + calculatorProvider = Provider.of(context); + settingsProvider = Provider.of(context); + + getGrades(); + + final currentAvg = GoalPlannerHelper.averageEvals(grades); + + final result = getResult(); + + List subjectGrades = getSubjectGrades(widget.subject); + + double avg = AverageHelper.averageEvals(subjectGrades); + + var nullavg = GroupAverage(average: 0.0, subject: widget.subject, uid: "0"); + double groupAverage = gradeProvider.groupAverages + .firstWhere((e) => e.subject == widget.subject, orElse: () => nullavg) + .average; + + return Scaffold( + body: SafeArea( + child: ListView( + padding: const EdgeInsets.only( + top: 5.0, + bottom: 220.0, + right: 15.0, + left: 2.0, + ), + children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // const BackButton(), + // Padding( + // padding: const EdgeInsets.only(right: 15.0), + // child: Row( + // children: [ + // Text( + // 'goal_planner_title'.i18n, + // style: const TextStyle( + // fontWeight: FontWeight.w500, fontSize: 18.0), + // ), + // const SizedBox( + // width: 5, + // ), + // const BetaChip(), + // ], + // ), + // ), + // ], + // ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + const BackButton(), + BackButton( + color: Colors.red, + onPressed: () => + GoalTrackPopup.show(context, subject: widget.subject), + ), + RoundBorderIcon( + icon: Icon( + SubjectIcon.resolveVariant( + context: context, + subject: widget.subject, + ), + size: 18, + weight: 1.5, + ), + ), + const SizedBox( + width: 5.0, + ), + Text( + (widget.subject.isRenamed + ? widget.subject.renamedTo + : widget.subject.name) ?? + 'goal_planner_title'.i18n, + style: const TextStyle( + fontSize: 20.0, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + Row( + children: [ + if (groupAverage != 0) + AverageDisplay(average: groupAverage, border: true), + const SizedBox(width: 6.0), + AverageDisplay(average: avg), + ], + ), + ], + ), + const SizedBox(height: 12.0), + Padding( + padding: const EdgeInsets.only(left: 22.0, right: 22.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "set_a_goal".i18n, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20.0, + ), + ), + const SizedBox(height: 4.0), + Text( + goalValue.toString(), + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 48.0, + color: gradeColor(goalValue.round(), settingsProvider), + ), + ), + // Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // Text( + // "select_subject".i18n, + // style: const TextStyle( + // fontWeight: FontWeight.bold, + // fontSize: 20.0, + // ), + // ), + // const SizedBox(height: 4.0), + // Column( + // children: [ + // Icon( + // SubjectIcon.resolveVariant( + // context: context, + // subject: widget.subject, + // ), + // size: 48.0, + // ), + // Text( + // (widget.subject.isRenamed + // ? widget.subject.renamedTo + // : widget.subject.name) ?? + // '', + // style: const TextStyle( + // fontSize: 17.0, + // fontWeight: FontWeight.w500, + // ), + // ) + // ], + // ) + // ], + // ) + const SizedBox(height: 24.0), + Text( + "pick_route".i18n, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20.0, + ), + ), + const SizedBox(height: 12.0), + if (recommended != null) + RouteOption( + plan: recommended!, + mark: RouteMark.recommended, + selected: selectedRoute == recommended!, + onSelected: () => setState(() { + selectedRoute = recommended; + }), + ), + if (fastest != null && fastest != recommended) + RouteOption( + plan: fastest!, + mark: RouteMark.fastest, + selected: selectedRoute == fastest!, + onSelected: () => setState(() { + selectedRoute = fastest; + }), + ), + ...otherPlans.map((e) => RouteOption( + plan: e, + selected: selectedRoute == e, + onSelected: () => setState(() { + selectedRoute = e; + }), + )), + if (result != PlanResult.available) Text(result.name.i18n), + ], + ), + ), + ], + ), + ), + bottomSheet: MediaQuery.removePadding( + context: context, + removeBottom: false, + removeTop: true, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Container( + padding: const EdgeInsets.only(top: 24.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(24.0)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(.1), + blurRadius: 8.0, + ) + ]), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GoalInput( + value: goalValue, + currentAverage: currentAvg, + onChanged: (v) => setState(() { + selectedRoute = null; + goalValue = v; + }), + ), + const SizedBox(height: 24.0), + SizedBox( + width: double.infinity, + child: RawMaterialButton( + onPressed: () async { + if (selectedRoute == null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('${"pick_route".i18n}...'))); + } + + final goalPlans = await fetchGoalPlans(); + final goalAvgs = await fetchGoalAverages(); + final goalBeforeGrades = await fetchGoalBees(); + final goalPinDates = await fetchGoalPinDates(); + + goalPlans[widget.subject.id] = + selectedRoute!.dbString; + goalAvgs[widget.subject.id] = + goalValue.toStringAsFixed(2); + goalBeforeGrades[widget.subject.id] = + avg.toStringAsFixed(2); + goalPinDates[widget.subject.id] = + DateTime.now().toIso8601String(); + // goalPlans[widget.subject.id] = '1,2,3,4,5,'; + // goalAvgs[widget.subject.id] = '3.69'; + // goalBeforeGrades[widget.subject.id] = '3.69'; + // goalPinDates[widget.subject.id] = + // DateTime.now().toIso8601String(); + + await dbProvider.userStore.storeSubjectGoalPlans( + goalPlans, + userId: user.id!); + await dbProvider.userStore.storeSubjectGoalAverages( + goalAvgs, + userId: user.id!); + await dbProvider.userStore.storeSubjectGoalBefores( + goalBeforeGrades, + userId: user.id!); + await dbProvider.userStore.storeSubjectGoalPinDates( + goalPinDates, + userId: user.id!); + + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + }, + fillColor: Theme.of(context).colorScheme.secondary, + shape: const StadiumBorder(), + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + "track_it".i18n, + style: const TextStyle( + color: Colors.white, + fontSize: 20.0, + fontWeight: FontWeight.w600, + ), + ), + ), + ) + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/mobile/goal_planner/goal_planner_screen.i18n.dart b/lib/ui/mobile/goal_planner/goal_planner_screen.i18n.dart index 8b1ec3b..83dfbe0 100644 --- a/lib/ui/mobile/goal_planner/goal_planner_screen.i18n.dart +++ b/lib/ui/mobile/goal_planner/goal_planner_screen.i18n.dart @@ -1,70 +1,70 @@ -import 'package:i18n_extension/i18n_extension.dart'; - -extension Localization on String { - static final _t = Translations.byLocale("hu_hu") + - { - "en_en": { - "goal_planner_title": "Goal Planning", - "set_a_goal": "Your Goal", - "select_subject": "Subject", - "pick_route": "Pick a Route", - "track_it": "Track it!", - "recommended": "Recommended", - "fastest": "Fastest", - "unsolvable": "Unsolvable :(", - "unreachable": "Unreachable :(", - "reached": "Already reached! :D", - // new plan popup - "goalplan_title": "Goal Planning", - "goalplan_subtitle": "Enter the average you want to achieve!", - "goalplan_plans_title": "Choose the route", - "goalplan_plans_subtitle": - "You can achieve your goal with these tickets, choose one that you like the most! You can change this later.", - "show_my_ways": "Show me my options!", - }, - "hu_hu": { - "goal_planner_title": "Cél követés", - "set_a_goal": "Kitűzött cél", - "select_subject": "Tantárgy", - "pick_route": "Válassz egy utat", - "track_it": "Követés!", - "recommended": "Ajánlott", - "fastest": "Leggyorsabb", - "unsolvable": "Megoldhatatlan :(", - "unreachable": "Elérhetetlen :(", - "reached": "Már elérted! :D", - // new plan popup - "goalplan_title": "Cél kitűzése", - "goalplan_subtitle": "Add meg az elérni kívánt átlagot!", - "goalplan_plans_title": "Válaszd ki az utat", - "goalplan_plans_subtitle": - "Ezekkel a jegyekkel érheted el a célodat, válassz egyet, ami a legjobban tetszik! Ezt később változtathatod.", - "show_my_ways": "Mutasd a lehetőségeimet!", - }, - "de_de": { - "goal_planner_title": "Zielplanung", - "set_a_goal": "Dein Ziel", - "select_subject": "Thema", - "pick_route": "Wähle einen Weg", - "track_it": "Verfolge es!", - "recommended": "Empfohlen", - "fastest": "Am schnellsten", - "unsolvable": "Unlösbar :(", - "unreachable": "Unerreichbar :(", - "reached": "Bereits erreicht! :D", - // new plan popup - "goalplan_title": "Zielplanung", - "goalplan_subtitle": - "Geben Sie den Durchschnitt ein, den Sie erreichen möchten!", - "goalplan_plans_title": "Wählen Sie die Route", - "goalplan_plans_subtitle": - "Sie können Ihr Ziel mit diesen Tickets erreichen. Wählen Sie eines aus, das Ihnen am besten gefällt! Sie können dies später ändern.", - "show_my_ways": "Zeigen Sie mir meine Optionen!", - }, - }; - - String get i18n => localize(this, _t); - String fill(List params) => localizeFill(this, params); - String plural(int value) => localizePlural(value, this, _t); - String version(Object modifier) => localizeVersion(modifier, this, _t); -} +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "goal_planner_title": "Goal Planning", + "set_a_goal": "Your Goal", + "select_subject": "Subject", + "pick_route": "Pick a Route", + "track_it": "Track it!", + "recommended": "Recommended", + "fastest": "Fastest", + "unsolvable": "Unsolvable :(", + "unreachable": "Unreachable :(", + "reached": "Already reached! :D", + // new plan popup + "goalplan_title": "Goal Planning", + "goalplan_subtitle": "Enter the average you want to achieve!", + "goalplan_plans_title": "Choose the route", + "goalplan_plans_subtitle": + "You can achieve your goal with these tickets, choose one that you like the most! You can change this later.", + "show_my_ways": "Show me my options!", + }, + "hu_hu": { + "goal_planner_title": "Cél követés", + "set_a_goal": "Kitűzött cél", + "select_subject": "Tantárgy", + "pick_route": "Válassz egy utat", + "track_it": "Követés!", + "recommended": "Ajánlott", + "fastest": "Leggyorsabb", + "unsolvable": "Megoldhatatlan :(", + "unreachable": "Elérhetetlen :(", + "reached": "Már elérted! :D", + // new plan popup + "goalplan_title": "Cél kitűzése", + "goalplan_subtitle": "Add meg az elérni kívánt átlagot!", + "goalplan_plans_title": "Válaszd ki az utat", + "goalplan_plans_subtitle": + "Ezekkel a jegyekkel érheted el a célodat, válassz egyet, ami a legjobban tetszik! Ezt később változtathatod.", + "show_my_ways": "Mutasd a lehetőségeimet!", + }, + "de_de": { + "goal_planner_title": "Zielplanung", + "set_a_goal": "Dein Ziel", + "select_subject": "Thema", + "pick_route": "Wähle einen Weg", + "track_it": "Verfolge es!", + "recommended": "Empfohlen", + "fastest": "Am schnellsten", + "unsolvable": "Unlösbar :(", + "unreachable": "Unerreichbar :(", + "reached": "Bereits erreicht! :D", + // new plan popup + "goalplan_title": "Zielplanung", + "goalplan_subtitle": + "Geben Sie den Durchschnitt ein, den Sie erreichen möchten!", + "goalplan_plans_title": "Wählen Sie die Route", + "goalplan_plans_subtitle": + "Sie können Ihr Ziel mit diesen Tickets erreichen. Wählen Sie eines aus, das Ihnen am besten gefällt! Sie können dies später ändern.", + "show_my_ways": "Zeigen Sie mir meine Optionen!", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/lib/ui/mobile/goal_planner/goal_state_screen.dart b/lib/ui/mobile/goal_planner/goal_state_screen.dart index df1e6d7..beb0fbc 100644 --- a/lib/ui/mobile/goal_planner/goal_state_screen.dart +++ b/lib/ui/mobile/goal_planner/goal_state_screen.dart @@ -1,471 +1,471 @@ -// ignore_for_file: use_build_context_synchronously - -import 'package:refilc/api/providers/database_provider.dart'; -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/helpers/average_helper.dart'; -import 'package:refilc/helpers/subject.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:refilc_kreta_api/models/grade.dart'; -import 'package:refilc_kreta_api/models/subject.dart'; -import 'package:refilc_kreta_api/providers/grade_provider.dart'; -import 'package:refilc_mobile_ui/common/action_button.dart'; -import 'package:refilc_mobile_ui/common/average_display.dart'; -import 'package:refilc_mobile_ui/common/panel/panel.dart'; -import 'package:refilc_mobile_ui/common/progress_bar.dart'; -import 'package:refilc_mobile_ui/common/round_border_icon.dart'; -import 'package:refilc_plus/providers/goal_provider.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_state_screen.i18n.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_track_popup.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/route_option.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:provider/provider.dart'; - -import 'graph.dart'; - -class GoalStateScreen extends StatefulWidget { - final GradeSubject subject; - - const GoalStateScreen({super.key, required this.subject}); - - @override - State createState() => _GoalStateScreenState(); -} - -class _GoalStateScreenState extends State { - late UserProvider user; - late DatabaseProvider db; - late GradeProvider gradeProvider; - late SettingsProvider settingsProvider; - - double currAvg = 0.0; - double goalAvg = 0.0; - double beforeAvg = 0.0; - double afterAvg = 0.0; - double avgDifference = 0; - - Plan? plan; - - late Widget gradeGraph; - - DateTime goalPinDate = DateTime.now(); - - void fetchGoalAverages() async { - var goalAvgRes = await db.userQuery.subjectGoalAverages(userId: user.id!); - var beforeAvgRes = await db.userQuery.subjectGoalBefores(userId: user.id!); - - goalPinDate = DateTime.parse((await db.userQuery - .subjectGoalPinDates(userId: user.id!))[widget.subject.id]!); - - String? goalAvgStr = goalAvgRes[widget.subject.id]; - String? beforeAvgStr = beforeAvgRes[widget.subject.id]; - goalAvg = double.parse(goalAvgStr ?? '0.0'); - beforeAvg = double.parse(beforeAvgStr ?? '0.0'); - - avgDifference = ((goalAvg - beforeAvg) / beforeAvg.abs()) * 100; - - setState(() {}); - } - - void fetchGoalPlan() async { - var planRes = await db.userQuery.subjectGoalPlans(userId: user.id!); - List prePlan = planRes[widget.subject.id]!.split(','); - prePlan.removeLast(); - - plan = Plan( - prePlan.map((e) => int.parse(e)).toList(), - ); - - setState(() {}); - } - - List getSubjectGrades(GradeSubject subject) => - gradeProvider.grades.where((e) => (e.subject == subject)).toList(); - - List getAfterGoalGrades(GradeSubject subject) => gradeProvider.grades - .where((e) => (e.subject == subject && e.date.isAfter(goalPinDate))) - .toList(); - - @override - void initState() { - super.initState(); - user = Provider.of(context, listen: false); - db = Provider.of(context, listen: false); - - WidgetsBinding.instance.addPostFrameCallback((_) { - fetchGoalAverages(); - fetchGoalPlan(); - }); - } - - @override - Widget build(BuildContext context) { - gradeProvider = Provider.of(context); - settingsProvider = Provider.of(context); - - var subjectGrades = getSubjectGrades(widget.subject).toList(); - currAvg = AverageHelper.averageEvals(subjectGrades); - - var afterGoalGrades = getAfterGoalGrades(widget.subject).toList(); - afterAvg = AverageHelper.averageEvals(afterGoalGrades); - - Color averageColor = currAvg >= 1 && currAvg <= 5 - ? ColorTween( - begin: settingsProvider.gradeColors[currAvg.floor() - 1], - end: settingsProvider.gradeColors[currAvg.ceil() - 1]) - .transform(currAvg - currAvg.floor())! - : Theme.of(context).colorScheme.secondary; - - gradeGraph = Padding( - padding: const EdgeInsets.only( - top: 12.0, - bottom: 8.0, - ), - child: Panel( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.only(top: 16.0, right: 12.0), - child: GoalGraph(afterGoalGrades, - dayThreshold: 5, classAvg: goalAvg), - ), - const SizedBox(height: 5.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'look_at_graph'.i18n, - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 23.0, - ), - ), - Text( - 'thats_progress'.i18n, - style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 20.0, - ), - ), - const SizedBox(height: 15.0), - ProgressBar( - value: currAvg / goalAvg, - backgroundColor: averageColor, - height: 16.0, - ), - const SizedBox(height: 8.0), - ], - ), - ), - ], - ), - ), - ); - - return Scaffold( - body: ListView( - padding: EdgeInsets.zero, - children: [ - Container( - decoration: const BoxDecoration( - // image: DecorationImage( - // image: - // AssetImage('assets/images/subject_covers/math_light.png'), - // fit: BoxFit.fitWidth, - // alignment: Alignment.topCenter, - // ), - ), - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of(context).scaffoldBackgroundColor.withOpacity(0.2), - Theme.of(context).scaffoldBackgroundColor, - ], - stops: const [ - 0.1, - 0.22, - ], - ), - ), - child: Padding( - padding: const EdgeInsets.only( - top: 60.0, - left: 2.0, - right: 2.0, - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const BackButton(), - IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0)), - title: Text("attention".i18n), - content: Text("attention_body".i18n), - actions: [ - ActionButton( - label: "delete".i18n, - onTap: () async { - // clear the goal - await Provider.of(context, - listen: false) - .clearGoal(widget.subject); - // close the modal and the goal page - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - }, - icon: const Icon(FeatherIcons.trash2), - ), - ], - ), - const SizedBox(height: 22.0), - Column( - children: [ - RoundBorderIcon( - icon: Icon( - SubjectIcon.resolveVariant( - context: context, - subject: widget.subject, - ), - size: 26.0, - weight: 2.5, - ), - padding: 8.0, - width: 2.5, - ), - const SizedBox( - height: 10.0, - ), - Text( - (widget.subject.isRenamed - ? widget.subject.renamedTo - : widget.subject.name) ?? - 'goal_planner_title'.i18n, - style: const TextStyle( - fontSize: 30.0, - fontWeight: FontWeight.w700, - ), - ), - Text( - 'almost_there'.i18n, - style: const TextStyle( - fontSize: 22.0, - fontWeight: FontWeight.w400, - height: 1.0, - ), - ), - ], - ), - const SizedBox(height: 28.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - 'started_with'.i18n, - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 20.0, - ), - ), - const SizedBox(width: 5.0), - AverageDisplay(average: beforeAvg), - ], - ), - Row( - children: [ - Text( - 'current'.i18n, - style: const TextStyle( - fontWeight: FontWeight.w700, - fontSize: 20.0, - ), - ), - const SizedBox(width: 5.0), - AverageDisplay(average: currAvg), - const SizedBox(width: 5.0), - // ide majd kell average difference - ], - ), - ], - ), - ), - const SizedBox(height: 10.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Panel( - padding: const EdgeInsets.all(18.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'your_goal'.i18n, - style: const TextStyle( - fontSize: 23.0, - fontWeight: FontWeight.w700, - ), - ), - RawMaterialButton( - onPressed: () async { - GoalTrackPopup.show(context, - subject: widget.subject); - // Navigator.of(context).push( - // CupertinoPageRoute( - // builder: (context) => - // GoalPlannerScreen( - // subject: widget.subject))); - }, - fillColor: Colors.black, - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric( - horizontal: 18.0), - child: Text( - "change_it".i18n, - style: const TextStyle( - height: 1.0, - color: Colors.white, - fontSize: 14.0, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - goalAvg.toString(), - style: const TextStyle( - height: 1.1, - fontSize: 42.0, - fontWeight: FontWeight.w800, - ), - ), - const SizedBox(width: 10.0), - Center( - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 5.0, - horizontal: 8.0, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(45.0), - color: avgDifference.isNegative - ? Colors.redAccent.shade400 - .withOpacity(.15) - : Colors.greenAccent.shade700 - .withOpacity(.15), - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Icon( - avgDifference.isNegative - ? FeatherIcons.chevronDown - : FeatherIcons.chevronUp, - color: avgDifference.isNegative - ? Colors.redAccent.shade400 - : Colors.greenAccent.shade700, - size: 18.0, - ), - const SizedBox(width: 5.0), - Text( - '${avgDifference.toStringAsFixed(2)}%', - textAlign: TextAlign.center, - style: TextStyle( - color: avgDifference.isNegative - ? Colors.redAccent.shade400 - : Colors.greenAccent.shade700, - fontSize: 22.0, - height: 0.8, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ], - ), - ], - ), - ), - ), - const SizedBox(height: 5.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: gradeGraph, - ), - const SizedBox(height: 5.0), - Padding( - padding: const EdgeInsets.only( - left: 12.0, - right: 12.0, - top: 5.0, - bottom: 8.0, - ), - child: Panel( - padding: const EdgeInsets.all(18.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'you_need'.i18n, - style: const TextStyle( - fontSize: 23.0, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - const SizedBox(height: 8.0), - plan != null - ? RouteOptionRow( - plan: plan!, - ) - : const Text(''), - ], - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } -} +// ignore_for_file: use_build_context_synchronously + +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/helpers/average_helper.dart'; +import 'package:refilc/helpers/subject.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc_kreta_api/models/grade.dart'; +import 'package:refilc_kreta_api/models/subject.dart'; +import 'package:refilc_kreta_api/providers/grade_provider.dart'; +import 'package:refilc_mobile_ui/common/action_button.dart'; +import 'package:refilc_mobile_ui/common/average_display.dart'; +import 'package:refilc_mobile_ui/common/panel/panel.dart'; +import 'package:refilc_mobile_ui/common/progress_bar.dart'; +import 'package:refilc_mobile_ui/common/round_border_icon.dart'; +import 'package:refilc_plus/providers/goal_provider.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_state_screen.i18n.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_track_popup.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/route_option.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; + +import 'graph.dart'; + +class GoalStateScreen extends StatefulWidget { + final GradeSubject subject; + + const GoalStateScreen({super.key, required this.subject}); + + @override + State createState() => _GoalStateScreenState(); +} + +class _GoalStateScreenState extends State { + late UserProvider user; + late DatabaseProvider db; + late GradeProvider gradeProvider; + late SettingsProvider settingsProvider; + + double currAvg = 0.0; + double goalAvg = 0.0; + double beforeAvg = 0.0; + double afterAvg = 0.0; + double avgDifference = 0; + + Plan? plan; + + late Widget gradeGraph; + + DateTime goalPinDate = DateTime.now(); + + void fetchGoalAverages() async { + var goalAvgRes = await db.userQuery.subjectGoalAverages(userId: user.id!); + var beforeAvgRes = await db.userQuery.subjectGoalBefores(userId: user.id!); + + goalPinDate = DateTime.parse((await db.userQuery + .subjectGoalPinDates(userId: user.id!))[widget.subject.id]!); + + String? goalAvgStr = goalAvgRes[widget.subject.id]; + String? beforeAvgStr = beforeAvgRes[widget.subject.id]; + goalAvg = double.parse(goalAvgStr ?? '0.0'); + beforeAvg = double.parse(beforeAvgStr ?? '0.0'); + + avgDifference = ((goalAvg - beforeAvg) / beforeAvg.abs()) * 100; + + setState(() {}); + } + + void fetchGoalPlan() async { + var planRes = await db.userQuery.subjectGoalPlans(userId: user.id!); + List prePlan = planRes[widget.subject.id]!.split(','); + prePlan.removeLast(); + + plan = Plan( + prePlan.map((e) => int.parse(e)).toList(), + ); + + setState(() {}); + } + + List getSubjectGrades(GradeSubject subject) => + gradeProvider.grades.where((e) => (e.subject == subject)).toList(); + + List getAfterGoalGrades(GradeSubject subject) => gradeProvider.grades + .where((e) => (e.subject == subject && e.date.isAfter(goalPinDate))) + .toList(); + + @override + void initState() { + super.initState(); + user = Provider.of(context, listen: false); + db = Provider.of(context, listen: false); + + WidgetsBinding.instance.addPostFrameCallback((_) { + fetchGoalAverages(); + fetchGoalPlan(); + }); + } + + @override + Widget build(BuildContext context) { + gradeProvider = Provider.of(context); + settingsProvider = Provider.of(context); + + var subjectGrades = getSubjectGrades(widget.subject).toList(); + currAvg = AverageHelper.averageEvals(subjectGrades); + + var afterGoalGrades = getAfterGoalGrades(widget.subject).toList(); + afterAvg = AverageHelper.averageEvals(afterGoalGrades); + + Color averageColor = currAvg >= 1 && currAvg <= 5 + ? ColorTween( + begin: settingsProvider.gradeColors[currAvg.floor() - 1], + end: settingsProvider.gradeColors[currAvg.ceil() - 1]) + .transform(currAvg - currAvg.floor())! + : Theme.of(context).colorScheme.secondary; + + gradeGraph = Padding( + padding: const EdgeInsets.only( + top: 12.0, + bottom: 8.0, + ), + child: Panel( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(top: 16.0, right: 12.0), + child: GoalGraph(afterGoalGrades, + dayThreshold: 5, classAvg: goalAvg), + ), + const SizedBox(height: 5.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'look_at_graph'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 23.0, + ), + ), + Text( + 'thats_progress'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 20.0, + ), + ), + const SizedBox(height: 15.0), + ProgressBar( + value: currAvg / goalAvg, + backgroundColor: averageColor, + height: 16.0, + ), + const SizedBox(height: 8.0), + ], + ), + ), + ], + ), + ), + ); + + return Scaffold( + body: ListView( + padding: EdgeInsets.zero, + children: [ + Container( + decoration: const BoxDecoration( + // image: DecorationImage( + // image: + // AssetImage('assets/images/subject_covers/math_light.png'), + // fit: BoxFit.fitWidth, + // alignment: Alignment.topCenter, + // ), + ), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).scaffoldBackgroundColor.withOpacity(0.2), + Theme.of(context).scaffoldBackgroundColor, + ], + stops: const [ + 0.1, + 0.22, + ], + ), + ), + child: Padding( + padding: const EdgeInsets.only( + top: 60.0, + left: 2.0, + right: 2.0, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const BackButton(), + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0)), + title: Text("attention".i18n), + content: Text("attention_body".i18n), + actions: [ + ActionButton( + label: "delete".i18n, + onTap: () async { + // clear the goal + await Provider.of(context, + listen: false) + .clearGoal(widget.subject); + // close the modal and the goal page + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + icon: const Icon(FeatherIcons.trash2), + ), + ], + ), + const SizedBox(height: 22.0), + Column( + children: [ + RoundBorderIcon( + icon: Icon( + SubjectIcon.resolveVariant( + context: context, + subject: widget.subject, + ), + size: 26.0, + weight: 2.5, + ), + padding: 8.0, + width: 2.5, + ), + const SizedBox( + height: 10.0, + ), + Text( + (widget.subject.isRenamed + ? widget.subject.renamedTo + : widget.subject.name) ?? + 'goal_planner_title'.i18n, + style: const TextStyle( + fontSize: 30.0, + fontWeight: FontWeight.w700, + ), + ), + Text( + 'almost_there'.i18n, + style: const TextStyle( + fontSize: 22.0, + fontWeight: FontWeight.w400, + height: 1.0, + ), + ), + ], + ), + const SizedBox(height: 28.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'started_with'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20.0, + ), + ), + const SizedBox(width: 5.0), + AverageDisplay(average: beforeAvg), + ], + ), + Row( + children: [ + Text( + 'current'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20.0, + ), + ), + const SizedBox(width: 5.0), + AverageDisplay(average: currAvg), + const SizedBox(width: 5.0), + // ide majd kell average difference + ], + ), + ], + ), + ), + const SizedBox(height: 10.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Panel( + padding: const EdgeInsets.all(18.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'your_goal'.i18n, + style: const TextStyle( + fontSize: 23.0, + fontWeight: FontWeight.w700, + ), + ), + RawMaterialButton( + onPressed: () async { + GoalTrackPopup.show(context, + subject: widget.subject); + // Navigator.of(context).push( + // CupertinoPageRoute( + // builder: (context) => + // GoalPlannerScreen( + // subject: widget.subject))); + }, + fillColor: Colors.black, + shape: const StadiumBorder(), + padding: const EdgeInsets.symmetric( + horizontal: 18.0), + child: Text( + "change_it".i18n, + style: const TextStyle( + height: 1.0, + color: Colors.white, + fontSize: 14.0, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + goalAvg.toString(), + style: const TextStyle( + height: 1.1, + fontSize: 42.0, + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(width: 10.0), + Center( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 5.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(45.0), + color: avgDifference.isNegative + ? Colors.redAccent.shade400 + .withOpacity(.15) + : Colors.greenAccent.shade700 + .withOpacity(.15), + ), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Icon( + avgDifference.isNegative + ? FeatherIcons.chevronDown + : FeatherIcons.chevronUp, + color: avgDifference.isNegative + ? Colors.redAccent.shade400 + : Colors.greenAccent.shade700, + size: 18.0, + ), + const SizedBox(width: 5.0), + Text( + '${avgDifference.toStringAsFixed(2)}%', + textAlign: TextAlign.center, + style: TextStyle( + color: avgDifference.isNegative + ? Colors.redAccent.shade400 + : Colors.greenAccent.shade700, + fontSize: 22.0, + height: 0.8, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 5.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: gradeGraph, + ), + const SizedBox(height: 5.0), + Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + top: 5.0, + bottom: 8.0, + ), + child: Panel( + padding: const EdgeInsets.all(18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'you_need'.i18n, + style: const TextStyle( + fontSize: 23.0, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + const SizedBox(height: 8.0), + plan != null + ? RouteOptionRow( + plan: plan!, + ) + : const Text(''), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart b/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart index 19c56ed..51b7ac4 100644 --- a/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart +++ b/lib/ui/mobile/goal_planner/goal_state_screen.i18n.dart @@ -1,87 +1,87 @@ -import 'package:i18n_extension/i18n_extension.dart'; - -extension Localization on String { - static final _t = Translations.byLocale("hu_hu") + - { - "en_en": { - // base page - "goal_planner_title": "Goal Planning", - "almost_there": "Almost there! Keep going!", - "started_with": "Started with:", - "current": "Current:", - "your_goal": "Your goal:", - "change_it": "Change it", - "look_at_graph": "Look at this graph!", - "thats_progress": - "Now that's what I call progress! Push a little more, you're almost there..", - "you_need": "You need:", - // done modal - "congrats_title": "🎉 Congratulations!", - "goal_reached": "You reached your goal after %s days!", - "started_at": "You started at", - "improved_by": "and improved your grade by %s", - "detailed_stats": "See my detailed stats", - "later": "Yay! I'll see my stats later.", - // sure delete modal - "delete": "Delete", - "attention": "Attention!", - "attention_body": - "Your goal and progress will be lost forever and cannot be restored.", - }, - "hu_hu": { - // base page - "goal_planner_title": "Cél követés", - "almost_there": "Majdnem megvan! Így tovább!", - "started_with": "Így kezdődött:", - "current": "Jelenlegi:", - "your_goal": "Célod:", - "change_it": "Megváltoztatás", - "look_at_graph": "Nézd meg ezt a grafikont!", - "thats_progress": - "Ezt nevezem haladásnak! Hajts még egy kicsit, már majdnem kész..", - "you_need": "Szükséges:", - // done modal - "congrats_title": "🎉 Gratulálunk!", - "goal_reached": "%s nap után érted el a célod!", - "started_at": "Átlagod kezdéskor:", - "improved_by": "%s-os javulást értél el!", - "detailed_stats": "Részletes statisztikám", - "later": "Hurrá! Megnézem máskor.", - // sure delete modal - "delete": "Törlés", - "attention": "Figyelem!", - "attention_body": - "A kitűzött célod és haladásod örökre elveszik és nem lesz visszaállítható.", - }, - "de_de": { - // base page - "goal_planner_title": "Zielplanung", - "almost_there": "Fast dort! Weitermachen!", - "started_with": "Begann mit:", - "current": "Aktuell:", - "your_goal": "Dein Ziel:", - "change_it": "Ändern Sie es", - "look_at_graph": "Schauen Sie sich diese Grafik an!", - "thats_progress": - "Das nenne ich Fortschritt! Drücken Sie noch ein wenig, Sie haben es fast geschafft..", - "you_need": "Du brauchst:", - // done modal - "congrats_title": "🎉 Glückwunsch!", - "goal_reached": "Du hast dein Ziel nach %s Tagen erreicht!", - "started_at": "Gesamtbewertung:", - "improved_by": "Sie haben %s Verbesserung erreicht!", - "detailed_stats": "Detaillierte Statistiken", - "later": "Hurra! Ich schaue später nach.", - // sure delete modal - "delete": "Löschen", - "attention": "Achtung!", - "attention_body": - "Ihr Ziel und Ihr Fortschritt gehen für immer verloren und können nicht wiederhergestellt werden.", - }, - }; - - String get i18n => localize(this, _t); - String fill(List params) => localizeFill(this, params); - String plural(int value) => localizePlural(value, this, _t); - String version(Object modifier) => localizeVersion(modifier, this, _t); -} +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + // base page + "goal_planner_title": "Goal Planning", + "almost_there": "Almost there! Keep going!", + "started_with": "Started with:", + "current": "Current:", + "your_goal": "Your goal:", + "change_it": "Change it", + "look_at_graph": "Look at this graph!", + "thats_progress": + "Now that's what I call progress! Push a little more, you're almost there..", + "you_need": "You need:", + // done modal + "congrats_title": "🎉 Congratulations!", + "goal_reached": "You reached your goal after %s days!", + "started_at": "You started at", + "improved_by": "and improved your grade by %s", + "detailed_stats": "See my detailed stats", + "later": "Yay! I'll see my stats later.", + // sure delete modal + "delete": "Delete", + "attention": "Attention!", + "attention_body": + "Your goal and progress will be lost forever and cannot be restored.", + }, + "hu_hu": { + // base page + "goal_planner_title": "Cél követés", + "almost_there": "Majdnem megvan! Így tovább!", + "started_with": "Így kezdődött:", + "current": "Jelenlegi:", + "your_goal": "Célod:", + "change_it": "Megváltoztatás", + "look_at_graph": "Nézd meg ezt a grafikont!", + "thats_progress": + "Ezt nevezem haladásnak! Hajts még egy kicsit, már majdnem kész..", + "you_need": "Szükséges:", + // done modal + "congrats_title": "🎉 Gratulálunk!", + "goal_reached": "%s nap után érted el a célod!", + "started_at": "Átlagod kezdéskor:", + "improved_by": "%s-os javulást értél el!", + "detailed_stats": "Részletes statisztikám", + "later": "Hurrá! Megnézem máskor.", + // sure delete modal + "delete": "Törlés", + "attention": "Figyelem!", + "attention_body": + "A kitűzött célod és haladásod örökre elveszik és nem lesz visszaállítható.", + }, + "de_de": { + // base page + "goal_planner_title": "Zielplanung", + "almost_there": "Fast dort! Weitermachen!", + "started_with": "Begann mit:", + "current": "Aktuell:", + "your_goal": "Dein Ziel:", + "change_it": "Ändern Sie es", + "look_at_graph": "Schauen Sie sich diese Grafik an!", + "thats_progress": + "Das nenne ich Fortschritt! Drücken Sie noch ein wenig, Sie haben es fast geschafft..", + "you_need": "Du brauchst:", + // done modal + "congrats_title": "🎉 Glückwunsch!", + "goal_reached": "Du hast dein Ziel nach %s Tagen erreicht!", + "started_at": "Gesamtbewertung:", + "improved_by": "Sie haben %s Verbesserung erreicht!", + "detailed_stats": "Detaillierte Statistiken", + "later": "Hurra! Ich schaue später nach.", + // sure delete modal + "delete": "Löschen", + "attention": "Achtung!", + "attention_body": + "Ihr Ziel und Ihr Fortschritt gehen für immer verloren und können nicht wiederhergestellt werden.", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/lib/ui/mobile/goal_planner/goal_track_popup.dart b/lib/ui/mobile/goal_planner/goal_track_popup.dart index 1d235ed..f50a52f 100644 --- a/lib/ui/mobile/goal_planner/goal_track_popup.dart +++ b/lib/ui/mobile/goal_planner/goal_track_popup.dart @@ -1,364 +1,364 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:refilc/api/providers/database_provider.dart'; -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/helpers/average_helper.dart'; -import 'package:refilc_kreta_api/models/grade.dart'; -import 'package:refilc_kreta_api/models/subject.dart'; -import 'package:refilc_kreta_api/providers/grade_provider.dart'; -import 'package:refilc_mobile_ui/common/average_display.dart'; -import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart'; -import 'package:refilc_plus/models/premium_scopes.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_input.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/route_option.dart'; - -class GoalTrackPopup extends StatefulWidget { - const GoalTrackPopup({super.key, required this.subject}); - - final GradeSubject subject; - - static void show(BuildContext context, {required GradeSubject subject}) => - showRoundedModalBottomSheet( - context, - child: GoalTrackPopup(subject: subject), - showHandle: true, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - ); - - @override - GoalTrackPopupState createState() => GoalTrackPopupState(); -} - -class GoalTrackPopupState extends State { - late UserProvider user; - late DatabaseProvider dbProvider; - late GradeProvider gradeProvider; - - List getSubjectGrades(GradeSubject subject) => - gradeProvider.grades.where((e) => e.subject == subject).toList(); - - double goalValue = 4.0; - List grades = []; - - Plan? recommended; - Plan? fastest; - Plan? selectedRoute; - List otherPlans = []; - - bool plansPage = false; - - @override - void initState() { - super.initState(); - user = Provider.of(context, listen: false); - dbProvider = Provider.of(context, listen: false); - } - - Future> fetchGoalPlans() async { - return await dbProvider.userQuery.subjectGoalPlans(userId: user.id!); - } - - Future> fetchGoalAverages() async { - return await dbProvider.userQuery.subjectGoalAverages(userId: user.id!); - } - - // haha bees lol - Future> fetchGoalBees() async { - return await dbProvider.userQuery.subjectGoalBefores(userId: user.id!); - } - - Future> fetchGoalPinDates() async { - return await dbProvider.userQuery.subjectGoalPinDates(userId: user.id!); - } - - PlanResult getResult() { - final currentAvg = GoalPlannerHelper.averageEvals(grades); - - recommended = null; - fastest = null; - otherPlans = []; - - if (currentAvg >= goalValue) return PlanResult.reached; - - final planner = GoalPlanner(goalValue, grades); - final plans = planner.solve(); - - plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3) - .abs() - .compareTo(b.avg - (2 * goalValue + 5) / 3)); - - try { - final singleSolution = plans.every((e) => e.sigma == 0); - recommended = - plans.where((e) => singleSolution ? true : e.sigma > 0).first; - plans.removeWhere((e) => e == recommended); - } catch (_) {} - - plans.sort((a, b) => a.plan.length.compareTo(b.plan.length)); - - try { - fastest = plans.removeAt(0); - } catch (_) {} - - // print((recommended?.plan.length ?? 0).toString() + '-kuki'); - // print((fastest?.plan.length ?? 0).toString() + '--asd'); - - if ((((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0)) >= - 5) && - fastest != null) { - recommended = fastest; - } - - if (recommended == null) { - recommended = null; - fastest = null; - otherPlans = []; - selectedRoute = null; - return PlanResult.unsolvable; - } - - // print(recommended!.plan.length.toString() + '--------'); - - if (recommended!.plan.length > 20) { - recommended = null; - fastest = null; - otherPlans = []; - selectedRoute = null; - return PlanResult.unreachable; - } - - otherPlans = List.from(plans); - - // only save 2 items if not plus member - if (!Provider.of(context) - .hasScope(PremiumScopes.unlimitedGoalPlanner)) { - if (otherPlans.length > 2) { - otherPlans.removeRange(2, otherPlans.length - 1); - } - } - - return PlanResult.available; - } - - void getGrades() { - grades = getSubjectGrades(widget.subject).toList(); - } - - @override - Widget build(BuildContext context) { - gradeProvider = Provider.of(context); - - getGrades(); - - final currentAvg = GoalPlannerHelper.averageEvals(grades); - - final result = getResult(); - - List subjectGrades = getSubjectGrades(widget.subject); - - double avg = AverageHelper.averageEvals(subjectGrades); - - double listLength = (otherPlans.length + - (recommended != null ? 1 : 0) + - (fastest != null && fastest != recommended ? 1 : 0)); - - return Container( - padding: const EdgeInsets.only(top: 24.0), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - AverageDisplay( - average: avg, - scale: 1.3, - ), - const SizedBox(width: 12.0), - const Icon( - Icons.arrow_forward, - size: 24.0, - ), - const SizedBox(width: 12.0), - AverageDisplay( - average: goalValue, - border: true, - dashed: true, - scale: 1.3, - ), - ], - ), - const SizedBox( - height: 14.0, - ), - Text( - plansPage - ? 'goalplan_plans_title'.i18n - : 'goalplan_title'.i18n, - style: const TextStyle( - fontSize: 20.0, fontWeight: FontWeight.w700), - textAlign: TextAlign.center), - Text( - plansPage - ? 'goalplan_plans_subtitle'.i18n - : 'goalplan_subtitle'.i18n, - style: const TextStyle( - fontSize: 16.0, fontWeight: FontWeight.w500), - textAlign: TextAlign.center), - ], - ), - const SizedBox(height: 24.0), - if (!plansPage) - GoalInput( - value: goalValue, - currentAverage: currentAvg, - onChanged: (v) => setState(() { - selectedRoute = null; - goalValue = v; - }), - ), - if (plansPage && listLength > 2) - SizedBox( - height: (MediaQuery.of(context).size.height * 0.5), - child: SingleChildScrollView( - child: Column( - children: [ - if (recommended != null) - RouteOption( - plan: recommended!, - mark: RouteMark.recommended, - selected: selectedRoute == recommended!, - onSelected: () => setState(() { - selectedRoute = recommended; - }), - ), - if (fastest != null && fastest != recommended) - RouteOption( - plan: fastest!, - mark: RouteMark.fastest, - selected: selectedRoute == fastest!, - onSelected: () => setState(() { - selectedRoute = fastest; - }), - ), - ...otherPlans.map((e) => RouteOption( - plan: e, - selected: selectedRoute == e, - onSelected: () => setState(() { - selectedRoute = e; - }), - )), - if (result != PlanResult.available) - Text(result.name.i18n), - ], - ), - ), - ), - if (plansPage && listLength <= 2) - Column( - children: [ - if (recommended != null) - RouteOption( - plan: recommended!, - mark: RouteMark.recommended, - selected: selectedRoute == recommended!, - onSelected: () => setState(() { - selectedRoute = recommended; - }), - ), - if (fastest != null && fastest != recommended) - RouteOption( - plan: fastest!, - mark: RouteMark.fastest, - selected: selectedRoute == fastest!, - onSelected: () => setState(() { - selectedRoute = fastest; - }), - ), - ...otherPlans.map((e) => RouteOption( - plan: e, - selected: selectedRoute == e, - onSelected: () => setState(() { - selectedRoute = e; - }), - )), - if (result != PlanResult.available) Text(result.name.i18n), - ], - ), - const SizedBox(height: 24.0), - SizedBox( - width: double.infinity, - child: RawMaterialButton( - onPressed: () async { - if (!plansPage) { - setState(() { - plansPage = true; - }); - return; - } - - if (selectedRoute == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${"pick_route".i18n}...'))); - } - - final goalPlans = await fetchGoalPlans(); - final goalAvgs = await fetchGoalAverages(); - final goalBeforeGrades = await fetchGoalBees(); - final goalPinDates = await fetchGoalPinDates(); - - goalPlans[widget.subject.id] = selectedRoute!.dbString; - goalAvgs[widget.subject.id] = goalValue.toStringAsFixed(2); - goalBeforeGrades[widget.subject.id] = - avg.toStringAsFixed(2); - goalPinDates[widget.subject.id] = - DateTime.now().toIso8601String(); - // goalPlans[widget.subject.id] = '1,2,3,4,5,'; - // goalAvgs[widget.subject.id] = '3.69'; - // goalBeforeGrades[widget.subject.id] = '3.69'; - // goalPinDates[widget.subject.id] = - // DateTime.now().toIso8601String(); - - await dbProvider.userStore - .storeSubjectGoalPlans(goalPlans, userId: user.id!); - await dbProvider.userStore - .storeSubjectGoalAverages(goalAvgs, userId: user.id!); - await dbProvider.userStore.storeSubjectGoalBefores( - goalBeforeGrades, - userId: user.id!); - await dbProvider.userStore.storeSubjectGoalPinDates( - goalPinDates, - userId: user.id!); - - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - }, - fillColor: Theme.of(context).colorScheme.secondary, - shape: const StadiumBorder(), - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - plansPage ? "track_it".i18n : "show_my_ways".i18n, - style: const TextStyle( - color: Colors.white, - fontSize: 20.0, - fontWeight: FontWeight.w600, - ), - ), - ), - ) - ], - ), - ), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/helpers/average_helper.dart'; +import 'package:refilc_kreta_api/models/grade.dart'; +import 'package:refilc_kreta_api/models/subject.dart'; +import 'package:refilc_kreta_api/providers/grade_provider.dart'; +import 'package:refilc_mobile_ui/common/average_display.dart'; +import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart'; +import 'package:refilc_plus/models/premium_scopes.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_input.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/route_option.dart'; + +class GoalTrackPopup extends StatefulWidget { + const GoalTrackPopup({super.key, required this.subject}); + + final GradeSubject subject; + + static void show(BuildContext context, {required GradeSubject subject}) => + showRoundedModalBottomSheet( + context, + child: GoalTrackPopup(subject: subject), + showHandle: true, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + ); + + @override + GoalTrackPopupState createState() => GoalTrackPopupState(); +} + +class GoalTrackPopupState extends State { + late UserProvider user; + late DatabaseProvider dbProvider; + late GradeProvider gradeProvider; + + List getSubjectGrades(GradeSubject subject) => + gradeProvider.grades.where((e) => e.subject == subject).toList(); + + double goalValue = 4.0; + List grades = []; + + Plan? recommended; + Plan? fastest; + Plan? selectedRoute; + List otherPlans = []; + + bool plansPage = false; + + @override + void initState() { + super.initState(); + user = Provider.of(context, listen: false); + dbProvider = Provider.of(context, listen: false); + } + + Future> fetchGoalPlans() async { + return await dbProvider.userQuery.subjectGoalPlans(userId: user.id!); + } + + Future> fetchGoalAverages() async { + return await dbProvider.userQuery.subjectGoalAverages(userId: user.id!); + } + + // haha bees lol + Future> fetchGoalBees() async { + return await dbProvider.userQuery.subjectGoalBefores(userId: user.id!); + } + + Future> fetchGoalPinDates() async { + return await dbProvider.userQuery.subjectGoalPinDates(userId: user.id!); + } + + PlanResult getResult() { + final currentAvg = GoalPlannerHelper.averageEvals(grades); + + recommended = null; + fastest = null; + otherPlans = []; + + if (currentAvg >= goalValue) return PlanResult.reached; + + final planner = GoalPlanner(goalValue, grades); + final plans = planner.solve(); + + plans.sort((a, b) => (a.avg - (2 * goalValue + 5) / 3) + .abs() + .compareTo(b.avg - (2 * goalValue + 5) / 3)); + + try { + final singleSolution = plans.every((e) => e.sigma == 0); + recommended = + plans.where((e) => singleSolution ? true : e.sigma > 0).first; + plans.removeWhere((e) => e == recommended); + } catch (_) {} + + plans.sort((a, b) => a.plan.length.compareTo(b.plan.length)); + + try { + fastest = plans.removeAt(0); + } catch (_) {} + + // print((recommended?.plan.length ?? 0).toString() + '-kuki'); + // print((fastest?.plan.length ?? 0).toString() + '--asd'); + + if ((((recommended?.plan.length ?? 0) - (fastest?.plan.length ?? 0)) >= + 5) && + fastest != null) { + recommended = fastest; + } + + if (recommended == null) { + recommended = null; + fastest = null; + otherPlans = []; + selectedRoute = null; + return PlanResult.unsolvable; + } + + // print(recommended!.plan.length.toString() + '--------'); + + if (recommended!.plan.length > 20) { + recommended = null; + fastest = null; + otherPlans = []; + selectedRoute = null; + return PlanResult.unreachable; + } + + otherPlans = List.from(plans); + + // only save 2 items if not plus member + if (!Provider.of(context) + .hasScope(PremiumScopes.unlimitedGoalPlanner)) { + if (otherPlans.length > 2) { + otherPlans.removeRange(2, otherPlans.length - 1); + } + } + + return PlanResult.available; + } + + void getGrades() { + grades = getSubjectGrades(widget.subject).toList(); + } + + @override + Widget build(BuildContext context) { + gradeProvider = Provider.of(context); + + getGrades(); + + final currentAvg = GoalPlannerHelper.averageEvals(grades); + + final result = getResult(); + + List subjectGrades = getSubjectGrades(widget.subject); + + double avg = AverageHelper.averageEvals(subjectGrades); + + double listLength = (otherPlans.length + + (recommended != null ? 1 : 0) + + (fastest != null && fastest != recommended ? 1 : 0)); + + return Container( + padding: const EdgeInsets.only(top: 24.0), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AverageDisplay( + average: avg, + scale: 1.3, + ), + const SizedBox(width: 12.0), + const Icon( + Icons.arrow_forward, + size: 24.0, + ), + const SizedBox(width: 12.0), + AverageDisplay( + average: goalValue, + border: true, + dashed: true, + scale: 1.3, + ), + ], + ), + const SizedBox( + height: 14.0, + ), + Text( + plansPage + ? 'goalplan_plans_title'.i18n + : 'goalplan_title'.i18n, + style: const TextStyle( + fontSize: 20.0, fontWeight: FontWeight.w700), + textAlign: TextAlign.center), + Text( + plansPage + ? 'goalplan_plans_subtitle'.i18n + : 'goalplan_subtitle'.i18n, + style: const TextStyle( + fontSize: 16.0, fontWeight: FontWeight.w500), + textAlign: TextAlign.center), + ], + ), + const SizedBox(height: 24.0), + if (!plansPage) + GoalInput( + value: goalValue, + currentAverage: currentAvg, + onChanged: (v) => setState(() { + selectedRoute = null; + goalValue = v; + }), + ), + if (plansPage && listLength > 2) + SizedBox( + height: (MediaQuery.of(context).size.height * 0.5), + child: SingleChildScrollView( + child: Column( + children: [ + if (recommended != null) + RouteOption( + plan: recommended!, + mark: RouteMark.recommended, + selected: selectedRoute == recommended!, + onSelected: () => setState(() { + selectedRoute = recommended; + }), + ), + if (fastest != null && fastest != recommended) + RouteOption( + plan: fastest!, + mark: RouteMark.fastest, + selected: selectedRoute == fastest!, + onSelected: () => setState(() { + selectedRoute = fastest; + }), + ), + ...otherPlans.map((e) => RouteOption( + plan: e, + selected: selectedRoute == e, + onSelected: () => setState(() { + selectedRoute = e; + }), + )), + if (result != PlanResult.available) + Text(result.name.i18n), + ], + ), + ), + ), + if (plansPage && listLength <= 2) + Column( + children: [ + if (recommended != null) + RouteOption( + plan: recommended!, + mark: RouteMark.recommended, + selected: selectedRoute == recommended!, + onSelected: () => setState(() { + selectedRoute = recommended; + }), + ), + if (fastest != null && fastest != recommended) + RouteOption( + plan: fastest!, + mark: RouteMark.fastest, + selected: selectedRoute == fastest!, + onSelected: () => setState(() { + selectedRoute = fastest; + }), + ), + ...otherPlans.map((e) => RouteOption( + plan: e, + selected: selectedRoute == e, + onSelected: () => setState(() { + selectedRoute = e; + }), + )), + if (result != PlanResult.available) Text(result.name.i18n), + ], + ), + const SizedBox(height: 24.0), + SizedBox( + width: double.infinity, + child: RawMaterialButton( + onPressed: () async { + if (!plansPage) { + setState(() { + plansPage = true; + }); + return; + } + + if (selectedRoute == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('${"pick_route".i18n}...'))); + } + + final goalPlans = await fetchGoalPlans(); + final goalAvgs = await fetchGoalAverages(); + final goalBeforeGrades = await fetchGoalBees(); + final goalPinDates = await fetchGoalPinDates(); + + goalPlans[widget.subject.id] = selectedRoute!.dbString; + goalAvgs[widget.subject.id] = goalValue.toStringAsFixed(2); + goalBeforeGrades[widget.subject.id] = + avg.toStringAsFixed(2); + goalPinDates[widget.subject.id] = + DateTime.now().toIso8601String(); + // goalPlans[widget.subject.id] = '1,2,3,4,5,'; + // goalAvgs[widget.subject.id] = '3.69'; + // goalBeforeGrades[widget.subject.id] = '3.69'; + // goalPinDates[widget.subject.id] = + // DateTime.now().toIso8601String(); + + await dbProvider.userStore + .storeSubjectGoalPlans(goalPlans, userId: user.id!); + await dbProvider.userStore + .storeSubjectGoalAverages(goalAvgs, userId: user.id!); + await dbProvider.userStore.storeSubjectGoalBefores( + goalBeforeGrades, + userId: user.id!); + await dbProvider.userStore.storeSubjectGoalPinDates( + goalPinDates, + userId: user.id!); + + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + }, + fillColor: Theme.of(context).colorScheme.secondary, + shape: const StadiumBorder(), + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + plansPage ? "track_it".i18n : "show_my_ways".i18n, + style: const TextStyle( + color: Colors.white, + fontSize: 20.0, + fontWeight: FontWeight.w600, + ), + ), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/mobile/goal_planner/grade_display.dart b/lib/ui/mobile/goal_planner/grade_display.dart index 31e3d65..3682bc3 100644 --- a/lib/ui/mobile/goal_planner/grade_display.dart +++ b/lib/ui/mobile/goal_planner/grade_display.dart @@ -1,34 +1,34 @@ -import 'package:refilc/models/settings.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_input.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class GradeDisplay extends StatelessWidget { - const GradeDisplay({super.key, required this.grade}); - - final int grade; - - @override - Widget build(BuildContext context) { - SettingsProvider settings = Provider.of(context); - - return Container( - width: 36, - height: 36, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: gradeColor(grade, settings).withOpacity(.3), - ), - child: Center( - child: Text( - grade.toInt().toString(), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22.0, - color: gradeColor(grade, settings), - ), - ), - ), - ); - } -} +import 'package:refilc/models/settings.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_input.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class GradeDisplay extends StatelessWidget { + const GradeDisplay({super.key, required this.grade}); + + final int grade; + + @override + Widget build(BuildContext context) { + SettingsProvider settings = Provider.of(context); + + return Container( + width: 36, + height: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: gradeColor(grade, settings).withOpacity(.3), + ), + child: Center( + child: Text( + grade.toInt().toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22.0, + color: gradeColor(grade, settings), + ), + ), + ), + ); + } +} diff --git a/lib/ui/mobile/goal_planner/graph.dart b/lib/ui/mobile/goal_planner/graph.dart index 64bda44..25f4701 100644 --- a/lib/ui/mobile/goal_planner/graph.dart +++ b/lib/ui/mobile/goal_planner/graph.dart @@ -1,269 +1,269 @@ -import 'dart:math'; - -import 'package:refilc/helpers/average_helper.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_kreta_api/models/grade.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/graph.i18n.dart'; -import 'package:flutter/material.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:provider/provider.dart'; - -class GoalGraph extends StatefulWidget { - const GoalGraph(this.data, {super.key, this.dayThreshold = 7, this.classAvg}); - - final List data; - final int dayThreshold; - final double? classAvg; - - @override - GoalGraphState createState() => GoalGraphState(); -} - -class GoalGraphState extends State { - late SettingsProvider settings; - - List getSpots(List data) { - List subjectData = []; - List> sortedData = [[]]; - - // Sort by date descending - data.sort((a, b) => -a.writeDate.compareTo(b.writeDate)); - - // Sort data to points by treshold - for (var element in data) { - if (sortedData.last.isNotEmpty && - sortedData.last.last.writeDate.difference(element.writeDate).inDays > - widget.dayThreshold) { - sortedData.add([]); - } - for (var dataList in sortedData) { - dataList.add(element); - } - } - - // Create FlSpots from points - for (var dataList in sortedData) { - double average = AverageHelper.averageEvals(dataList); - - if (dataList.isNotEmpty) { - subjectData.add(FlSpot( - dataList[0].writeDate.month + - (dataList[0].writeDate.day / 31) + - ((dataList[0].writeDate.year - data.last.writeDate.year) * 12), - double.parse(average.toStringAsFixed(2)), - )); - } - } - - return subjectData; - } - - @override - Widget build(BuildContext context) { - settings = Provider.of(context); - - List subjectSpots = []; - List ghostSpots = []; - List extraLinesV = []; - List extraLinesH = []; - - // Filter data - List data = widget.data - .where((e) => e.value.weight != 0) - .where((e) => e.type == GradeType.midYear) - .where((e) => e.gradeType?.name == "Osztalyzat") - .toList(); - - // Filter ghost data - List ghostData = widget.data - .where((e) => e.value.weight != 0) - .where((e) => e.type == GradeType.ghost) - .toList(); - - // Calculate average - double average = AverageHelper.averageEvals(data); - - // Calculate graph color - Color averageColor = average >= 1 && average <= 5 - ? ColorTween( - begin: settings.gradeColors[average.floor() - 1], - end: settings.gradeColors[average.ceil() - 1]) - .transform(average - average.floor())! - : Theme.of(context).colorScheme.secondary; - - subjectSpots = getSpots(data); - - // naplo/#73 - if (subjectSpots.isNotEmpty) { - ghostSpots = getSpots(data + ghostData); - - // hax - ghostSpots = ghostSpots - .where((e) => e.x >= subjectSpots.map((f) => f.x).reduce(max)) - .toList(); - ghostSpots = ghostSpots.map((e) => FlSpot(e.x + 0.1, e.y)).toList(); - ghostSpots.add(subjectSpots.firstWhere( - (e) => e.x >= subjectSpots.map((f) => f.x).reduce(max), - orElse: () => const FlSpot(-1, -1))); - ghostSpots.removeWhere( - (element) => element.x == -1 && element.y == -1); // naplo/#74 - } - - // Horizontal line displaying the class average - if (widget.classAvg != null && - widget.classAvg! > 0.0 && - settings.graphClassAvg) { - extraLinesH.add(HorizontalLine( - y: widget.classAvg!, - color: AppColors.of(context).text.withOpacity(.75), - )); - } - - // LineChart is really cute because it tries to render it's contents outside of it's rect. - return widget.data.length <= 2 - ? SizedBox( - height: 150, - child: Center( - child: Text( - "not_enough_grades".i18n, - textAlign: TextAlign.center, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ) - : ClipRect( - child: SizedBox( - height: 158, - child: subjectSpots.length > 1 - ? Padding( - padding: const EdgeInsets.only(top: 8.0, right: 8.0), - child: LineChart( - LineChartData( - extraLinesData: ExtraLinesData( - verticalLines: extraLinesV, - horizontalLines: extraLinesH), - lineBarsData: [ - LineChartBarData( - preventCurveOverShooting: true, - spots: subjectSpots, - isCurved: true, - color: averageColor, - barWidth: 8, - isStrokeCapRound: true, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - averageColor.withOpacity(0.7), - averageColor.withOpacity(0.3), - averageColor.withOpacity(0.2), - averageColor.withOpacity(0.1), - ], - stops: const [0.1, 0.6, 0.8, 1], - ), - // colors: [ - // averageColor.withOpacity(0.7), - // averageColor.withOpacity(0.3), - // averageColor.withOpacity(0.2), - // averageColor.withOpacity(0.1), - // ], - // gradientColorStops: [0.1, 0.6, 0.8, 1], - // gradientFrom: const Offset(0, 0), - // gradientTo: const Offset(0, 1), - ), - ), - if (ghostData.isNotEmpty && ghostSpots.isNotEmpty) - LineChartBarData( - preventCurveOverShooting: true, - spots: ghostSpots, - isCurved: true, - color: AppColors.of(context).text, - barWidth: 8, - isStrokeCapRound: true, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData( - show: true, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - AppColors.of(context) - .text - .withOpacity(0.7), - AppColors.of(context) - .text - .withOpacity(0.3), - AppColors.of(context) - .text - .withOpacity(0.2), - AppColors.of(context) - .text - .withOpacity(0.1), - ], - stops: const [0.1, 0.6, 0.8, 1], - ), - ), - ), - ], - minY: 1, - maxY: 5, - gridData: const FlGridData( - show: true, - horizontalInterval: 1, - // checkToShowVerticalLine: (_) => false, - // getDrawingHorizontalLine: (_) => FlLine( - // color: AppColors.of(context).text.withOpacity(.15), - // strokeWidth: 2, - // ), - // getDrawingVerticalLine: (_) => FlLine( - // color: AppColors.of(context).text.withOpacity(.25), - // strokeWidth: 2, - // ), - ), - lineTouchData: LineTouchData( - touchTooltipData: const LineTouchTooltipData( - // tooltipBgColor: Colors.grey.shade800, - fitInsideVertically: true, - fitInsideHorizontally: true, - ), - handleBuiltInTouches: true, - touchSpotThreshold: 20.0, - getTouchedSpotIndicator: (_, spots) { - return List.generate( - spots.length, - (index) => TouchedSpotIndicatorData( - FlLine( - color: Colors.grey.shade900, - strokeWidth: 3.5, - ), - FlDotData( - getDotPainter: (a, b, c, d) => - FlDotCirclePainter( - strokeWidth: 0, - color: Colors.grey.shade900, - radius: 10.0, - ), - ), - ), - ); - }, - ), - borderData: FlBorderData( - show: false, - border: Border.all( - color: Theme.of(context).scaffoldBackgroundColor, - width: 4, - ), - ), - ), - ), - ) - : null, - ), - ); - } -} +import 'dart:math'; + +import 'package:refilc/helpers/average_helper.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_kreta_api/models/grade.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/graph.i18n.dart'; +import 'package:flutter/material.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:provider/provider.dart'; + +class GoalGraph extends StatefulWidget { + const GoalGraph(this.data, {super.key, this.dayThreshold = 7, this.classAvg}); + + final List data; + final int dayThreshold; + final double? classAvg; + + @override + GoalGraphState createState() => GoalGraphState(); +} + +class GoalGraphState extends State { + late SettingsProvider settings; + + List getSpots(List data) { + List subjectData = []; + List> sortedData = [[]]; + + // Sort by date descending + data.sort((a, b) => -a.writeDate.compareTo(b.writeDate)); + + // Sort data to points by treshold + for (var element in data) { + if (sortedData.last.isNotEmpty && + sortedData.last.last.writeDate.difference(element.writeDate).inDays > + widget.dayThreshold) { + sortedData.add([]); + } + for (var dataList in sortedData) { + dataList.add(element); + } + } + + // Create FlSpots from points + for (var dataList in sortedData) { + double average = AverageHelper.averageEvals(dataList); + + if (dataList.isNotEmpty) { + subjectData.add(FlSpot( + dataList[0].writeDate.month + + (dataList[0].writeDate.day / 31) + + ((dataList[0].writeDate.year - data.last.writeDate.year) * 12), + double.parse(average.toStringAsFixed(2)), + )); + } + } + + return subjectData; + } + + @override + Widget build(BuildContext context) { + settings = Provider.of(context); + + List subjectSpots = []; + List ghostSpots = []; + List extraLinesV = []; + List extraLinesH = []; + + // Filter data + List data = widget.data + .where((e) => e.value.weight != 0) + .where((e) => e.type == GradeType.midYear) + .where((e) => e.gradeType?.name == "Osztalyzat") + .toList(); + + // Filter ghost data + List ghostData = widget.data + .where((e) => e.value.weight != 0) + .where((e) => e.type == GradeType.ghost) + .toList(); + + // Calculate average + double average = AverageHelper.averageEvals(data); + + // Calculate graph color + Color averageColor = average >= 1 && average <= 5 + ? ColorTween( + begin: settings.gradeColors[average.floor() - 1], + end: settings.gradeColors[average.ceil() - 1]) + .transform(average - average.floor())! + : Theme.of(context).colorScheme.secondary; + + subjectSpots = getSpots(data); + + // naplo/#73 + if (subjectSpots.isNotEmpty) { + ghostSpots = getSpots(data + ghostData); + + // hax + ghostSpots = ghostSpots + .where((e) => e.x >= subjectSpots.map((f) => f.x).reduce(max)) + .toList(); + ghostSpots = ghostSpots.map((e) => FlSpot(e.x + 0.1, e.y)).toList(); + ghostSpots.add(subjectSpots.firstWhere( + (e) => e.x >= subjectSpots.map((f) => f.x).reduce(max), + orElse: () => const FlSpot(-1, -1))); + ghostSpots.removeWhere( + (element) => element.x == -1 && element.y == -1); // naplo/#74 + } + + // Horizontal line displaying the class average + if (widget.classAvg != null && + widget.classAvg! > 0.0 && + settings.graphClassAvg) { + extraLinesH.add(HorizontalLine( + y: widget.classAvg!, + color: AppColors.of(context).text.withOpacity(.75), + )); + } + + // LineChart is really cute because it tries to render it's contents outside of it's rect. + return widget.data.length <= 2 + ? SizedBox( + height: 150, + child: Center( + child: Text( + "not_enough_grades".i18n, + textAlign: TextAlign.center, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ) + : ClipRect( + child: SizedBox( + height: 158, + child: subjectSpots.length > 1 + ? Padding( + padding: const EdgeInsets.only(top: 8.0, right: 8.0), + child: LineChart( + LineChartData( + extraLinesData: ExtraLinesData( + verticalLines: extraLinesV, + horizontalLines: extraLinesH), + lineBarsData: [ + LineChartBarData( + preventCurveOverShooting: true, + spots: subjectSpots, + isCurved: true, + color: averageColor, + barWidth: 8, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + averageColor.withOpacity(0.7), + averageColor.withOpacity(0.3), + averageColor.withOpacity(0.2), + averageColor.withOpacity(0.1), + ], + stops: const [0.1, 0.6, 0.8, 1], + ), + // colors: [ + // averageColor.withOpacity(0.7), + // averageColor.withOpacity(0.3), + // averageColor.withOpacity(0.2), + // averageColor.withOpacity(0.1), + // ], + // gradientColorStops: [0.1, 0.6, 0.8, 1], + // gradientFrom: const Offset(0, 0), + // gradientTo: const Offset(0, 1), + ), + ), + if (ghostData.isNotEmpty && ghostSpots.isNotEmpty) + LineChartBarData( + preventCurveOverShooting: true, + spots: ghostSpots, + isCurved: true, + color: AppColors.of(context).text, + barWidth: 8, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppColors.of(context) + .text + .withOpacity(0.7), + AppColors.of(context) + .text + .withOpacity(0.3), + AppColors.of(context) + .text + .withOpacity(0.2), + AppColors.of(context) + .text + .withOpacity(0.1), + ], + stops: const [0.1, 0.6, 0.8, 1], + ), + ), + ), + ], + minY: 1, + maxY: 5, + gridData: const FlGridData( + show: true, + horizontalInterval: 1, + // checkToShowVerticalLine: (_) => false, + // getDrawingHorizontalLine: (_) => FlLine( + // color: AppColors.of(context).text.withOpacity(.15), + // strokeWidth: 2, + // ), + // getDrawingVerticalLine: (_) => FlLine( + // color: AppColors.of(context).text.withOpacity(.25), + // strokeWidth: 2, + // ), + ), + lineTouchData: LineTouchData( + touchTooltipData: const LineTouchTooltipData( + // tooltipBgColor: Colors.grey.shade800, + fitInsideVertically: true, + fitInsideHorizontally: true, + ), + handleBuiltInTouches: true, + touchSpotThreshold: 20.0, + getTouchedSpotIndicator: (_, spots) { + return List.generate( + spots.length, + (index) => TouchedSpotIndicatorData( + FlLine( + color: Colors.grey.shade900, + strokeWidth: 3.5, + ), + FlDotData( + getDotPainter: (a, b, c, d) => + FlDotCirclePainter( + strokeWidth: 0, + color: Colors.grey.shade900, + radius: 10.0, + ), + ), + ), + ); + }, + ), + borderData: FlBorderData( + show: false, + border: Border.all( + color: Theme.of(context).scaffoldBackgroundColor, + width: 4, + ), + ), + ), + ), + ) + : null, + ), + ); + } +} diff --git a/lib/ui/mobile/goal_planner/graph.i18n.dart b/lib/ui/mobile/goal_planner/graph.i18n.dart index 50e2ea8..74ec227 100644 --- a/lib/ui/mobile/goal_planner/graph.i18n.dart +++ b/lib/ui/mobile/goal_planner/graph.i18n.dart @@ -1,21 +1,21 @@ -import 'package:i18n_extension/i18n_extension.dart'; - -extension Localization on String { - static final _t = Translations.byLocale("hu_hu") + - { - "en_en": { - "not_enough_grades": "Not enough data to show a graph here.", - }, - "hu_hu": { - "not_enough_grades": "Nem szereztél még elég jegyet grafikon mutatáshoz.", - }, - "de_de": { - "not_enough_grades": "Noch nicht genug Noten, um die Grafik zu zeigen.", - }, - }; - - String get i18n => localize(this, _t); - String fill(List params) => localizeFill(this, params); - String plural(int value) => localizePlural(value, this, _t); - String version(Object modifier) => localizeVersion(modifier, this, _t); -} +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "not_enough_grades": "Not enough data to show a graph here.", + }, + "hu_hu": { + "not_enough_grades": "Nem szereztél még elég jegyet grafikon mutatáshoz.", + }, + "de_de": { + "not_enough_grades": "Noch nicht genug Noten, um die Grafik zu zeigen.", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/lib/ui/mobile/goal_planner/route_option.dart b/lib/ui/mobile/goal_planner/route_option.dart index 8bf9be7..1f75eb2 100644 --- a/lib/ui/mobile/goal_planner/route_option.dart +++ b/lib/ui/mobile/goal_planner/route_option.dart @@ -1,204 +1,204 @@ -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; -import 'package:refilc_plus/ui/mobile/goal_planner/grade_display.dart'; -import 'package:flutter/material.dart'; - -enum RouteMark { recommended, fastest } - -class RouteOption extends StatelessWidget { - const RouteOption( - {super.key, - required this.plan, - this.mark, - this.selected = false, - required this.onSelected}); - - final Plan plan; - final RouteMark? mark; - final bool selected; - final void Function() onSelected; - - Widget markLabel({Color? colorOverride}) { - TextStyle style = - TextStyle(fontWeight: FontWeight.bold, color: colorOverride); - - switch (mark!) { - case RouteMark.recommended: - return Text("recommended".i18n, style: style); - case RouteMark.fastest: - return Text("fastest".i18n, style: style); - } - } - - Color markColor(BuildContext context) { - switch (mark) { - case RouteMark.recommended: - return const Color.fromARGB(255, 104, 93, 255); - case RouteMark.fastest: - return const Color.fromARGB(255, 255, 91, 146); - default: - return Theme.of(context).colorScheme.primary; - } - } - - @override - Widget build(BuildContext context) { - List gradeWidgets = []; - - for (int i = 5; i > 1; i--) { - final count = plan.plan.where((e) => e == i).length; - - if (count > 4) { - gradeWidgets.add(Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${count}x", - style: TextStyle( - fontSize: 22.0, - fontWeight: FontWeight.w500, - color: AppColors.of(context).text.withOpacity(.7), - ), - ), - const SizedBox(width: 4.0), - GradeDisplay(grade: i), - ], - )); - } else { - gradeWidgets - .addAll(List.generate(count, (_) => GradeDisplay(grade: i))); - } - - if (count > 0) { - gradeWidgets.add(SizedBox( - height: 36.0, - width: 32.0, - child: Center( - child: Icon(Icons.add, - color: AppColors.of(context).text.withOpacity(.5))), - )); - } - } - - gradeWidgets.removeLast(); - - return Padding( - padding: const EdgeInsets.only(bottom: 12.0), - child: SizedBox( - width: double.infinity, - child: Card( - surfaceTintColor: - selected ? markColor(context).withOpacity(.2) : Colors.white, - margin: EdgeInsets.zero, - elevation: 5, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - side: selected - ? BorderSide(color: markColor(context), width: 1.5) - : BorderSide.none, - ), - child: InkWell( - borderRadius: BorderRadius.circular(16.0), - onTap: onSelected, - child: Padding( - padding: const EdgeInsets.only( - top: 16.0, bottom: 16.0, left: 20.0, right: 12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (mark != null) ...[ - // Chip( - // label: markLabel(), - // visualDensity: VisualDensity.compact, - // backgroundColor: - // selected ? markColor(context) : Colors.transparent, - // labelPadding: const EdgeInsets.symmetric(horizontal: 8.0), - // labelStyle: - // TextStyle(color: selected ? Colors.white : null), - // shape: StadiumBorder( - // side: BorderSide( - // color: markColor(context), - // width: 3.0, - // ), - // ), - // ), - markLabel( - colorOverride: selected ? markColor(context) : null), - const SizedBox(height: 6.0), - ], - Wrap( - spacing: 4.0, - runSpacing: 8.0, - children: gradeWidgets, - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class RouteOptionRow extends StatelessWidget { - const RouteOptionRow({ - super.key, - required this.plan, - this.mark, - }); - - final Plan plan; - final RouteMark? mark; - - @override - Widget build(BuildContext context) { - List gradeWidgets = []; - - for (int i = 5; i > 1; i--) { - final count = plan.plan.where((e) => e == i).length; - - if (count > 4) { - gradeWidgets.add(Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "${count}x", - style: TextStyle( - fontSize: 22.0, - fontWeight: FontWeight.w500, - color: AppColors.of(context).text.withOpacity(.7), - ), - ), - const SizedBox(width: 4.0), - GradeDisplay(grade: i), - ], - )); - } else { - gradeWidgets - .addAll(List.generate(count, (_) => GradeDisplay(grade: i))); - } - - if (count > 0) { - gradeWidgets.add(SizedBox( - height: 36.0, - width: 32.0, - child: Center( - child: Icon(Icons.add, - color: AppColors.of(context).text.withOpacity(.5))), - )); - } - } - - gradeWidgets.removeLast(); - - return Wrap( - spacing: 4.0, - runSpacing: 8.0, - children: gradeWidgets, - ); - } -} +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/goal_planner_screen.i18n.dart'; +import 'package:refilc_plus/ui/mobile/goal_planner/grade_display.dart'; +import 'package:flutter/material.dart'; + +enum RouteMark { recommended, fastest } + +class RouteOption extends StatelessWidget { + const RouteOption( + {super.key, + required this.plan, + this.mark, + this.selected = false, + required this.onSelected}); + + final Plan plan; + final RouteMark? mark; + final bool selected; + final void Function() onSelected; + + Widget markLabel({Color? colorOverride}) { + TextStyle style = + TextStyle(fontWeight: FontWeight.bold, color: colorOverride); + + switch (mark!) { + case RouteMark.recommended: + return Text("recommended".i18n, style: style); + case RouteMark.fastest: + return Text("fastest".i18n, style: style); + } + } + + Color markColor(BuildContext context) { + switch (mark) { + case RouteMark.recommended: + return const Color.fromARGB(255, 104, 93, 255); + case RouteMark.fastest: + return const Color.fromARGB(255, 255, 91, 146); + default: + return Theme.of(context).colorScheme.primary; + } + } + + @override + Widget build(BuildContext context) { + List gradeWidgets = []; + + for (int i = 5; i > 1; i--) { + final count = plan.plan.where((e) => e == i).length; + + if (count > 4) { + gradeWidgets.add(Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${count}x", + style: TextStyle( + fontSize: 22.0, + fontWeight: FontWeight.w500, + color: AppColors.of(context).text.withOpacity(.7), + ), + ), + const SizedBox(width: 4.0), + GradeDisplay(grade: i), + ], + )); + } else { + gradeWidgets + .addAll(List.generate(count, (_) => GradeDisplay(grade: i))); + } + + if (count > 0) { + gradeWidgets.add(SizedBox( + height: 36.0, + width: 32.0, + child: Center( + child: Icon(Icons.add, + color: AppColors.of(context).text.withOpacity(.5))), + )); + } + } + + gradeWidgets.removeLast(); + + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: SizedBox( + width: double.infinity, + child: Card( + surfaceTintColor: + selected ? markColor(context).withOpacity(.2) : Colors.white, + margin: EdgeInsets.zero, + elevation: 5, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + side: selected + ? BorderSide(color: markColor(context), width: 1.5) + : BorderSide.none, + ), + child: InkWell( + borderRadius: BorderRadius.circular(16.0), + onTap: onSelected, + child: Padding( + padding: const EdgeInsets.only( + top: 16.0, bottom: 16.0, left: 20.0, right: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (mark != null) ...[ + // Chip( + // label: markLabel(), + // visualDensity: VisualDensity.compact, + // backgroundColor: + // selected ? markColor(context) : Colors.transparent, + // labelPadding: const EdgeInsets.symmetric(horizontal: 8.0), + // labelStyle: + // TextStyle(color: selected ? Colors.white : null), + // shape: StadiumBorder( + // side: BorderSide( + // color: markColor(context), + // width: 3.0, + // ), + // ), + // ), + markLabel( + colorOverride: selected ? markColor(context) : null), + const SizedBox(height: 6.0), + ], + Wrap( + spacing: 4.0, + runSpacing: 8.0, + children: gradeWidgets, + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class RouteOptionRow extends StatelessWidget { + const RouteOptionRow({ + super.key, + required this.plan, + this.mark, + }); + + final Plan plan; + final RouteMark? mark; + + @override + Widget build(BuildContext context) { + List gradeWidgets = []; + + for (int i = 5; i > 1; i--) { + final count = plan.plan.where((e) => e == i).length; + + if (count > 4) { + gradeWidgets.add(Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "${count}x", + style: TextStyle( + fontSize: 22.0, + fontWeight: FontWeight.w500, + color: AppColors.of(context).text.withOpacity(.7), + ), + ), + const SizedBox(width: 4.0), + GradeDisplay(grade: i), + ], + )); + } else { + gradeWidgets + .addAll(List.generate(count, (_) => GradeDisplay(grade: i))); + } + + if (count > 0) { + gradeWidgets.add(SizedBox( + height: 36.0, + width: 32.0, + child: Center( + child: Icon(Icons.add, + color: AppColors.of(context).text.withOpacity(.5))), + )); + } + } + + gradeWidgets.removeLast(); + + return Wrap( + spacing: 4.0, + runSpacing: 8.0, + children: gradeWidgets, + ); + } +} diff --git a/lib/ui/mobile/plus/activation_view/activation_dashboard.dart b/lib/ui/mobile/plus/activation_view/activation_dashboard.dart index 5dbfb32..7133ac8 100644 --- a/lib/ui/mobile/plus/activation_view/activation_dashboard.dart +++ b/lib/ui/mobile/plus/activation_view/activation_dashboard.dart @@ -1,201 +1,204 @@ -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -class ActivationDashboard extends StatefulWidget { - const ActivationDashboard({super.key}); - - @override - State createState() => _ActivationDashboardState(); -} - -class _ActivationDashboardState extends State { - bool manualActivationLoading = false; - - Future onManualActivation() async { - final data = await Clipboard.getData("text/plain"); - if (data == null || data.text == null || data.text == "") { - return; - } - setState(() { - manualActivationLoading = true; - }); - final result = - // ignore: use_build_context_synchronously - await context.read().auth.finishAuth(data.text!); - setState(() { - manualActivationLoading = false; - }); - - if (!result && mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text( - "Sikertelen aktiválás. Kérlek próbáld újra később!", - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), - backgroundColor: Colors.red, - )); - } - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - Center( - child: Image.asset( - "assets/icons/ic_rounded.png", - height: 64.0, - ), - // child: SvgPicture.asset( - // "assets/images/github.svg", - // height: 64.0, - // ), - ), - const SizedBox(height: 32.0), - const Text( - "Válassz fizetési módot, majd folytasd a fizetést a Stripe felületén, hogy aktiváld az előfizetésed.", - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18.0), - ), - // const SizedBox(height: 12.0), - // Card( - // shape: RoundedRectangleBorder( - // borderRadius: BorderRadius.circular(14.0)), - // child: const Padding( - // padding: EdgeInsets.all(16.0), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // children: [ - // Icon(FeatherIcons.alertTriangle, - // size: 20.0, color: Colors.orange), - // SizedBox(width: 12.0), - // Text( - // "Figyelem!", - // style: TextStyle( - // fontSize: 18.0, fontWeight: FontWeight.bold), - // ), - // ], - // ), - // SizedBox(height: 6.0), - // Text( - // "Az automatikus visszairányítás az alkalmazásba nem mindig működik. Ebben az esetben kérjük nyomd meg lent a \"Manuális aktiválás\" gombot!", - // style: TextStyle(fontSize: 16.0), - // ), - // ], - // ), - // ), - // ), - const SizedBox(height: 12.0), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14.0)), - child: const Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(FeatherIcons.alertTriangle, - size: 20.0, color: Colors.orange), - SizedBox(width: 12.0), - Text( - "Figyelem!", - style: TextStyle( - fontSize: 18.0, fontWeight: FontWeight.bold), - ), - ], - ), - SizedBox(height: 6.0), - Text( - "Az aktiválás azonnal történik, amint kifizetted a szolgáltatás díját. A szolgáltatás automatikusan megújul, lemondásra a beállításokban lesz lehetőséget.", - style: TextStyle(fontSize: 16.0), - ), - ], - ), - ), - ), - const SizedBox(height: 12.0), - Card( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14.0)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Ha fizetés után a Stripe nem irányít vissza az alkalmazásba automatikusan, aktiválhatod a támogatásod a munkamenet azonosítóval, melyet kimásolhatsz a hibás URL \"session_id\" paraméteréből.", - style: - TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500), - ), - const SizedBox(height: 6.0), - Center( - child: TextButton.icon( - onPressed: onManualActivation, - style: ButtonStyle( - foregroundColor: WidgetStatePropertyAll( - Theme.of(context).colorScheme.secondary), - overlayColor: WidgetStatePropertyAll(Theme.of(context) - .colorScheme - .secondary - .withOpacity(.1)), - ), - icon: manualActivationLoading - ? const SizedBox( - height: 16.0, - width: 16.0, - child: CircularProgressIndicator(), - ) - : const Icon(FeatherIcons.key, size: 20.0), - label: const Padding( - padding: EdgeInsets.only(left: 8.0), - child: Text( - "Aktiválás azonosítóval", - style: TextStyle(fontSize: 16.0), - ), - ), - ), - ), - ], - ), - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.only(bottom: 24.0), - child: Center( - child: TextButton.icon( - onPressed: () { - Navigator.of(context).pop(); - }, - style: ButtonStyle( - foregroundColor: - WidgetStatePropertyAll(AppColors.of(context).text), - overlayColor: WidgetStatePropertyAll( - AppColors.of(context).text.withOpacity(.1)), - ), - icon: const Icon(FeatherIcons.arrowLeft, size: 20.0), - label: const Text( - "Vissza", - style: TextStyle(fontSize: 16.0), - ), - ), - ), - ), - ], - ), - ); - } -} +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class ActivationDashboard extends StatefulWidget { + const ActivationDashboard({super.key}); + + @override + State createState() => _ActivationDashboardState(); +} + +class _ActivationDashboardState extends State { + bool manualActivationLoading = false; + + Future onManualActivation() async { + final data = await Clipboard.getData("text/plain"); + if (data == null || data.text == null || data.text == "") { + return; + } + setState(() { + manualActivationLoading = true; + }); + final result = + // ignore: use_build_context_synchronously + await context + .read() + .auth + .finishAuth(data.text!); + setState(() { + manualActivationLoading = false; + }); + + if (!result && mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text( + "Sikertelen aktiválás. Kérlek próbáld újra később!", + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + backgroundColor: Colors.red, + )); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + Center( + child: Image.asset( + "assets/icons/ic_rounded.png", + height: 64.0, + ), + // child: SvgPicture.asset( + // "assets/images/github.svg", + // height: 64.0, + // ), + ), + const SizedBox(height: 32.0), + const Text( + "Válassz fizetési módot, majd folytasd a fizetést a Stripe felületén, hogy aktiváld az előfizetésed.", + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.w700, fontSize: 18.0), + ), + // const SizedBox(height: 12.0), + // Card( + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.circular(14.0)), + // child: const Padding( + // padding: EdgeInsets.all(16.0), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // children: [ + // Icon(FeatherIcons.alertTriangle, + // size: 20.0, color: Colors.orange), + // SizedBox(width: 12.0), + // Text( + // "Figyelem!", + // style: TextStyle( + // fontSize: 18.0, fontWeight: FontWeight.bold), + // ), + // ], + // ), + // SizedBox(height: 6.0), + // Text( + // "Az automatikus visszairányítás az alkalmazásba nem mindig működik. Ebben az esetben kérjük nyomd meg lent a \"Manuális aktiválás\" gombot!", + // style: TextStyle(fontSize: 16.0), + // ), + // ], + // ), + // ), + // ), + const SizedBox(height: 12.0), + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14.0)), + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(FeatherIcons.alertTriangle, + size: 20.0, color: Colors.orange), + SizedBox(width: 12.0), + Text( + "Figyelem!", + style: TextStyle( + fontSize: 18.0, fontWeight: FontWeight.bold), + ), + ], + ), + SizedBox(height: 6.0), + Text( + "Az aktiválás azonnal történik, amint kifizetted a szolgáltatás díját. A szolgáltatás automatikusan megújul, lemondásra a beállításokban lesz lehetőséget.", + style: TextStyle(fontSize: 16.0), + ), + ], + ), + ), + ), + const SizedBox(height: 12.0), + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14.0)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Ha fizetés után a Stripe nem irányít vissza az alkalmazásba automatikusan, aktiválhatod a támogatásod a munkamenet azonosítóval, melyet kimásolhatsz a hibás URL \"session_id\" paraméteréből.", + style: + TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 6.0), + Center( + child: TextButton.icon( + onPressed: onManualActivation, + style: ButtonStyle( + foregroundColor: WidgetStatePropertyAll( + Theme.of(context).colorScheme.secondary), + overlayColor: WidgetStatePropertyAll(Theme.of(context) + .colorScheme + .secondary + .withOpacity(.1)), + ), + icon: manualActivationLoading + ? const SizedBox( + height: 16.0, + width: 16.0, + child: CircularProgressIndicator(), + ) + : const Icon(FeatherIcons.key, size: 20.0), + label: const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Text( + "Aktiválás azonosítóval", + style: TextStyle(fontSize: 16.0), + ), + ), + ), + ), + ], + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: Center( + child: TextButton.icon( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ButtonStyle( + foregroundColor: + WidgetStatePropertyAll(AppColors.of(context).text), + overlayColor: WidgetStatePropertyAll( + AppColors.of(context).text.withOpacity(.1)), + ), + icon: const Icon(FeatherIcons.arrowLeft, size: 20.0), + label: const Text( + "Vissza", + style: TextStyle(fontSize: 16.0), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/mobile/plus/activation_view/activation_view.dart b/lib/ui/mobile/plus/activation_view/activation_view.dart index 80cc4d2..c63d725 100644 --- a/lib/ui/mobile/plus/activation_view/activation_view.dart +++ b/lib/ui/mobile/plus/activation_view/activation_view.dart @@ -1,97 +1,103 @@ -import 'package:animations/animations.dart'; -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'package:refilc_plus/ui/mobile/plus/activation_view/activation_dashboard.dart'; -import 'package:flutter/material.dart'; -import 'package:lottie/lottie.dart'; -import 'package:provider/provider.dart'; -import 'package:refilc_plus/ui/mobile/plus/plus_things.i18n.dart'; - -class PremiumActivationView extends StatefulWidget { - const PremiumActivationView({super.key, required this.product}); - - final String product; - - @override - State createState() => _PremiumActivationViewState(); -} - -class _PremiumActivationViewState extends State - with SingleTickerProviderStateMixin { - late AnimationController animation; - bool activated = false; - - @override - void initState() { - super.initState(); - context.read().auth.initAuth(product: widget.product); - - animation = - AnimationController(vsync: this, duration: const Duration(seconds: 2)); - } - - @override - void dispose() { - animation.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final premium = context.watch(); - - if (premium.hasPremium && !activated) { - activated = true; - animation.forward(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Future.delayed(const Duration(seconds: 5)).then((value) { - if (mounted) { - // pop the anim - Navigator.of(context).pop(); - // pop the plus view - Navigator.of(context).pop(); - // show alert to save code - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "copy_code_asap".i18n, - textAlign: TextAlign.center, - style: TextStyle( - color: AppColors.of(context).text, - fontWeight: FontWeight.w600, - ), - ), - backgroundColor: AppColors.of(context).background, - ), - ); - } - }); - }); - } - - return Scaffold( - body: PageTransitionSwitcher( - transitionBuilder: (child, primaryAnimation, secondaryAnimation) => - SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - fillColor: Colors.transparent, - child: child, - ), - child: premium.hasPremium - ? Center( - child: SizedBox( - width: 400, - child: Lottie.network( - "https://assets2.lottiefiles.com/packages/lf20_wkebwzpz.json", - controller: animation, - ), - ), - ) - : const SafeArea(child: ActivationDashboard()), - ), - ); - } -} +import 'package:animations/animations.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:refilc_plus/ui/mobile/plus/activation_view/activation_dashboard.dart'; +import 'package:flutter/material.dart'; +import 'package:lottie/lottie.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc_plus/ui/mobile/plus/plus_things.i18n.dart'; + +class PremiumActivationView extends StatefulWidget { + const PremiumActivationView({ + super.key, + required this.product, + required this.paymentProvider, + }); + + final String product; + final String paymentProvider; + + @override + State createState() => _PremiumActivationViewState(); +} + +class _PremiumActivationViewState extends State + with SingleTickerProviderStateMixin { + late AnimationController animation; + bool activated = false; + + @override + void initState() { + super.initState(); + context.read().auth.initAuth( + product: widget.product, paymentProvider: widget.paymentProvider); + + animation = + AnimationController(vsync: this, duration: const Duration(seconds: 2)); + } + + @override + void dispose() { + animation.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final premium = context.watch(); + + if (premium.hasPremium && !activated) { + activated = true; + animation.forward(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + Future.delayed(const Duration(seconds: 5)).then((value) { + if (mounted) { + // pop the anim + Navigator.of(context).pop(); + // pop the plus view + Navigator.of(context).pop(); + // show alert to save code + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "copy_code_asap".i18n, + textAlign: TextAlign.center, + style: TextStyle( + color: AppColors.of(context).text, + fontWeight: FontWeight.w600, + ), + ), + backgroundColor: AppColors.of(context).background, + ), + ); + } + }); + }); + } + + return Scaffold( + body: PageTransitionSwitcher( + transitionBuilder: (child, primaryAnimation, secondaryAnimation) => + SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + fillColor: Colors.transparent, + child: child, + ), + child: premium.hasPremium + ? Center( + child: SizedBox( + width: 400, + child: Lottie.network( + "https://assets2.lottiefiles.com/packages/lf20_wkebwzpz.json", + controller: animation, + ), + ), + ) + : const SafeArea(child: ActivationDashboard()), + ), + ); + } +} diff --git a/lib/ui/mobile/plus/plus_things.i18n.dart b/lib/ui/mobile/plus/plus_things.i18n.dart index 011bf52..49f8ab1 100644 --- a/lib/ui/mobile/plus/plus_things.i18n.dart +++ b/lib/ui/mobile/plus/plus_things.i18n.dart @@ -1,138 +1,138 @@ -import 'package:i18n_extension/i18n_extension.dart'; - -extension Localization on String { - static final _t = Translations.byLocale("hu_hu") + - { - "en_en": { - // upsell titles - "u_title_1": "Use more accounts?", - "u_title_2": "Want to try the updates in advance?", - "u_title_3": "\"Hi kitty, do you have an owner?\"", - "u_title_4": "Would you write down your tasks?", - "u_title_5": "Lazy to do maths?", - "u_title_6": "I know, the plain grey is not very nice :P", - "u_title_7": - "\"What were we doing in class? Was there English homework??\"", - "u_title_8": "Now that's something special!", - "u_title_9": "Woah! What beautiful letters!", - "u_title_10": "Need more suggestions?", - "u_title_11": "Not epic, but ultra-super?", - "u_title_12": "Do you even need it in your calendar?!", - "u_title_13": "Wanna see your past years' grades?", - // upsell descriptions - "u_desc_1": "The limit increases with each tier.", - "u_desc_2": - "Subscribe to reFilc+ to receive beta updates in advance.", - "u_desc_3": "For a unique greeting, just the lowest tier is enough!", - "u_desc_4": - "Support us and make a note of all your important things.", - "u_desc_5": - "reFilc+ makes it easier to calculate your projected average.", - "u_desc_6": "With Gold tier, you can recolour to anything.", - "u_desc_7": "No more questions in Gold.", - "u_desc_8": "Upgrade to Gold to change the app icon.", - "u_desc_9": "You can also change the font with Gold tier.", - "u_desc_10": - "Support us on Gold tier and use all the features of goal setting!", - "u_desc_11": "With reFilc+ lowest tier it's also available!", - "u_desc_12": "Sync your time-table with reFilc+ Gold!", - "u_desc_13": "You can export every year's grades with reFilc+!", - // button - "subscribe": "Subscribe", - "subscriber": "Subscribed", - // other - "copy_code_asap": - "Copy your reFilc+ ID, in case you loose your device!", - }, - "hu_hu": { - // upsell titles - "u_title_1": "Több fiókot használnál?", - "u_title_2": "Előre kipróbálnád a frissítéseket?", - "u_title_3": "\"Szia cica, van gazdád?\"", - "u_title_4": "Felírnád a feladataid?", - "u_title_5": "Lusta vagy matekozni?", - "u_title_6": "Tudom, nem túl szép a sima szürke :P", - "u_title_7": "\"Mit is csináltunk órán? Volt angol házi??\"", - "u_title_8": "Ez aztán különleges!", - "u_title_9": "Woah! Micsoda gyönyörű betűk!", - "u_title_10": "Még több javaslat kell?", - "u_title_11": "Nem epikus, hanem ultraszuper?", - "u_title_12": "Még a naptáradba is kell?!", - "u_title_13": "Szeretnéd látni az előző évi jegyeid?", - // upsell descriptions - "u_desc_1": "Minden támogatási szinttel egyre magasabb a limit.", - "u_desc_2": - "Fizess elő reFilc+-ra, hogy előre megkapd a béta frissítéseket.", - "u_desc_3": "Az egyedi üdvözléshez elég csupán a legalsó szint!", - "u_desc_4": "Támogass minket, és jegyezd fel minden fontos dolgod.", - "u_desc_5": "reFilc+-al egyszerűbb kiszámolnod a tervezett átlagod.", - "u_desc_6": "Gold szintű támogatással átszínezhetsz bármilyenre.", - "u_desc_7": "Nincs több ilyen kérdés, ha Gold szinten támogatsz.", - "u_desc_8": - "Fizess elő Gold szintre az alkalmazás ikonjának megváltoztatásához.", - "u_desc_9": - "Gold szintű támogatással megváltoztathatod a betűtípust is.", - "u_desc_10": - "Támogass Gold szinten és használd ki a cél kitűzés minden funkcióját!", - "u_desc_11": "A reFilc+ alap szintjével ez is elérhető!", - "u_desc_12": "Szinkronizáld az órarended reFilc+ Gold-al!", - "u_desc_13": "Minden évi jegyedet exportálhatod reFilc+-al!", - // button - "subscribe": "Előfizetés", - "subscriber": "Előfizetve", - // other - "copy_code_asap": - "Másold ki a reFilc+ ID-t, mielőtt elveszítenéd a telefonod!", - }, - "de_de": { - // upsell titles - "u_title_1": "Mehr Accounts nutzen?", - "u_title_2": "Willst du die Updates im vorraus testen?", - "u_title_3": "\"Hallo mein Kätzchen, hast du einen besitzer?\"", - "u_title_4": "Würdest du deine Aufgaben aufschreiben?", - "u_title_5": "Faul um Mathe zu machen?", - "u_title_6": "Ich weiß, das schlichte Grau ist nicht so toll :P", - "u_title_7": - "\"Was haben wir im Unterricht gemacht? Gab es Englisch Hausaufgaben??\"", - "u_title_8": "Na das ist mal was besonderes!", - "u_title_9": "Wow! Was für schöne Texte!", - "u_title_10": "Brauchst du mehr Vorschläge?", - "u_title_11": - "Willst du vielleicht statt episch etwas anderes wie super?", - "u_title_12": "Brauchst du das wirklich in deinem Kalender?!", - "u_title_13": "Möchtest du deine Noten der vergangenen Jahre sehen?", - // upsell descriptions - "u_desc_1": "Das limit erhöht sich mit jedem Abo-plan.", - "u_desc_2": - "Abonniere reFilc+ um Beta Updates im vorraus zu erhalten.", - "u_desc_3": - "Für eine eigene Begrüßung ist der niedrigste Abo-Plan schon genug!", - "u_desc_4": - "Unterstütze uns und schreib alles wichtige für dich auf.", - "u_desc_5": - "reFilc+ macht es einfacher deinen Durchschnitt zu berechnen.", - "u_desc_6": "Mit dem Gold-Plan, kannst du alles anders Färben.", - "u_desc_7": "Keine weiteren Fragen mit Gold.", - "u_desc_8": "Upgrade auf Gold um das App Icon zu ändern.", - "u_desc_9": - "Du kannst mit dem Gold-Plan auch die Schriftart verändern.", - "u_desc_10": - "Unterstütze uns mit einem Gold-Plan und benutze alle features des Ziel-setzens!", - "u_desc_11": - "Mit reFilc+ niedrigstem Abo-Plan ist es auch Verfügbar!", - "u_desc_12": "Synchronisiere deinen Stundenplan mit reFilc+ Gold!", - "u_desc_13": "Du kannst jede Jahresnote mit reFilc+ exportieren!", - // button - "subscribe": "Abonnieren", - "subscriber": "im Abonnement", - // other - "copy_code_asap": - "Kopieren Sie Ihre reFilc+ ID, bevor Sie Ihr Handy verlieren!", - }, - }; - - String get i18n => localize(this, _t); - String fill(List params) => localizeFill(this, params); - String plural(int value) => localizePlural(value, this, _t); - String version(Object modifier) => localizeVersion(modifier, this, _t); -} +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + // upsell titles + "u_title_1": "Use more accounts?", + "u_title_2": "Want to try the updates in advance?", + "u_title_3": "\"Hi kitty, do you have an owner?\"", + "u_title_4": "Would you write down your tasks?", + "u_title_5": "Lazy to do maths?", + "u_title_6": "I know, the plain grey is not very nice :P", + "u_title_7": + "\"What were we doing in class? Was there English homework??\"", + "u_title_8": "Now that's something special!", + "u_title_9": "Woah! What beautiful letters!", + "u_title_10": "Need more suggestions?", + "u_title_11": "Not epic, but ultra-super?", + "u_title_12": "Do you even need it in your calendar?!", + "u_title_13": "Wanna see your past years' grades?", + // upsell descriptions + "u_desc_1": "The limit increases with each tier.", + "u_desc_2": + "Subscribe to reFilc+ to receive beta updates in advance.", + "u_desc_3": "For a unique greeting, just the lowest tier is enough!", + "u_desc_4": + "Support us and make a note of all your important things.", + "u_desc_5": + "reFilc+ makes it easier to calculate your projected average.", + "u_desc_6": "With Gold tier, you can recolour to anything.", + "u_desc_7": "No more questions in Gold.", + "u_desc_8": "Upgrade to Gold to change the app icon.", + "u_desc_9": "You can also change the font with Gold tier.", + "u_desc_10": + "Support us on Gold tier and use all the features of goal setting!", + "u_desc_11": "With reFilc+ lowest tier it's also available!", + "u_desc_12": "Sync your time-table with reFilc+ Gold!", + "u_desc_13": "You can export every year's grades with reFilc+!", + // button + "subscribe": "Subscribe", + "subscriber": "Subscribed", + // other + "copy_code_asap": + "Copy your reFilc+ ID, in case you loose your device!", + }, + "hu_hu": { + // upsell titles + "u_title_1": "Több fiókot használnál?", + "u_title_2": "Előre kipróbálnád a frissítéseket?", + "u_title_3": "\"Szia cica, van gazdád?\"", + "u_title_4": "Felírnád a feladataid?", + "u_title_5": "Lusta vagy matekozni?", + "u_title_6": "Tudom, nem túl szép a sima szürke :P", + "u_title_7": "\"Mit is csináltunk órán? Volt angol házi??\"", + "u_title_8": "Ez aztán különleges!", + "u_title_9": "Woah! Micsoda gyönyörű betűk!", + "u_title_10": "Még több javaslat kell?", + "u_title_11": "Nem epikus, hanem ultraszuper?", + "u_title_12": "Még a naptáradba is kell?!", + "u_title_13": "Szeretnéd látni az előző évi jegyeid?", + // upsell descriptions + "u_desc_1": "Minden támogatási szinttel egyre magasabb a limit.", + "u_desc_2": + "Fizess elő reFilc+-ra, hogy előre megkapd a béta frissítéseket.", + "u_desc_3": "Az egyedi üdvözléshez elég csupán a legalsó szint!", + "u_desc_4": "Támogass minket, és jegyezd fel minden fontos dolgod.", + "u_desc_5": "reFilc+-al egyszerűbb kiszámolnod a tervezett átlagod.", + "u_desc_6": "Gold szintű támogatással átszínezhetsz bármilyenre.", + "u_desc_7": "Nincs több ilyen kérdés, ha Gold szinten támogatsz.", + "u_desc_8": + "Fizess elő Gold szintre az alkalmazás ikonjának megváltoztatásához.", + "u_desc_9": + "Gold szintű támogatással megváltoztathatod a betűtípust is.", + "u_desc_10": + "Támogass Gold szinten és használd ki a cél kitűzés minden funkcióját!", + "u_desc_11": "A reFilc+ alap szintjével ez is elérhető!", + "u_desc_12": "Szinkronizáld az órarended reFilc+ Gold-al!", + "u_desc_13": "Minden évi jegyedet exportálhatod reFilc+-al!", + // button + "subscribe": "Előfizetés", + "subscriber": "Előfizetve", + // other + "copy_code_asap": + "Másold ki a reFilc+ ID-t, mielőtt elveszítenéd a telefonod!", + }, + "de_de": { + // upsell titles + "u_title_1": "Mehr Accounts nutzen?", + "u_title_2": "Willst du die Updates im vorraus testen?", + "u_title_3": "\"Hallo mein Kätzchen, hast du einen besitzer?\"", + "u_title_4": "Würdest du deine Aufgaben aufschreiben?", + "u_title_5": "Faul um Mathe zu machen?", + "u_title_6": "Ich weiß, das schlichte Grau ist nicht so toll :P", + "u_title_7": + "\"Was haben wir im Unterricht gemacht? Gab es Englisch Hausaufgaben??\"", + "u_title_8": "Na das ist mal was besonderes!", + "u_title_9": "Wow! Was für schöne Texte!", + "u_title_10": "Brauchst du mehr Vorschläge?", + "u_title_11": + "Willst du vielleicht statt episch etwas anderes wie super?", + "u_title_12": "Brauchst du das wirklich in deinem Kalender?!", + "u_title_13": "Möchtest du deine Noten der vergangenen Jahre sehen?", + // upsell descriptions + "u_desc_1": "Das limit erhöht sich mit jedem Abo-plan.", + "u_desc_2": + "Abonniere reFilc+ um Beta Updates im vorraus zu erhalten.", + "u_desc_3": + "Für eine eigene Begrüßung ist der niedrigste Abo-Plan schon genug!", + "u_desc_4": + "Unterstütze uns und schreib alles wichtige für dich auf.", + "u_desc_5": + "reFilc+ macht es einfacher deinen Durchschnitt zu berechnen.", + "u_desc_6": "Mit dem Gold-Plan, kannst du alles anders Färben.", + "u_desc_7": "Keine weiteren Fragen mit Gold.", + "u_desc_8": "Upgrade auf Gold um das App Icon zu ändern.", + "u_desc_9": + "Du kannst mit dem Gold-Plan auch die Schriftart verändern.", + "u_desc_10": + "Unterstütze uns mit einem Gold-Plan und benutze alle features des Ziel-setzens!", + "u_desc_11": + "Mit reFilc+ niedrigstem Abo-Plan ist es auch Verfügbar!", + "u_desc_12": "Synchronisiere deinen Stundenplan mit reFilc+ Gold!", + "u_desc_13": "Du kannst jede Jahresnote mit reFilc+ exportieren!", + // button + "subscribe": "Abonnieren", + "subscriber": "im Abonnement", + // other + "copy_code_asap": + "Kopieren Sie Ihre reFilc+ ID, bevor Sie Ihr Handy verlieren!", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/lib/ui/mobile/plus/premium_inline.dart b/lib/ui/mobile/plus/premium_inline.dart index c3ea0ce..6d58fe3 100644 --- a/lib/ui/mobile/plus/premium_inline.dart +++ b/lib/ui/mobile/plus/premium_inline.dart @@ -1,74 +1,74 @@ -// ignore_for_file: unused_element - -import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; -import 'package:flutter/material.dart'; - -enum PremiumInlineFeature { nickname, theme, widget, goal, stats } - -const Map _featureAssets = { - PremiumInlineFeature.nickname: - "assets/images/premium_nickname_inline_showcase.png", - PremiumInlineFeature.theme: "assets/images/premium_theme_inline_showcase.png", - PremiumInlineFeature.widget: - "assets/images/premium_widget_inline_showcase.png", - PremiumInlineFeature.goal: "assets/images/premium_goal_inline_showcase.png", - PremiumInlineFeature.stats: "assets/images/premium_stats_inline_showcase.png", -}; - -const Map _featuresInline = { - PremiumInlineFeature.nickname: PremiumFeature.profile, - PremiumInlineFeature.theme: PremiumFeature.customcolors, - PremiumInlineFeature.widget: PremiumFeature.widget, - // PremiumInlineFeature.goal: PremiumFeature.goalplanner, - PremiumInlineFeature.stats: PremiumFeature.gradestats, -}; - -class PremiumInline extends StatelessWidget { - const PremiumInline({super.key, required this.features}); - - final List features; - - String _getAsset() { - for (int i = 0; i < features.length; i++) { - if (DateTime.now().day % features.length == i) { - return _featureAssets[features[i]]!; - } - } - - return _featureAssets[features[0]]!; - } - - PremiumFeature _getFeature() { - for (int i = 0; i < features.length; i++) { - if (DateTime.now().day % features.length == i) { - return _featuresInline[features[i]]!; - } - } - - return _featuresInline[features[0]]!; - } - - @override - Widget build(BuildContext context) { - return const SizedBox( - height: 0, - ); - // return Stack( - // children: [ - // Image.asset(_getAsset()), - // Positioned.fill( - // child: Material( - // type: MaterialType.transparency, - // child: InkWell( - // borderRadius: BorderRadius.circular(16.0), - // onTap: () { - // PlusLockedFeaturePopup.show( - // context: context, feature: _getFeature()); - // }, - // ), - // ), - // ), - // ], - // ); - } -} +// ignore_for_file: unused_element + +import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; +import 'package:flutter/material.dart'; + +enum PremiumInlineFeature { nickname, theme, widget, goal, stats } + +const Map _featureAssets = { + PremiumInlineFeature.nickname: + "assets/images/premium_nickname_inline_showcase.png", + PremiumInlineFeature.theme: "assets/images/premium_theme_inline_showcase.png", + PremiumInlineFeature.widget: + "assets/images/premium_widget_inline_showcase.png", + PremiumInlineFeature.goal: "assets/images/premium_goal_inline_showcase.png", + PremiumInlineFeature.stats: "assets/images/premium_stats_inline_showcase.png", +}; + +const Map _featuresInline = { + PremiumInlineFeature.nickname: PremiumFeature.profile, + PremiumInlineFeature.theme: PremiumFeature.customcolors, + PremiumInlineFeature.widget: PremiumFeature.widget, + // PremiumInlineFeature.goal: PremiumFeature.goalplanner, + PremiumInlineFeature.stats: PremiumFeature.gradestats, +}; + +class PremiumInline extends StatelessWidget { + const PremiumInline({super.key, required this.features}); + + final List features; + + String _getAsset() { + for (int i = 0; i < features.length; i++) { + if (DateTime.now().day % features.length == i) { + return _featureAssets[features[i]]!; + } + } + + return _featureAssets[features[0]]!; + } + + PremiumFeature _getFeature() { + for (int i = 0; i < features.length; i++) { + if (DateTime.now().day % features.length == i) { + return _featuresInline[features[i]]!; + } + } + + return _featuresInline[features[0]]!; + } + + @override + Widget build(BuildContext context) { + return const SizedBox( + height: 0, + ); + // return Stack( + // children: [ + // Image.asset(_getAsset()), + // Positioned.fill( + // child: Material( + // type: MaterialType.transparency, + // child: InkWell( + // borderRadius: BorderRadius.circular(16.0), + // onTap: () { + // PlusLockedFeaturePopup.show( + // context: context, feature: _getFeature()); + // }, + // ), + // ), + // ), + // ], + // ); + } +} diff --git a/lib/ui/mobile/plus/settings_inline.dart b/lib/ui/mobile/plus/settings_inline.dart index 2653913..4f19639 100644 --- a/lib/ui/mobile/plus/settings_inline.dart +++ b/lib/ui/mobile/plus/settings_inline.dart @@ -1,91 +1,91 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:refilc_mobile_ui/plus/plus_screen.dart'; -import 'package:refilc_plus/models/premium_scopes.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'plus_things.i18n.dart'; -import 'package:refilc_mobile_ui/screens/settings/settings_helper.dart'; - -class PlusSettingsInline extends StatelessWidget { - const PlusSettingsInline({super.key}); - - @override - Widget build(BuildContext context) { - final String plusTier = Provider.of(context) - .hasScope(PremiumScopes.tierGold) - ? 'gold' - : (Provider.of(context).hasScope(PremiumScopes.tierBasic) - ? 'basic' - : 'none'); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: GestureDetector( - onTap: () { - if (plusTier == 'none') { - Navigator.of(context, rootNavigator: true) - .push(MaterialPageRoute(builder: (context) { - return const PlusScreen(); - })); - } else { - SettingsHelper.plusOptions(context); - } - }, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - image: DecorationImage( - image: plusTier == 'gold' - ? const AssetImage('assets/images/btn_plus_gold.png') - : const AssetImage('assets/images/btn_plus_standard.png'), - fit: BoxFit.fitWidth, - ), - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const SizedBox( - width: 2.0, - ), - Image.asset( - plusTier == 'gold' - ? 'assets/images/plus_tier_ink.png' - : 'assets/images/plus_tier_cap.png', - width: 23.0, - height: 23.0, - ), - const SizedBox( - width: 14.0, - ), - Text( - 'reFilc+', - style: TextStyle( - color: plusTier == 'gold' - ? const Color(0xFF341C01) - : const Color(0xFF150D4E), - fontSize: 18.0, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - Text( - plusTier == 'none' ? '0.99 €' : 'subscriber'.i18n, - style: const TextStyle( - color: Color(0xFF150D4E), - fontSize: 15.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ); - } -} +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc_mobile_ui/plus/plus_screen.dart'; +import 'package:refilc_plus/models/premium_scopes.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'plus_things.i18n.dart'; +import 'package:refilc_mobile_ui/screens/settings/settings_helper.dart'; + +class PlusSettingsInline extends StatelessWidget { + const PlusSettingsInline({super.key}); + + @override + Widget build(BuildContext context) { + final String plusTier = Provider.of(context) + .hasScope(PremiumScopes.tierGold) + ? 'gold' + : (Provider.of(context).hasScope(PremiumScopes.tierBasic) + ? 'basic' + : 'none'); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: GestureDetector( + onTap: () { + if (plusTier == 'none') { + Navigator.of(context, rootNavigator: true) + .push(MaterialPageRoute(builder: (context) { + return const PlusScreen(); + })); + } else { + SettingsHelper.plusOptions(context); + } + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + image: plusTier == 'gold' + ? const AssetImage('assets/images/btn_plus_gold.png') + : const AssetImage('assets/images/btn_plus_standard.png'), + fit: BoxFit.fitWidth, + ), + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + width: 2.0, + ), + Image.asset( + plusTier == 'gold' + ? 'assets/images/plus_tier_ink.png' + : 'assets/images/plus_tier_cap.png', + width: 23.0, + height: 23.0, + ), + const SizedBox( + width: 14.0, + ), + Text( + 'reFilc+', + style: TextStyle( + color: plusTier == 'gold' + ? const Color(0xFF341C01) + : const Color(0xFF150D4E), + fontSize: 18.0, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + Text( + plusTier == 'none' ? '0.99 €' : 'subscriber'.i18n, + style: const TextStyle( + color: Color(0xFF150D4E), + fontSize: 15.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/mobile/plus/upsell.dart b/lib/ui/mobile/plus/upsell.dart index b65cc01..2a16ca5 100644 --- a/lib/ui/mobile/plus/upsell.dart +++ b/lib/ui/mobile/plus/upsell.dart @@ -1,486 +1,486 @@ -import 'package:flutter_svg/svg.dart'; -import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart'; -import 'package:refilc_mobile_ui/plus/plus_screen.dart'; -import 'package:flutter/material.dart'; -import 'plus_things.i18n.dart'; - -enum PremiumFeature { - // old things - gradestats, - customcolors, - profile, - iconpack, - subjectrename, - weeklytimetable, - widget, - // new things - moreAccounts, // cap, (ink, sponge) - betaReleases, // cap - welcomeMessage, // cap - selfNotes, // cap - gradeCalculation, // ink - liveActivity, // ink - timetableNotes, // ink - iconChange, // sponge - fontChange, // sponge - goalPlanner, // sponge - gradeRarities, - calendarSync, - gradeExporting, // basic -} - -enum PremiumFeatureLevel { - old, - cap, - ink, - sponge, - // new new new - basic, - gold, -} - -const Map _featureLevels = { - // old things - PremiumFeature.gradestats: PremiumFeatureLevel.old, - PremiumFeature.customcolors: PremiumFeatureLevel.old, - PremiumFeature.profile: PremiumFeatureLevel.old, - PremiumFeature.iconpack: PremiumFeatureLevel.old, - PremiumFeature.subjectrename: PremiumFeatureLevel.old, - PremiumFeature.weeklytimetable: PremiumFeatureLevel.old, - PremiumFeature.widget: PremiumFeatureLevel.old, - // new things - PremiumFeature.moreAccounts: PremiumFeatureLevel.basic, - PremiumFeature.betaReleases: PremiumFeatureLevel.basic, - PremiumFeature.welcomeMessage: PremiumFeatureLevel.basic, - PremiumFeature.selfNotes: PremiumFeatureLevel.basic, - PremiumFeature.gradeCalculation: PremiumFeatureLevel.basic, - PremiumFeature.liveActivity: PremiumFeatureLevel.gold, - PremiumFeature.timetableNotes: PremiumFeatureLevel.gold, - PremiumFeature.iconChange: PremiumFeatureLevel.gold, - PremiumFeature.fontChange: PremiumFeatureLevel.gold, - PremiumFeature.goalPlanner: PremiumFeatureLevel.gold, - PremiumFeature.gradeRarities: PremiumFeatureLevel.basic, - PremiumFeature.calendarSync: PremiumFeatureLevel.gold, - PremiumFeature.gradeExporting: PremiumFeatureLevel.basic, -}; - -// const Map _featureAssets = { -// // old -// PremiumFeature.gradestats: "assets/images/premium_stats_showcase.png", -// PremiumFeature.customcolors: "assets/images/premium_theme_showcase.png", -// PremiumFeature.profile: "assets/images/premium_nickname_showcase.png", -// PremiumFeature.weeklytimetable: -// "assets/images/premium_timetable_showcase.png", -// // PremiumFeature.goalplanner: "assets/images/premium_goal_showcase.png", -// PremiumFeature.widget: "assets/images/premium_widget_showcase.png", -// // new -// PremiumFeature.moreAccounts: "assets/images/premium_banner/more_accounts.png", -// PremiumFeature.betaReleases: "assets/images/premium_banner/beta_releases.png", -// PremiumFeature.welcomeMessage: -// "assets/images/premium_banner/welcome_message.png", -// PremiumFeature.selfNotes: "assets/images/premium_banner/self_notes.png", -// PremiumFeature.gradeCalculation: -// "assets/images/premium_banner/grade_calc.png", -// PremiumFeature.liveActivity: "assets/images/premium_banner/live_activity.png", -// PremiumFeature.timetableNotes: -// "assets/images/premium_banner/timetable_notes.png", -// PremiumFeature.iconChange: "assets/images/premium_banner/app_icon.png", -// PremiumFeature.fontChange: "assets/images/premium_banner/font.png", -// PremiumFeature.goalPlanner: "assets/images/premium_banner/goal_planner.png", - -// PremiumFeature.gradeRarities: -// "assets/images/premium_banner/grade_rarities.png", -// PremiumFeature.calendarSync: "assets/images/premium_banner/calendar_sync.png", -// }; - -const Map _featureTitles = { - // old shit - PremiumFeature.gradestats: "Találtál egy prémium funkciót.", - PremiumFeature.customcolors: "Több személyre szabás kell?", - PremiumFeature.profile: "Nem tetszik a neved?", - PremiumFeature.iconpack: "Jobban tetszettek a régi ikonok?", - PremiumFeature.subjectrename: - "Sokáig tart elolvasni, hogy \"Földrajz természettudomány\"?", - PremiumFeature.weeklytimetable: "Szeretnéd egyszerre az egész hetet látni?", - // PremiumFeature.goalplanner: "Kövesd a céljaidat, sok-sok statisztikával.", - PremiumFeature.widget: "Órák a kezdőképernyőd kényelméből.", - // new shit - PremiumFeature.moreAccounts: "u_title_1", - PremiumFeature.betaReleases: "u_title_2", - PremiumFeature.welcomeMessage: "u_title_3", - PremiumFeature.selfNotes: "u_title_4", - PremiumFeature.gradeCalculation: "u_title_5", - PremiumFeature.liveActivity: "u_title_6", - PremiumFeature.timetableNotes: "u_title_7", - PremiumFeature.iconChange: "u_title_8", - PremiumFeature.fontChange: "u_title_9", - PremiumFeature.goalPlanner: "u_title_10", - PremiumFeature.gradeRarities: "u_title_11", - PremiumFeature.calendarSync: "u_title_12", - PremiumFeature.gradeExporting: "u_title_13", -}; - -const Map _featureDescriptions = { - // old - PremiumFeature.gradestats: - "Támogass Kupak szinten, hogy több statisztikát láthass. ", - PremiumFeature.customcolors: - "Támogass Kupak szinten, és szabd személyre az elemek, a háttér, és a panelek színeit.", - PremiumFeature.profile: - "Kupak szinten változtathatod a nevedet, sőt, akár a profilképedet is.", - PremiumFeature.iconpack: - "Támogass Kupak szinten, hogy ikon témát választhass.", - PremiumFeature.subjectrename: - "Támogass Kupak szinten, hogy átnevezhesd Föcire.", - PremiumFeature.weeklytimetable: - "Támogass Tinta szinten a heti órarend funkcióért.", - // PremiumFeature.goalplanner: "A célkövetéshez támogass Tinta szinten.", - PremiumFeature.widget: - "Támogass Tinta szinten, és helyezz egy widgetet a kezdőképernyődre.", - // new - PremiumFeature.moreAccounts: "u_desc_1", - PremiumFeature.betaReleases: "u_desc_2", - PremiumFeature.welcomeMessage: "u_desc_3", - PremiumFeature.selfNotes: "u_desc_4", - PremiumFeature.gradeCalculation: "u_desc_5", - PremiumFeature.liveActivity: "u_desc_6", - PremiumFeature.timetableNotes: "u_desc_7", - PremiumFeature.iconChange: "u_desc_8", - PremiumFeature.fontChange: "u_desc_9", - PremiumFeature.goalPlanner: "u_desc_10", - PremiumFeature.gradeRarities: "u_desc_11", - PremiumFeature.calendarSync: "u_desc_12", - PremiumFeature.gradeExporting: "u_desc_13", -}; - -// class PremiumLockedFeatureUpsell extends StatelessWidget { -// const PremiumLockedFeatureUpsell({super.key, required this.feature}); - -// static void show( -// {required BuildContext context, required PremiumFeature feature}) => -// showRoundedModalBottomSheet(context, -// child: PremiumLockedFeatureUpsell(feature: feature)); - -// final PremiumFeature feature; - -// IconData _getIcon() => _featureLevels[feature] == PremiumFeatureLevel.cap -// ? FilcIcons.kupak -// : _featureLevels[feature] == PremiumFeatureLevel.ink -// ? FilcIcons.tinta -// : FilcIcons.tinta; -// Color _getColor(BuildContext context) => -// _featureLevels[feature] == PremiumFeatureLevel.gold -// ? const Color(0xFFC89B08) -// : Theme.of(context).brightness == Brightness.light -// ? const Color(0xff691A9B) -// : const Color(0xffA66FC8); -// String? _getAsset() => _featureAssets[feature]; -// String _getTitle() => _featureTitles[feature]!; -// String _getDescription() => _featureDescriptions[feature]!; - -// @override -// Widget build(BuildContext context) { -// final Color color = _getColor(context); - -// return Dialog( -// child: Padding( -// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0), -// child: Column( -// mainAxisSize: MainAxisSize.min, -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// // Title Bar -// Row( -// mainAxisAlignment: MainAxisAlignment.spaceBetween, -// children: [ -// Padding( -// padding: const EdgeInsets.only(left: 8.0), -// child: Icon(_getIcon()), -// ), -// IconButton( -// onPressed: () => Navigator.of(context).pop(), -// icon: const Icon(Icons.close), -// ), -// ], -// ), - -// // Image showcase -// if (_getAsset() != null) -// Padding( -// padding: const EdgeInsets.only(top: 8.0), -// child: Image.asset(_getAsset()!), -// ), - -// // Dialog title -// Padding( -// padding: const EdgeInsets.only(top: 12.0), -// child: Text( -// _getTitle(), -// style: const TextStyle( -// fontWeight: FontWeight.bold, -// fontSize: 20.0, -// ), -// ), -// ), - -// // Dialog description -// Padding( -// padding: const EdgeInsets.only(top: 8.0), -// child: Text( -// _getDescription(), -// style: const TextStyle( -// fontSize: 16.0, -// ), -// ), -// ), - -// // CTA button -// Padding( -// padding: const EdgeInsets.only(top: 8.0), -// child: SizedBox( -// width: double.infinity, -// child: TextButton( -// style: ButtonStyle( -// backgroundColor: -// WidgetStatePropertyAll(color.withOpacity(.25)), -// foregroundColor: WidgetStatePropertyAll(color), -// overlayColor: -// WidgetStatePropertyAll(color.withOpacity(.1))), -// onPressed: () { -// Navigator.of(context, rootNavigator: true) -// .push(MaterialPageRoute(builder: (context) { -// return const PlusScreen(); -// })); -// }, -// child: const Text( -// "Vigyél oda!", -// style: TextStyle( -// fontWeight: FontWeight.bold, -// fontSize: 18.0, -// ), -// ), -// ), -// ), -// ), -// ], -// ), -// ), -// ); -// } -// } - -class PlusLockedFeaturePopup extends StatelessWidget { - const PlusLockedFeaturePopup({super.key, required this.feature}); - - static void show({ - required BuildContext context, - required PremiumFeature feature, - }) => - showRoundedModalBottomSheet( - context, - child: PlusLockedFeaturePopup( - feature: feature, - ), - showHandle: false, - ); - - final PremiumFeature feature; - - PremiumFeatureLevel? _getFeatureLevel() => _featureLevels[feature]; - - // IconData _getIcon() => _featureLevels[feature] == PremiumFeatureLevel.cap - // ? FilcIcons.kupak - // : _featureLevels[feature] == PremiumFeatureLevel.ink - // ? FilcIcons.tinta - // : FilcIcons.tinta; - // Color _getColor(BuildContext context) => - // _featureLevels[feature] == PremiumFeatureLevel.gold - // ? const Color(0xFFC89B08) - // : Theme.of(context).brightness == Brightness.light - // ? const Color(0xff691A9B) - // : const Color(0xffA66FC8); - // String? _getAsset() => _featureAssets[feature]; - String _getTitle() => _featureTitles[feature]!.i18n; - String _getDescription() => _featureDescriptions[feature]!.i18n; - - @override - Widget build(BuildContext context) { - final bool isGold = _getFeatureLevel() == PremiumFeatureLevel.gold; - - return Container( - decoration: BoxDecoration( - color: isGold ? const Color(0xFFF7EDD9) : const Color(0xFFDCDAF7), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12.0), - ), - ), - child: Stack( - children: [ - SvgPicture.asset( - // "assets/svg/mesh_bg.svg", - "assets/svg/cover_arts/grid.svg", - // ignore: deprecated_member_use - color: isGold ? const Color(0xFFf0dcb6) : const Color(0xFFbcb8f0), - width: MediaQuery.of(context).size.width, - ), - SizedBox( - width: MediaQuery.of(context).size.width, - child: Padding( - padding: const EdgeInsets.all(18.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: isGold - ? const Color(0xFF341C01) - : const Color(0xFF130667), - borderRadius: BorderRadius.circular( - 2.0, - ), - ), - ), - const SizedBox( - height: 38.0, - ), - Image.asset( - 'assets/images/plus_${isGold ? 'gold' : 'standard'}.png', - width: 66, - height: 66, - ), - const SizedBox( - height: 55.0, - ), - Container( - width: double.infinity, - decoration: BoxDecoration( - color: const Color( - 0xFFF7F9FC, - ).withOpacity(0.7), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12.0), - bottom: Radius.circular(6.0), - ), - ), - padding: const EdgeInsets.all(14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isGold ? 'reFilc+ Gold' : 'reFilc+', - style: TextStyle( - color: isGold - ? const Color(0xFFAD7637) - : const Color(0xFF7463E2), - fontSize: 14.0, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox( - height: 12.0, - ), - Text( - _getTitle(), - style: TextStyle( - color: isGold - ? const Color(0xFF341C01) - : const Color(0xFF150D4E), - fontSize: 20.0, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox( - height: 8.0, - ), - Text( - _getDescription(), - style: TextStyle( - color: isGold - ? const Color(0xFF341C01) - : const Color(0xFF150D4E), - fontSize: 14.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - const SizedBox( - height: 6.0, - ), - Container( - width: double.infinity, - decoration: BoxDecoration( - color: const Color( - 0xFFF7F9FC, - ).withOpacity(0.7), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(6.0), - bottom: Radius.circular(12.0), - ), - ), - padding: const EdgeInsets.all(14.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'A reFilc+ 0.99 €, a reFilc+ Gold 2.99 €', - style: TextStyle( - color: isGold - ? const Color(0xFF341C01) - : const Color(0xFF150D4E), - fontSize: 14.0, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - const SizedBox( - height: 24.0, - ), - GestureDetector( - onTap: () { - Navigator.of(context, rootNavigator: true) - .push(MaterialPageRoute(builder: (context) { - return const PlusScreen(); - })); - }, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - 'assets/images/btn_plus_${isGold ? 'gold' : 'standard'}.png'), - ), - borderRadius: BorderRadius.circular(12.0), - ), - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'subscribe'.i18n, - style: TextStyle( - color: isGold - ? const Color(0xFF341C01) - : const Color(0xFF150D4E), - fontSize: 18.0, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} +import 'package:flutter_svg/svg.dart'; +import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart'; +import 'package:refilc_mobile_ui/plus/plus_screen.dart'; +import 'package:flutter/material.dart'; +import 'plus_things.i18n.dart'; + +enum PremiumFeature { + // old things + gradestats, + customcolors, + profile, + iconpack, + subjectrename, + weeklytimetable, + widget, + // new things + moreAccounts, // cap, (ink, sponge) + betaReleases, // cap + welcomeMessage, // cap + selfNotes, // cap + gradeCalculation, // ink + liveActivity, // ink + timetableNotes, // ink + iconChange, // sponge + fontChange, // sponge + goalPlanner, // sponge + gradeRarities, + calendarSync, + gradeExporting, // basic +} + +enum PremiumFeatureLevel { + old, + cap, + ink, + sponge, + // new new new + basic, + gold, +} + +const Map _featureLevels = { + // old things + PremiumFeature.gradestats: PremiumFeatureLevel.old, + PremiumFeature.customcolors: PremiumFeatureLevel.old, + PremiumFeature.profile: PremiumFeatureLevel.old, + PremiumFeature.iconpack: PremiumFeatureLevel.old, + PremiumFeature.subjectrename: PremiumFeatureLevel.old, + PremiumFeature.weeklytimetable: PremiumFeatureLevel.old, + PremiumFeature.widget: PremiumFeatureLevel.old, + // new things + PremiumFeature.moreAccounts: PremiumFeatureLevel.basic, + PremiumFeature.betaReleases: PremiumFeatureLevel.basic, + PremiumFeature.welcomeMessage: PremiumFeatureLevel.basic, + PremiumFeature.selfNotes: PremiumFeatureLevel.basic, + PremiumFeature.gradeCalculation: PremiumFeatureLevel.basic, + PremiumFeature.liveActivity: PremiumFeatureLevel.gold, + PremiumFeature.timetableNotes: PremiumFeatureLevel.gold, + PremiumFeature.iconChange: PremiumFeatureLevel.gold, + PremiumFeature.fontChange: PremiumFeatureLevel.gold, + PremiumFeature.goalPlanner: PremiumFeatureLevel.gold, + PremiumFeature.gradeRarities: PremiumFeatureLevel.basic, + PremiumFeature.calendarSync: PremiumFeatureLevel.gold, + PremiumFeature.gradeExporting: PremiumFeatureLevel.basic, +}; + +// const Map _featureAssets = { +// // old +// PremiumFeature.gradestats: "assets/images/premium_stats_showcase.png", +// PremiumFeature.customcolors: "assets/images/premium_theme_showcase.png", +// PremiumFeature.profile: "assets/images/premium_nickname_showcase.png", +// PremiumFeature.weeklytimetable: +// "assets/images/premium_timetable_showcase.png", +// // PremiumFeature.goalplanner: "assets/images/premium_goal_showcase.png", +// PremiumFeature.widget: "assets/images/premium_widget_showcase.png", +// // new +// PremiumFeature.moreAccounts: "assets/images/premium_banner/more_accounts.png", +// PremiumFeature.betaReleases: "assets/images/premium_banner/beta_releases.png", +// PremiumFeature.welcomeMessage: +// "assets/images/premium_banner/welcome_message.png", +// PremiumFeature.selfNotes: "assets/images/premium_banner/self_notes.png", +// PremiumFeature.gradeCalculation: +// "assets/images/premium_banner/grade_calc.png", +// PremiumFeature.liveActivity: "assets/images/premium_banner/live_activity.png", +// PremiumFeature.timetableNotes: +// "assets/images/premium_banner/timetable_notes.png", +// PremiumFeature.iconChange: "assets/images/premium_banner/app_icon.png", +// PremiumFeature.fontChange: "assets/images/premium_banner/font.png", +// PremiumFeature.goalPlanner: "assets/images/premium_banner/goal_planner.png", + +// PremiumFeature.gradeRarities: +// "assets/images/premium_banner/grade_rarities.png", +// PremiumFeature.calendarSync: "assets/images/premium_banner/calendar_sync.png", +// }; + +const Map _featureTitles = { + // old shit + PremiumFeature.gradestats: "Találtál egy prémium funkciót.", + PremiumFeature.customcolors: "Több személyre szabás kell?", + PremiumFeature.profile: "Nem tetszik a neved?", + PremiumFeature.iconpack: "Jobban tetszettek a régi ikonok?", + PremiumFeature.subjectrename: + "Sokáig tart elolvasni, hogy \"Földrajz természettudomány\"?", + PremiumFeature.weeklytimetable: "Szeretnéd egyszerre az egész hetet látni?", + // PremiumFeature.goalplanner: "Kövesd a céljaidat, sok-sok statisztikával.", + PremiumFeature.widget: "Órák a kezdőképernyőd kényelméből.", + // new shit + PremiumFeature.moreAccounts: "u_title_1", + PremiumFeature.betaReleases: "u_title_2", + PremiumFeature.welcomeMessage: "u_title_3", + PremiumFeature.selfNotes: "u_title_4", + PremiumFeature.gradeCalculation: "u_title_5", + PremiumFeature.liveActivity: "u_title_6", + PremiumFeature.timetableNotes: "u_title_7", + PremiumFeature.iconChange: "u_title_8", + PremiumFeature.fontChange: "u_title_9", + PremiumFeature.goalPlanner: "u_title_10", + PremiumFeature.gradeRarities: "u_title_11", + PremiumFeature.calendarSync: "u_title_12", + PremiumFeature.gradeExporting: "u_title_13", +}; + +const Map _featureDescriptions = { + // old + PremiumFeature.gradestats: + "Támogass Kupak szinten, hogy több statisztikát láthass. ", + PremiumFeature.customcolors: + "Támogass Kupak szinten, és szabd személyre az elemek, a háttér, és a panelek színeit.", + PremiumFeature.profile: + "Kupak szinten változtathatod a nevedet, sőt, akár a profilképedet is.", + PremiumFeature.iconpack: + "Támogass Kupak szinten, hogy ikon témát választhass.", + PremiumFeature.subjectrename: + "Támogass Kupak szinten, hogy átnevezhesd Föcire.", + PremiumFeature.weeklytimetable: + "Támogass Tinta szinten a heti órarend funkcióért.", + // PremiumFeature.goalplanner: "A célkövetéshez támogass Tinta szinten.", + PremiumFeature.widget: + "Támogass Tinta szinten, és helyezz egy widgetet a kezdőképernyődre.", + // new + PremiumFeature.moreAccounts: "u_desc_1", + PremiumFeature.betaReleases: "u_desc_2", + PremiumFeature.welcomeMessage: "u_desc_3", + PremiumFeature.selfNotes: "u_desc_4", + PremiumFeature.gradeCalculation: "u_desc_5", + PremiumFeature.liveActivity: "u_desc_6", + PremiumFeature.timetableNotes: "u_desc_7", + PremiumFeature.iconChange: "u_desc_8", + PremiumFeature.fontChange: "u_desc_9", + PremiumFeature.goalPlanner: "u_desc_10", + PremiumFeature.gradeRarities: "u_desc_11", + PremiumFeature.calendarSync: "u_desc_12", + PremiumFeature.gradeExporting: "u_desc_13", +}; + +// class PremiumLockedFeatureUpsell extends StatelessWidget { +// const PremiumLockedFeatureUpsell({super.key, required this.feature}); + +// static void show( +// {required BuildContext context, required PremiumFeature feature}) => +// showRoundedModalBottomSheet(context, +// child: PremiumLockedFeatureUpsell(feature: feature)); + +// final PremiumFeature feature; + +// IconData _getIcon() => _featureLevels[feature] == PremiumFeatureLevel.cap +// ? FilcIcons.kupak +// : _featureLevels[feature] == PremiumFeatureLevel.ink +// ? FilcIcons.tinta +// : FilcIcons.tinta; +// Color _getColor(BuildContext context) => +// _featureLevels[feature] == PremiumFeatureLevel.gold +// ? const Color(0xFFC89B08) +// : Theme.of(context).brightness == Brightness.light +// ? const Color(0xff691A9B) +// : const Color(0xffA66FC8); +// String? _getAsset() => _featureAssets[feature]; +// String _getTitle() => _featureTitles[feature]!; +// String _getDescription() => _featureDescriptions[feature]!; + +// @override +// Widget build(BuildContext context) { +// final Color color = _getColor(context); + +// return Dialog( +// child: Padding( +// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 16.0), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // Title Bar +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// Padding( +// padding: const EdgeInsets.only(left: 8.0), +// child: Icon(_getIcon()), +// ), +// IconButton( +// onPressed: () => Navigator.of(context).pop(), +// icon: const Icon(Icons.close), +// ), +// ], +// ), + +// // Image showcase +// if (_getAsset() != null) +// Padding( +// padding: const EdgeInsets.only(top: 8.0), +// child: Image.asset(_getAsset()!), +// ), + +// // Dialog title +// Padding( +// padding: const EdgeInsets.only(top: 12.0), +// child: Text( +// _getTitle(), +// style: const TextStyle( +// fontWeight: FontWeight.bold, +// fontSize: 20.0, +// ), +// ), +// ), + +// // Dialog description +// Padding( +// padding: const EdgeInsets.only(top: 8.0), +// child: Text( +// _getDescription(), +// style: const TextStyle( +// fontSize: 16.0, +// ), +// ), +// ), + +// // CTA button +// Padding( +// padding: const EdgeInsets.only(top: 8.0), +// child: SizedBox( +// width: double.infinity, +// child: TextButton( +// style: ButtonStyle( +// backgroundColor: +// WidgetStatePropertyAll(color.withOpacity(.25)), +// foregroundColor: WidgetStatePropertyAll(color), +// overlayColor: +// WidgetStatePropertyAll(color.withOpacity(.1))), +// onPressed: () { +// Navigator.of(context, rootNavigator: true) +// .push(MaterialPageRoute(builder: (context) { +// return const PlusScreen(); +// })); +// }, +// child: const Text( +// "Vigyél oda!", +// style: TextStyle( +// fontWeight: FontWeight.bold, +// fontSize: 18.0, +// ), +// ), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// } +// } + +class PlusLockedFeaturePopup extends StatelessWidget { + const PlusLockedFeaturePopup({super.key, required this.feature}); + + static void show({ + required BuildContext context, + required PremiumFeature feature, + }) => + showRoundedModalBottomSheet( + context, + child: PlusLockedFeaturePopup( + feature: feature, + ), + showHandle: false, + ); + + final PremiumFeature feature; + + PremiumFeatureLevel? _getFeatureLevel() => _featureLevels[feature]; + + // IconData _getIcon() => _featureLevels[feature] == PremiumFeatureLevel.cap + // ? FilcIcons.kupak + // : _featureLevels[feature] == PremiumFeatureLevel.ink + // ? FilcIcons.tinta + // : FilcIcons.tinta; + // Color _getColor(BuildContext context) => + // _featureLevels[feature] == PremiumFeatureLevel.gold + // ? const Color(0xFFC89B08) + // : Theme.of(context).brightness == Brightness.light + // ? const Color(0xff691A9B) + // : const Color(0xffA66FC8); + // String? _getAsset() => _featureAssets[feature]; + String _getTitle() => _featureTitles[feature]!.i18n; + String _getDescription() => _featureDescriptions[feature]!.i18n; + + @override + Widget build(BuildContext context) { + final bool isGold = _getFeatureLevel() == PremiumFeatureLevel.gold; + + return Container( + decoration: BoxDecoration( + color: isGold ? const Color(0xFFF7EDD9) : const Color(0xFFDCDAF7), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + ), + ), + child: Stack( + children: [ + SvgPicture.asset( + // "assets/svg/mesh_bg.svg", + "assets/svg/cover_arts/grid.svg", + // ignore: deprecated_member_use + color: isGold ? const Color(0xFFf0dcb6) : const Color(0xFFbcb8f0), + width: MediaQuery.of(context).size.width, + ), + SizedBox( + width: MediaQuery.of(context).size.width, + child: Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: isGold + ? const Color(0xFF341C01) + : const Color(0xFF130667), + borderRadius: BorderRadius.circular( + 2.0, + ), + ), + ), + const SizedBox( + height: 38.0, + ), + Image.asset( + 'assets/images/plus_${isGold ? 'gold' : 'standard'}.png', + width: 66, + height: 66, + ), + const SizedBox( + height: 55.0, + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color( + 0xFFF7F9FC, + ).withOpacity(0.7), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(6.0), + ), + ), + padding: const EdgeInsets.all(14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isGold ? 'reFilc+ Gold' : 'reFilc+', + style: TextStyle( + color: isGold + ? const Color(0xFFAD7637) + : const Color(0xFF7463E2), + fontSize: 14.0, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox( + height: 12.0, + ), + Text( + _getTitle(), + style: TextStyle( + color: isGold + ? const Color(0xFF341C01) + : const Color(0xFF150D4E), + fontSize: 20.0, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox( + height: 8.0, + ), + Text( + _getDescription(), + style: TextStyle( + color: isGold + ? const Color(0xFF341C01) + : const Color(0xFF150D4E), + fontSize: 14.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const SizedBox( + height: 6.0, + ), + Container( + width: double.infinity, + decoration: BoxDecoration( + color: const Color( + 0xFFF7F9FC, + ).withOpacity(0.7), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(6.0), + bottom: Radius.circular(12.0), + ), + ), + padding: const EdgeInsets.all(14.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'A reFilc+ 0.99 €, a reFilc+ Gold 2.99 €', + style: TextStyle( + color: isGold + ? const Color(0xFF341C01) + : const Color(0xFF150D4E), + fontSize: 14.0, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox( + height: 24.0, + ), + GestureDetector( + onTap: () { + Navigator.of(context, rootNavigator: true) + .push(MaterialPageRoute(builder: (context) { + return const PlusScreen(); + })); + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + 'assets/images/btn_plus_${isGold ? 'gold' : 'standard'}.png'), + ), + borderRadius: BorderRadius.circular(12.0), + ), + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'subscribe'.i18n, + style: TextStyle( + color: isGold + ? const Color(0xFF341C01) + : const Color(0xFF150D4E), + fontSize: 18.0, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/mobile/settings/app_icon_screen.dart b/lib/ui/mobile/settings/app_icon_screen.dart index a3f4fa5..925085e 100644 --- a/lib/ui/mobile/settings/app_icon_screen.dart +++ b/lib/ui/mobile/settings/app_icon_screen.dart @@ -1,242 +1,242 @@ -import 'package:refilc/models/settings.dart'; -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_mobile_ui/common/panel/panel.dart'; -// import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; -import 'package:refilc_plus/helpers/app_icon_helper.dart'; -// import 'package:refilc_plus/models/premium_scopes.dart'; -// import 'package:refilc_plus/providers/plus_provider.dart'; -// import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:provider/provider.dart'; -import 'app_icon_screen.i18n.dart'; - -class PremiumCustomAppIconMenu extends StatelessWidget { - const PremiumCustomAppIconMenu({super.key, required this.settings}); - - final SettingsProvider settings; - - @override - Widget build(BuildContext context) { - // return PanelButton( - // onPressed: () { - // if (!Provider.of(context, listen: false) - // .hasScope(PremiumScopes.changeAppIcon)) { - // PlusLockedFeaturePopup.show( - // context: context, feature: PremiumFeature.appiconchange); - // return; - // } - - // Navigator.of(context, rootNavigator: true).push( - // CupertinoPageRoute(builder: (context) => const ModifyAppIcon()), - // ); - // }, - // title: Text('custom_app_icon'.i18n), - // leading: const Icon(FeatherIcons.edit), - // ); - return const SizedBox( - width: 0, - height: 0, - ); - } -} - -class ModifyAppIcon extends StatefulWidget { - const ModifyAppIcon({super.key}); - - @override - State createState() => _ModifyAppIconState(); -} - -class _ModifyAppIconState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - - late SettingsProvider settings; - - @override - Widget build(BuildContext context) { - settings = Provider.of(context); - return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, - leading: BackButton(color: AppColors.of(context).text), - title: Text( - "app_icons".i18n, - style: TextStyle(color: AppColors.of(context).text), - ), - ), - body: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Panel( - title: Text("basic".i18n), - child: Column( - children: [ - AppIconItem( - iconName: 'refilc_default', - iconPath: 'assets/launch_icons/refilc_default.png', - displayName: 'reFilc Default', - description: 'Az alapértelmezett ikon.', - selected: settings.appIcon == 'refilc_default', - selectCallback: () async { - await AppIconHelper.setAppIcon('refilc_default'); - settings.update(appIcon: 'refilc_default'); - }, - ), - ], - ), - ), - // const SizedBox(height: 16.0), - // Panel( - // title: Text("seasonal".i18n), - // child: Column( - // children: [ - // // AppIconItem( - // // iconName: 'refilc_default', - // // iconPath: 'assets/launch_icons/refilc_default.png', - // // displayName: 'reFilc Default', - // // description: 'Az alapértelmezett ikon.', - // // selected: true, - // // selectCallback: () {}, - // // ), - // ], - // ), - // ), - const SizedBox(height: 16.0), - Panel( - title: Text("special".i18n), - child: Column( - children: [ - AppIconItem( - iconName: 'refilc_overcomplicated', - iconPath: - 'assets/launch_icons/refilc_overcomplicated.png', - displayName: 'Overcomplicated', - // description: 'Egy túlkomplikált ikon.', - selected: settings.appIcon == 'refilc_overcomplicated', - selectCallback: () async { - await AppIconHelper.setAppIcon( - 'refilc_overcomplicated'); - settings.update(appIcon: 'refilc_overcomplicated'); - }, - ), - AppIconItem( - iconName: 'refilc_concept', - iconPath: 'assets/launch_icons/refilc_concept.png', - displayName: 'Modern Concept', - // description: 'Egy modernebb, letisztultabb ikon.', - selected: settings.appIcon == 'refilc_concept', - selectCallback: () async { - await AppIconHelper.setAppIcon('refilc_concept'); - settings.update(appIcon: 'refilc_concept'); - }, - ), - ], - ), - ), - const SizedBox(height: 16.0), - Panel( - title: Text("other".i18n), - child: Column( - children: [ - AppIconItem( - iconName: 'refilc_pride', - iconPath: 'assets/launch_icons/refilc_pride.png', - displayName: 'Pride', - // description: '', - selected: settings.appIcon == 'refilc_pride', - selectCallback: () async { - await AppIconHelper.setAppIcon('refilc_pride'); - settings.update(appIcon: 'refilc_pride'); - }, - ), - ], - ), - ), - ], - ), - ), - )); - } -} - -class AppIconItem extends StatelessWidget { - const AppIconItem({ - super.key, - required this.iconName, - required this.iconPath, - required this.displayName, - this.description, - required this.selected, - required this.selectCallback, - }); - - final String iconName; - final String iconPath; - final String displayName; - final String? description; - final bool selected; - final void Function() selectCallback; - - @override - Widget build(BuildContext context) { - return ListTile( - minLeadingWidth: 32.0, - dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), - visualDensity: VisualDensity.compact, - onTap: () {}, - leading: Container( - height: 40, - width: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - image: DecorationImage( - image: AssetImage(iconPath), - fit: BoxFit.contain, - ), - ), - ), - title: InkWell( - onTap: selectCallback, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - displayName, - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16, - height: description == null ? 3.2 : 1.8, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (description != null) - Text( - description!, - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: AppColors.of(context).text.withOpacity(.75), - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - trailing: selected - ? Icon( - FeatherIcons.checkCircle, - color: Theme.of(context).colorScheme.primary, - ) - : const SizedBox(), - ); - } -} +import 'package:refilc/models/settings.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_mobile_ui/common/panel/panel.dart'; +// import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; +import 'package:refilc_plus/helpers/app_icon_helper.dart'; +// import 'package:refilc_plus/models/premium_scopes.dart'; +// import 'package:refilc_plus/providers/plus_provider.dart'; +// import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'app_icon_screen.i18n.dart'; + +class PremiumCustomAppIconMenu extends StatelessWidget { + const PremiumCustomAppIconMenu({super.key, required this.settings}); + + final SettingsProvider settings; + + @override + Widget build(BuildContext context) { + // return PanelButton( + // onPressed: () { + // if (!Provider.of(context, listen: false) + // .hasScope(PremiumScopes.changeAppIcon)) { + // PlusLockedFeaturePopup.show( + // context: context, feature: PremiumFeature.appiconchange); + // return; + // } + + // Navigator.of(context, rootNavigator: true).push( + // CupertinoPageRoute(builder: (context) => const ModifyAppIcon()), + // ); + // }, + // title: Text('custom_app_icon'.i18n), + // leading: const Icon(FeatherIcons.edit), + // ); + return const SizedBox( + width: 0, + height: 0, + ); + } +} + +class ModifyAppIcon extends StatefulWidget { + const ModifyAppIcon({super.key}); + + @override + State createState() => _ModifyAppIconState(); +} + +class _ModifyAppIconState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); + + late SettingsProvider settings; + + @override + Widget build(BuildContext context) { + settings = Provider.of(context); + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, + leading: BackButton(color: AppColors.of(context).text), + title: Text( + "app_icons".i18n, + style: TextStyle(color: AppColors.of(context).text), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Panel( + title: Text("basic".i18n), + child: Column( + children: [ + AppIconItem( + iconName: 'refilc_default', + iconPath: 'assets/launch_icons/refilc_default.png', + displayName: 'reFilc Default', + description: 'Az alapértelmezett ikon.', + selected: settings.appIcon == 'refilc_default', + selectCallback: () async { + await AppIconHelper.setAppIcon('refilc_default'); + settings.update(appIcon: 'refilc_default'); + }, + ), + ], + ), + ), + // const SizedBox(height: 16.0), + // Panel( + // title: Text("seasonal".i18n), + // child: Column( + // children: [ + // // AppIconItem( + // // iconName: 'refilc_default', + // // iconPath: 'assets/launch_icons/refilc_default.png', + // // displayName: 'reFilc Default', + // // description: 'Az alapértelmezett ikon.', + // // selected: true, + // // selectCallback: () {}, + // // ), + // ], + // ), + // ), + const SizedBox(height: 16.0), + Panel( + title: Text("special".i18n), + child: Column( + children: [ + AppIconItem( + iconName: 'refilc_overcomplicated', + iconPath: + 'assets/launch_icons/refilc_overcomplicated.png', + displayName: 'Overcomplicated', + // description: 'Egy túlkomplikált ikon.', + selected: settings.appIcon == 'refilc_overcomplicated', + selectCallback: () async { + await AppIconHelper.setAppIcon( + 'refilc_overcomplicated'); + settings.update(appIcon: 'refilc_overcomplicated'); + }, + ), + AppIconItem( + iconName: 'refilc_concept', + iconPath: 'assets/launch_icons/refilc_concept.png', + displayName: 'Modern Concept', + // description: 'Egy modernebb, letisztultabb ikon.', + selected: settings.appIcon == 'refilc_concept', + selectCallback: () async { + await AppIconHelper.setAppIcon('refilc_concept'); + settings.update(appIcon: 'refilc_concept'); + }, + ), + ], + ), + ), + const SizedBox(height: 16.0), + Panel( + title: Text("other".i18n), + child: Column( + children: [ + AppIconItem( + iconName: 'refilc_pride', + iconPath: 'assets/launch_icons/refilc_pride.png', + displayName: 'Pride', + // description: '', + selected: settings.appIcon == 'refilc_pride', + selectCallback: () async { + await AppIconHelper.setAppIcon('refilc_pride'); + settings.update(appIcon: 'refilc_pride'); + }, + ), + ], + ), + ), + ], + ), + ), + )); + } +} + +class AppIconItem extends StatelessWidget { + const AppIconItem({ + super.key, + required this.iconName, + required this.iconPath, + required this.displayName, + this.description, + required this.selected, + required this.selectCallback, + }); + + final String iconName; + final String iconPath; + final String displayName; + final String? description; + final bool selected; + final void Function() selectCallback; + + @override + Widget build(BuildContext context) { + return ListTile( + minLeadingWidth: 32.0, + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), + visualDensity: VisualDensity.compact, + onTap: () {}, + leading: Container( + height: 40, + width: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: AssetImage(iconPath), + fit: BoxFit.contain, + ), + ), + ), + title: InkWell( + onTap: selectCallback, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + displayName, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16, + height: description == null ? 3.2 : 1.8, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (description != null) + Text( + description!, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14, + color: AppColors.of(context).text.withOpacity(.75), + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + trailing: selected + ? Icon( + FeatherIcons.checkCircle, + color: Theme.of(context).colorScheme.primary, + ) + : const SizedBox(), + ); + } +} diff --git a/lib/ui/mobile/settings/app_icon_screen.i18n.dart b/lib/ui/mobile/settings/app_icon_screen.i18n.dart index e12b283..9a8d7db 100644 --- a/lib/ui/mobile/settings/app_icon_screen.i18n.dart +++ b/lib/ui/mobile/settings/app_icon_screen.i18n.dart @@ -1,36 +1,36 @@ -import 'package:i18n_extension/i18n_extension.dart'; - -extension Localization on String { - static final _t = Translations.byLocale("hu_hu") + - { - "en_en": { - "custom_app_icon": "Custom App Icon", - "app_icons": "App Icons", - "basic": "Basic", - "seasonal": "Seasonal", - "special": "Special", - "other": "Other", - }, - "hu_hu": { - "custom_app_icon": "Alkalmazásikon", - "app_icons": "Alkalmazásikonok", - "basic": "Egyszerű", - "seasonal": "Szezonális", - "special": "Különleges", - "other": "Egyéb", - }, - "de_de": { - "custom_app_icon": "App-Symbol", - "app_icons": "App-Symbole", - "basic": "Basic", - "seasonal": "Saisonal", - "special": "Besonders", - "other": "Andere", - }, - }; - - String get i18n => localize(this, _t); - String fill(List params) => localizeFill(this, params); - String plural(int value) => localizePlural(value, this, _t); - String version(Object modifier) => localizeVersion(modifier, this, _t); -} +import 'package:i18n_extension/i18n_extension.dart'; + +extension Localization on String { + static final _t = Translations.byLocale("hu_hu") + + { + "en_en": { + "custom_app_icon": "Custom App Icon", + "app_icons": "App Icons", + "basic": "Basic", + "seasonal": "Seasonal", + "special": "Special", + "other": "Other", + }, + "hu_hu": { + "custom_app_icon": "Alkalmazásikon", + "app_icons": "Alkalmazásikonok", + "basic": "Egyszerű", + "seasonal": "Szezonális", + "special": "Különleges", + "other": "Egyéb", + }, + "de_de": { + "custom_app_icon": "App-Symbol", + "app_icons": "App-Symbole", + "basic": "Basic", + "seasonal": "Saisonal", + "special": "Besonders", + "other": "Andere", + }, + }; + + String get i18n => localize(this, _t); + String fill(List params) => localizeFill(this, params); + String plural(int value) => localizePlural(value, this, _t); + String version(Object modifier) => localizeVersion(modifier, this, _t); +} diff --git a/lib/ui/mobile/settings/modify_teacher_names.dart b/lib/ui/mobile/settings/modify_teacher_names.dart index 48daf08..feacbf7 100644 --- a/lib/ui/mobile/settings/modify_teacher_names.dart +++ b/lib/ui/mobile/settings/modify_teacher_names.dart @@ -1,452 +1,452 @@ -// import 'package:dropdown_button2/dropdown_button2.dart'; -// import 'package:refilc/api/providers/database_provider.dart'; -// import 'package:refilc/api/providers/user_provider.dart'; -// import 'package:refilc/models/settings.dart'; -// import 'package:refilc/theme/colors/colors.dart'; -// import 'package:refilc/utils/format.dart'; -// import 'package:refilc_kreta_api/models/teacher.dart'; -// import 'package:refilc_kreta_api/providers/absence_provider.dart'; -// import 'package:refilc_kreta_api/providers/grade_provider.dart'; -// import 'package:refilc_kreta_api/providers/timetable_provider.dart'; -// import 'package:refilc_mobile_ui/common/panel/panel.dart'; -// import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; -// // import 'package:refilc_plus/models/premium_scopes.dart'; -// // import 'package:refilc_plus/providers/plus_provider.dart'; -// // import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; -// import 'package:flutter/cupertino.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -// import 'package:provider/provider.dart'; -// import 'package:refilc_mobile_ui/screens/settings/modify_names.i18n.dart'; - -// class MenuRenamedTeachers extends StatelessWidget { -// const MenuRenamedTeachers({Key? key, required this.settings}) -// : super(key: key); - -// final SettingsProvider settings; - -// @override -// Widget build(BuildContext context) { -// return PanelButton( -// padding: const EdgeInsets.only(left: 14.0), -// onPressed: () { -// // if (!Provider.of(context, listen: false) -// // .hasScope(PremiumScopes.renameTeachers)) { -// // PlusLockedFeaturePopup.show( -// // context: context, feature: PremiumFeature.teacherrename); -// // return; -// // } - -// Navigator.of(context, rootNavigator: true).push( -// CupertinoPageRoute(builder: (context) => const ModifyTeacherNames()), -// ); -// }, -// title: Text( -// "rename_teachers".i18n, -// style: TextStyle( -// color: AppColors.of(context) -// .text -// .withOpacity(settings.renamedTeachersEnabled ? 1.0 : .5)), -// ), -// leading: settings.renamedTeachersEnabled -// ? const Icon(FeatherIcons.users) -// : Icon(FeatherIcons.users, -// color: AppColors.of(context).text.withOpacity(.25)), -// trailingDivider: true, -// trailing: Switch( -// onChanged: (v) async { -// // if (!Provider.of(context, listen: false) -// // .hasScope(PremiumScopes.renameTeachers)) { -// // PlusLockedFeaturePopup.show( -// // context: context, feature: PremiumFeature.teacherrename); -// // return; -// // } - -// settings.update(renamedTeachersEnabled: v); -// await Provider.of(context, listen: false) -// .convertBySettings(); -// await Provider.of(context, listen: false) -// .convertBySettings(); -// await Provider.of(context, listen: false) -// .convertBySettings(); -// }, -// value: settings.renamedTeachersEnabled, -// activeColor: Theme.of(context).colorScheme.secondary, -// ), -// ); -// } -// } - -// class ModifyTeacherNames extends StatefulWidget { -// const ModifyTeacherNames({Key? key}) : super(key: key); - -// @override -// State createState() => _ModifyTeacherNamesState(); -// } - -// class _ModifyTeacherNamesState extends State { -// final GlobalKey _scaffoldKey = GlobalKey(); -// final _teacherName = TextEditingController(); -// String? selectedTeacherId; - -// late List teachers; -// late UserProvider user; -// late DatabaseProvider dbProvider; -// late SettingsProvider settings; - -// @override -// void initState() { -// super.initState(); -// teachers = (Provider.of(context, listen: false) -// .grades -// .map((e) => e.teacher) -// .toSet() -// .toList() -// ..sort((a, b) => a.name.compareTo(b.name))); -// user = Provider.of(context, listen: false); -// dbProvider = Provider.of(context, listen: false); -// } - -// Future> fetchRenamedTeachers() async { -// return await dbProvider.userQuery.renamedTeachers(userId: user.id!); -// } - -// void showRenameDialog() { -// showDialog( -// context: context, -// builder: (context) => StatefulBuilder(builder: (context, setS) { -// return AlertDialog( -// shape: const RoundedRectangleBorder( -// borderRadius: BorderRadius.all(Radius.circular(14.0))), -// title: Text("rename_teacher".i18n), -// content: Column( -// mainAxisSize: MainAxisSize.min, -// children: [ -// DropdownButton2( -// items: teachers -// .map((item) => DropdownMenuItem( -// value: item.id, -// child: Text( -// item.name, -// style: TextStyle( -// fontSize: 14, -// fontWeight: FontWeight.bold, -// color: AppColors.of(context).text, -// ), -// overflow: TextOverflow.ellipsis, -// ), -// )) -// .toList(), -// onChanged: (String? v) async { -// final renamedSubs = await fetchRenamedTeachers(); - -// setS(() { -// selectedTeacherId = v; - -// if (renamedSubs.containsKey(selectedTeacherId)) { -// _teacherName.text = renamedSubs[selectedTeacherId]!; -// } else { -// _teacherName.text = ""; -// } -// }); -// }, -// iconStyleData: IconStyleData( -// iconSize: 14, -// iconEnabledColor: AppColors.of(context).text, -// iconDisabledColor: AppColors.of(context).text, -// ), -// underline: const SizedBox(), -// menuItemStyleData: MenuItemStyleData(height: 40,), -// itemHeight: 40, -// itemPadding: const EdgeInsets.only(left: 14, right: 14), -// buttonWidth: 50, -// dropdownWidth: 300, -// dropdownPadding: null, -// buttonDecoration: BoxDecoration( -// borderRadius: BorderRadius.circular(8), -// ), -// dropdownDecoration: BoxDecoration( -// borderRadius: BorderRadius.circular(14), -// ), -// dropdownElevation: 8, -// scrollbarRadius: const Radius.circular(40), -// scrollbarThickness: 6, -// scrollbarAlwaysShow: true, -// offset: const Offset(-10, -10), -// buttonSplashColor: Colors.transparent, -// customButton: Container( -// width: double.infinity, -// decoration: BoxDecoration( -// border: Border.all(color: Colors.grey, width: 2), -// borderRadius: BorderRadius.circular(12.0), -// ), -// padding: const EdgeInsets.symmetric( -// vertical: 12.0, horizontal: 8.0), -// child: Text( -// selectedTeacherId == null -// ? "select_teacher".i18n -// : teachers -// .firstWhere( -// (element) => element.id == selectedTeacherId, -// orElse: () => Teacher( -// id: 'noid', name: "select_teacher".i18n), -// ) -// .name, -// style: Theme.of(context).textTheme.titleSmall!.copyWith( -// fontWeight: FontWeight.w700, -// color: AppColors.of(context).text.withOpacity(0.75)), -// overflow: TextOverflow.ellipsis, -// maxLines: 2, -// textAlign: TextAlign.center, -// ), -// ), -// ), -// const Padding( -// padding: EdgeInsets.symmetric(vertical: 8.0), -// child: Icon(FeatherIcons.arrowDown, size: 32), -// ), -// TextField( -// controller: _teacherName, -// decoration: InputDecoration( -// border: OutlineInputBorder( -// borderSide: -// const BorderSide(color: Colors.grey, width: 1.5), -// borderRadius: BorderRadius.circular(12.0), -// ), -// focusedBorder: OutlineInputBorder( -// borderSide: -// const BorderSide(color: Colors.grey, width: 1.5), -// borderRadius: BorderRadius.circular(12.0), -// ), -// contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), -// hintText: "modified_name".i18n, -// suffixIcon: IconButton( -// icon: const Icon( -// FeatherIcons.x, -// color: Colors.grey, -// ), -// onPressed: () { -// setState(() { -// _teacherName.text = ""; -// }); -// }, -// ), -// ), -// ), -// ], -// ), -// actions: [ -// TextButton( -// child: Text( -// "cancel".i18n, -// style: const TextStyle(fontWeight: FontWeight.w500), -// ), -// onPressed: () { -// Navigator.of(context).maybePop(); -// }, -// ), -// TextButton( -// child: Text( -// "done".i18n, -// style: const TextStyle(fontWeight: FontWeight.w500), -// ), -// onPressed: () async { -// if (selectedTeacherId != null) { -// final renamedSubs = await fetchRenamedTeachers(); - -// renamedSubs[selectedTeacherId!] = _teacherName.text; -// await dbProvider.userStore -// .storeRenamedTeachers(renamedSubs, userId: user.id!); -// await Provider.of(context, listen: false) -// .convertBySettings(); -// await Provider.of(context, listen: false) -// .convertBySettings(); -// await Provider.of(context, listen: false) -// .convertBySettings(); -// } -// Navigator.of(context).pop(true); -// setState(() {}); -// }, -// ), -// ], -// ); -// }), -// ).then((val) { -// _teacherName.text = ""; -// selectedTeacherId = null; -// }); -// } - -// @override -// Widget build(BuildContext context) { -// settings = Provider.of(context); -// return Scaffold( -// key: _scaffoldKey, -// appBar: AppBar( -// surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, -// leading: BackButton(color: AppColors.of(context).text), -// title: Text( -// "modify_teachers".i18n, -// style: TextStyle(color: AppColors.of(context).text), -// ), -// ), -// body: Padding( -// padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), -// child: SingleChildScrollView( -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// // Panel( -// // child: SwitchListTile( - -// // title: Text("italics_toggle".i18n), -// // onChanged: (value) => -// // settings.update(renamedTeachersItalics: value), -// // value: settings.renamedTeachersItalics, -// // ), -// // ), -// // const SizedBox( -// // height: 20, -// // ), -// InkWell( -// onTap: showRenameDialog, -// borderRadius: BorderRadius.circular(12.0), -// child: Container( -// width: double.infinity, -// decoration: BoxDecoration( -// border: Border.all(color: Colors.grey, width: 2), -// borderRadius: BorderRadius.circular(12.0), -// ), -// padding: const EdgeInsets.symmetric( -// vertical: 18.0, horizontal: 12.0), -// child: Center( -// child: Text( -// "rename_new_teacher".i18n, -// style: TextStyle( -// fontWeight: FontWeight.w600, -// fontSize: 18, -// color: AppColors.of(context).text.withOpacity(.85), -// ), -// ), -// ), -// ), -// ), -// const SizedBox( -// height: 30, -// ), -// FutureBuilder>( -// future: fetchRenamedTeachers(), -// builder: (context, snapshot) { -// if (!snapshot.hasData || snapshot.data!.isEmpty) { -// return Container(); -// } - -// return Panel( -// title: Text("renamed_teachers".i18n), -// child: Column( -// children: snapshot.data!.keys.map( -// (key) { -// Teacher? teacher = teachers.firstWhere( -// (element) => key == element.id, -// orElse: () => Teacher(id: 'noid', name: 'noname'), -// ); - -// if (teacher.id == 'noid') { -// return const SizedBox( -// width: 0, -// height: 0, -// ); -// } - -// String renameTo = snapshot.data![key]!; -// return RenamedTeacherItem( -// teacher: teacher, -// renamedTo: renameTo, -// modifyCallback: () { -// setState(() { -// selectedTeacherId = teacher.id; -// _teacherName.text = renameTo; -// }); -// showRenameDialog(); -// }, -// removeCallback: () { -// setState(() { -// Map subs = -// Map.from(snapshot.data!); -// subs.remove(key); -// dbProvider.userStore.storeRenamedTeachers( -// subs, -// userId: user.id!); -// }); -// }, -// ); -// }, -// ).toList(), -// ), -// ); -// }, -// ), -// ], -// ), -// ), -// )); -// } -// } - -// class RenamedTeacherItem extends StatelessWidget { -// const RenamedTeacherItem({ -// Key? key, -// required this.teacher, -// required this.renamedTo, -// required this.modifyCallback, -// required this.removeCallback, -// }) : super(key: key); - -// final Teacher teacher; -// final String renamedTo; -// final void Function() modifyCallback; -// final void Function() removeCallback; - -// @override -// Widget build(BuildContext context) { -// return ListTile( -// minLeadingWidth: 32.0, -// dense: true, -// contentPadding: -// const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), -// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), -// visualDensity: VisualDensity.compact, -// onTap: () {}, -// leading: Icon(FeatherIcons.user, -// color: AppColors.of(context).text.withOpacity(.75)), -// title: InkWell( -// onTap: modifyCallback, -// child: Column( -// crossAxisAlignment: CrossAxisAlignment.start, -// children: [ -// Text( -// teacher.name.capital(), -// style: TextStyle( -// fontWeight: FontWeight.w500, -// fontSize: 14, -// color: AppColors.of(context).text.withOpacity(.75)), -// maxLines: 1, -// overflow: TextOverflow.ellipsis, -// ), -// Text( -// renamedTo, -// style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16), -// maxLines: 2, -// overflow: TextOverflow.ellipsis, -// ), -// ], -// ), -// ), -// trailing: InkWell( -// onTap: removeCallback, -// child: Icon(FeatherIcons.trash, -// color: AppColors.of(context).red.withOpacity(.75)), -// ), -// ); -// } -// } +// import 'package:dropdown_button2/dropdown_button2.dart'; +// import 'package:refilc/api/providers/database_provider.dart'; +// import 'package:refilc/api/providers/user_provider.dart'; +// import 'package:refilc/models/settings.dart'; +// import 'package:refilc/theme/colors/colors.dart'; +// import 'package:refilc/utils/format.dart'; +// import 'package:refilc_kreta_api/models/teacher.dart'; +// import 'package:refilc_kreta_api/providers/absence_provider.dart'; +// import 'package:refilc_kreta_api/providers/grade_provider.dart'; +// import 'package:refilc_kreta_api/providers/timetable_provider.dart'; +// import 'package:refilc_mobile_ui/common/panel/panel.dart'; +// import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; +// // import 'package:refilc_plus/models/premium_scopes.dart'; +// // import 'package:refilc_plus/providers/plus_provider.dart'; +// // import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; +// import 'package:flutter/cupertino.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +// import 'package:provider/provider.dart'; +// import 'package:refilc_mobile_ui/screens/settings/modify_names.i18n.dart'; + +// class MenuRenamedTeachers extends StatelessWidget { +// const MenuRenamedTeachers({Key? key, required this.settings}) +// : super(key: key); + +// final SettingsProvider settings; + +// @override +// Widget build(BuildContext context) { +// return PanelButton( +// padding: const EdgeInsets.only(left: 14.0), +// onPressed: () { +// // if (!Provider.of(context, listen: false) +// // .hasScope(PremiumScopes.renameTeachers)) { +// // PlusLockedFeaturePopup.show( +// // context: context, feature: PremiumFeature.teacherrename); +// // return; +// // } + +// Navigator.of(context, rootNavigator: true).push( +// CupertinoPageRoute(builder: (context) => const ModifyTeacherNames()), +// ); +// }, +// title: Text( +// "rename_teachers".i18n, +// style: TextStyle( +// color: AppColors.of(context) +// .text +// .withOpacity(settings.renamedTeachersEnabled ? 1.0 : .5)), +// ), +// leading: settings.renamedTeachersEnabled +// ? const Icon(FeatherIcons.users) +// : Icon(FeatherIcons.users, +// color: AppColors.of(context).text.withOpacity(.25)), +// trailingDivider: true, +// trailing: Switch( +// onChanged: (v) async { +// // if (!Provider.of(context, listen: false) +// // .hasScope(PremiumScopes.renameTeachers)) { +// // PlusLockedFeaturePopup.show( +// // context: context, feature: PremiumFeature.teacherrename); +// // return; +// // } + +// settings.update(renamedTeachersEnabled: v); +// await Provider.of(context, listen: false) +// .convertBySettings(); +// await Provider.of(context, listen: false) +// .convertBySettings(); +// await Provider.of(context, listen: false) +// .convertBySettings(); +// }, +// value: settings.renamedTeachersEnabled, +// activeColor: Theme.of(context).colorScheme.secondary, +// ), +// ); +// } +// } + +// class ModifyTeacherNames extends StatefulWidget { +// const ModifyTeacherNames({Key? key}) : super(key: key); + +// @override +// State createState() => _ModifyTeacherNamesState(); +// } + +// class _ModifyTeacherNamesState extends State { +// final GlobalKey _scaffoldKey = GlobalKey(); +// final _teacherName = TextEditingController(); +// String? selectedTeacherId; + +// late List teachers; +// late UserProvider user; +// late DatabaseProvider dbProvider; +// late SettingsProvider settings; + +// @override +// void initState() { +// super.initState(); +// teachers = (Provider.of(context, listen: false) +// .grades +// .map((e) => e.teacher) +// .toSet() +// .toList() +// ..sort((a, b) => a.name.compareTo(b.name))); +// user = Provider.of(context, listen: false); +// dbProvider = Provider.of(context, listen: false); +// } + +// Future> fetchRenamedTeachers() async { +// return await dbProvider.userQuery.renamedTeachers(userId: user.id!); +// } + +// void showRenameDialog() { +// showDialog( +// context: context, +// builder: (context) => StatefulBuilder(builder: (context, setS) { +// return AlertDialog( +// shape: const RoundedRectangleBorder( +// borderRadius: BorderRadius.all(Radius.circular(14.0))), +// title: Text("rename_teacher".i18n), +// content: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// DropdownButton2( +// items: teachers +// .map((item) => DropdownMenuItem( +// value: item.id, +// child: Text( +// item.name, +// style: TextStyle( +// fontSize: 14, +// fontWeight: FontWeight.bold, +// color: AppColors.of(context).text, +// ), +// overflow: TextOverflow.ellipsis, +// ), +// )) +// .toList(), +// onChanged: (String? v) async { +// final renamedSubs = await fetchRenamedTeachers(); + +// setS(() { +// selectedTeacherId = v; + +// if (renamedSubs.containsKey(selectedTeacherId)) { +// _teacherName.text = renamedSubs[selectedTeacherId]!; +// } else { +// _teacherName.text = ""; +// } +// }); +// }, +// iconStyleData: IconStyleData( +// iconSize: 14, +// iconEnabledColor: AppColors.of(context).text, +// iconDisabledColor: AppColors.of(context).text, +// ), +// underline: const SizedBox(), +// menuItemStyleData: MenuItemStyleData(height: 40,), +// itemHeight: 40, +// itemPadding: const EdgeInsets.only(left: 14, right: 14), +// buttonWidth: 50, +// dropdownWidth: 300, +// dropdownPadding: null, +// buttonDecoration: BoxDecoration( +// borderRadius: BorderRadius.circular(8), +// ), +// dropdownDecoration: BoxDecoration( +// borderRadius: BorderRadius.circular(14), +// ), +// dropdownElevation: 8, +// scrollbarRadius: const Radius.circular(40), +// scrollbarThickness: 6, +// scrollbarAlwaysShow: true, +// offset: const Offset(-10, -10), +// buttonSplashColor: Colors.transparent, +// customButton: Container( +// width: double.infinity, +// decoration: BoxDecoration( +// border: Border.all(color: Colors.grey, width: 2), +// borderRadius: BorderRadius.circular(12.0), +// ), +// padding: const EdgeInsets.symmetric( +// vertical: 12.0, horizontal: 8.0), +// child: Text( +// selectedTeacherId == null +// ? "select_teacher".i18n +// : teachers +// .firstWhere( +// (element) => element.id == selectedTeacherId, +// orElse: () => Teacher( +// id: 'noid', name: "select_teacher".i18n), +// ) +// .name, +// style: Theme.of(context).textTheme.titleSmall!.copyWith( +// fontWeight: FontWeight.w700, +// color: AppColors.of(context).text.withOpacity(0.75)), +// overflow: TextOverflow.ellipsis, +// maxLines: 2, +// textAlign: TextAlign.center, +// ), +// ), +// ), +// const Padding( +// padding: EdgeInsets.symmetric(vertical: 8.0), +// child: Icon(FeatherIcons.arrowDown, size: 32), +// ), +// TextField( +// controller: _teacherName, +// decoration: InputDecoration( +// border: OutlineInputBorder( +// borderSide: +// const BorderSide(color: Colors.grey, width: 1.5), +// borderRadius: BorderRadius.circular(12.0), +// ), +// focusedBorder: OutlineInputBorder( +// borderSide: +// const BorderSide(color: Colors.grey, width: 1.5), +// borderRadius: BorderRadius.circular(12.0), +// ), +// contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), +// hintText: "modified_name".i18n, +// suffixIcon: IconButton( +// icon: const Icon( +// FeatherIcons.x, +// color: Colors.grey, +// ), +// onPressed: () { +// setState(() { +// _teacherName.text = ""; +// }); +// }, +// ), +// ), +// ), +// ], +// ), +// actions: [ +// TextButton( +// child: Text( +// "cancel".i18n, +// style: const TextStyle(fontWeight: FontWeight.w500), +// ), +// onPressed: () { +// Navigator.of(context).maybePop(); +// }, +// ), +// TextButton( +// child: Text( +// "done".i18n, +// style: const TextStyle(fontWeight: FontWeight.w500), +// ), +// onPressed: () async { +// if (selectedTeacherId != null) { +// final renamedSubs = await fetchRenamedTeachers(); + +// renamedSubs[selectedTeacherId!] = _teacherName.text; +// await dbProvider.userStore +// .storeRenamedTeachers(renamedSubs, userId: user.id!); +// await Provider.of(context, listen: false) +// .convertBySettings(); +// await Provider.of(context, listen: false) +// .convertBySettings(); +// await Provider.of(context, listen: false) +// .convertBySettings(); +// } +// Navigator.of(context).pop(true); +// setState(() {}); +// }, +// ), +// ], +// ); +// }), +// ).then((val) { +// _teacherName.text = ""; +// selectedTeacherId = null; +// }); +// } + +// @override +// Widget build(BuildContext context) { +// settings = Provider.of(context); +// return Scaffold( +// key: _scaffoldKey, +// appBar: AppBar( +// surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, +// leading: BackButton(color: AppColors.of(context).text), +// title: Text( +// "modify_teachers".i18n, +// style: TextStyle(color: AppColors.of(context).text), +// ), +// ), +// body: Padding( +// padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), +// child: SingleChildScrollView( +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // Panel( +// // child: SwitchListTile( + +// // title: Text("italics_toggle".i18n), +// // onChanged: (value) => +// // settings.update(renamedTeachersItalics: value), +// // value: settings.renamedTeachersItalics, +// // ), +// // ), +// // const SizedBox( +// // height: 20, +// // ), +// InkWell( +// onTap: showRenameDialog, +// borderRadius: BorderRadius.circular(12.0), +// child: Container( +// width: double.infinity, +// decoration: BoxDecoration( +// border: Border.all(color: Colors.grey, width: 2), +// borderRadius: BorderRadius.circular(12.0), +// ), +// padding: const EdgeInsets.symmetric( +// vertical: 18.0, horizontal: 12.0), +// child: Center( +// child: Text( +// "rename_new_teacher".i18n, +// style: TextStyle( +// fontWeight: FontWeight.w600, +// fontSize: 18, +// color: AppColors.of(context).text.withOpacity(.85), +// ), +// ), +// ), +// ), +// ), +// const SizedBox( +// height: 30, +// ), +// FutureBuilder>( +// future: fetchRenamedTeachers(), +// builder: (context, snapshot) { +// if (!snapshot.hasData || snapshot.data!.isEmpty) { +// return Container(); +// } + +// return Panel( +// title: Text("renamed_teachers".i18n), +// child: Column( +// children: snapshot.data!.keys.map( +// (key) { +// Teacher? teacher = teachers.firstWhere( +// (element) => key == element.id, +// orElse: () => Teacher(id: 'noid', name: 'noname'), +// ); + +// if (teacher.id == 'noid') { +// return const SizedBox( +// width: 0, +// height: 0, +// ); +// } + +// String renameTo = snapshot.data![key]!; +// return RenamedTeacherItem( +// teacher: teacher, +// renamedTo: renameTo, +// modifyCallback: () { +// setState(() { +// selectedTeacherId = teacher.id; +// _teacherName.text = renameTo; +// }); +// showRenameDialog(); +// }, +// removeCallback: () { +// setState(() { +// Map subs = +// Map.from(snapshot.data!); +// subs.remove(key); +// dbProvider.userStore.storeRenamedTeachers( +// subs, +// userId: user.id!); +// }); +// }, +// ); +// }, +// ).toList(), +// ), +// ); +// }, +// ), +// ], +// ), +// ), +// )); +// } +// } + +// class RenamedTeacherItem extends StatelessWidget { +// const RenamedTeacherItem({ +// Key? key, +// required this.teacher, +// required this.renamedTo, +// required this.modifyCallback, +// required this.removeCallback, +// }) : super(key: key); + +// final Teacher teacher; +// final String renamedTo; +// final void Function() modifyCallback; +// final void Function() removeCallback; + +// @override +// Widget build(BuildContext context) { +// return ListTile( +// minLeadingWidth: 32.0, +// dense: true, +// contentPadding: +// const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), +// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)), +// visualDensity: VisualDensity.compact, +// onTap: () {}, +// leading: Icon(FeatherIcons.user, +// color: AppColors.of(context).text.withOpacity(.75)), +// title: InkWell( +// onTap: modifyCallback, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// teacher.name.capital(), +// style: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 14, +// color: AppColors.of(context).text.withOpacity(.75)), +// maxLines: 1, +// overflow: TextOverflow.ellipsis, +// ), +// Text( +// renamedTo, +// style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 16), +// maxLines: 2, +// overflow: TextOverflow.ellipsis, +// ), +// ], +// ), +// ), +// trailing: InkWell( +// onTap: removeCallback, +// child: Icon(FeatherIcons.trash, +// color: AppColors.of(context).red.withOpacity(.75)), +// ), +// ); +// } +// } diff --git a/lib/ui/mobile/settings/settings_helper.dart b/lib/ui/mobile/settings/settings_helper.dart index efad545..9375638 100644 --- a/lib/ui/mobile/settings/settings_helper.dart +++ b/lib/ui/mobile/settings/settings_helper.dart @@ -1,143 +1,143 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:provider/provider.dart'; -import 'package:refilc/api/providers/database_provider.dart'; -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:refilc/ui/widgets/grade/grade_tile.dart'; -import 'package:refilc_kreta_api/models/grade.dart'; - -class GradeRarityTextSetting extends StatefulWidget { - const GradeRarityTextSetting({ - super.key, - required this.title, - required this.cancel, - required this.done, - required this.defaultRarities, - }); - - final String title; - final String cancel; - final String done; - final List defaultRarities; - - @override - GradeRarityTextSettingState createState() => GradeRarityTextSettingState(); -} - -class GradeRarityTextSettingState extends State { - late SettingsProvider settings; - late DatabaseProvider db; - late UserProvider user; - - final _rarityText = TextEditingController(); - - @override - void initState() { - super.initState(); - settings = Provider.of(context, listen: false); - db = Provider.of(context, listen: false); - user = Provider.of(context, listen: false); - } - - @override - Widget build(BuildContext context) { - return Column(children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: List.generate(5, (index) { - return ClipOval( - child: Material( - type: MaterialType.transparency, - child: InkWell( - onTap: () async { - showRenameDialog( - title: widget.title, - cancel: widget.cancel, - done: widget.done, - rarities: - await db.userQuery.getGradeRarities(userId: user.id!), - gradeIndex: (index + 1).toString(), - defaultRarities: widget.defaultRarities, - ); - }, - child: GradeValueWidget(GradeValue(index + 1, "", "", 0), - fill: true, size: 36.0), - ), - ), - ); - }), - ), - ), - ]); - } - - void showRenameDialog( - {required String title, - required String cancel, - required String done, - required Map rarities, - required String gradeIndex, - required List defaultRarities, - required}) { - showDialog( - context: context, - builder: (context) => StatefulBuilder(builder: (context, setS) { - String? rr = rarities[gradeIndex]; - rr ??= ''; - - _rarityText.text = rr; - - return AlertDialog( - title: Text(title), - content: TextField( - controller: _rarityText, - autofocus: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - label: Text(defaultRarities[int.parse(gradeIndex) - 1]), - suffixIcon: IconButton( - icon: const Icon(FeatherIcons.x), - onPressed: () { - setState(() { - _rarityText.clear(); - }); - }, - ), - ), - ), - actions: [ - TextButton( - child: Text( - cancel, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - onPressed: () { - Navigator.of(context).maybePop(); - }, - ), - TextButton( - child: Text( - done, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - onPressed: () { - rarities[gradeIndex] = _rarityText.text; - - Provider.of(context, listen: false) - .userStore - .storeGradeRarities(rarities, userId: user.id!); - - Navigator.of(context).pop(true); - }, - ), - ], - ); - }), - ).then((val) { - _rarityText.clear(); - }); - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc/ui/widgets/grade/grade_tile.dart'; +import 'package:refilc_kreta_api/models/grade.dart'; + +class GradeRarityTextSetting extends StatefulWidget { + const GradeRarityTextSetting({ + super.key, + required this.title, + required this.cancel, + required this.done, + required this.defaultRarities, + }); + + final String title; + final String cancel; + final String done; + final List defaultRarities; + + @override + GradeRarityTextSettingState createState() => GradeRarityTextSettingState(); +} + +class GradeRarityTextSettingState extends State { + late SettingsProvider settings; + late DatabaseProvider db; + late UserProvider user; + + final _rarityText = TextEditingController(); + + @override + void initState() { + super.initState(); + settings = Provider.of(context, listen: false); + db = Provider.of(context, listen: false); + user = Provider.of(context, listen: false); + } + + @override + Widget build(BuildContext context) { + return Column(children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(5, (index) { + return ClipOval( + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: () async { + showRenameDialog( + title: widget.title, + cancel: widget.cancel, + done: widget.done, + rarities: + await db.userQuery.getGradeRarities(userId: user.id!), + gradeIndex: (index + 1).toString(), + defaultRarities: widget.defaultRarities, + ); + }, + child: GradeValueWidget(GradeValue(index + 1, "", "", 0), + fill: true, size: 36.0), + ), + ), + ); + }), + ), + ), + ]); + } + + void showRenameDialog( + {required String title, + required String cancel, + required String done, + required Map rarities, + required String gradeIndex, + required List defaultRarities, + required}) { + showDialog( + context: context, + builder: (context) => StatefulBuilder(builder: (context, setS) { + String? rr = rarities[gradeIndex]; + rr ??= ''; + + _rarityText.text = rr; + + return AlertDialog( + title: Text(title), + content: TextField( + controller: _rarityText, + autofocus: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + label: Text(defaultRarities[int.parse(gradeIndex) - 1]), + suffixIcon: IconButton( + icon: const Icon(FeatherIcons.x), + onPressed: () { + setState(() { + _rarityText.clear(); + }); + }, + ), + ), + ), + actions: [ + TextButton( + child: Text( + cancel, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + onPressed: () { + Navigator.of(context).maybePop(); + }, + ), + TextButton( + child: Text( + done, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + onPressed: () { + rarities[gradeIndex] = _rarityText.text; + + Provider.of(context, listen: false) + .userStore + .storeGradeRarities(rarities, userId: user.id!); + + Navigator.of(context).pop(true); + }, + ), + ], + ); + }), + ).then((val) { + _rarityText.clear(); + }); + } +} diff --git a/lib/ui/mobile/settings/share_theme.dart b/lib/ui/mobile/settings/share_theme.dart index de16e26..e02cf99 100644 --- a/lib/ui/mobile/settings/share_theme.dart +++ b/lib/ui/mobile/settings/share_theme.dart @@ -1,26 +1,26 @@ -import 'package:refilc/models/settings.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class PremiumShareTheme extends StatefulWidget { - const PremiumShareTheme({super.key}); - - @override - State createState() => _PremiumShareThemeState(); -} - -class _PremiumShareThemeState extends State - with TickerProviderStateMixin { - late final SettingsProvider settingsProvider; - - @override - void initState() { - super.initState(); - settingsProvider = Provider.of(context, listen: false); - } - - @override - Widget build(BuildContext context) { - return const Scaffold(); - } -} +import 'package:refilc/models/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class PremiumShareTheme extends StatefulWidget { + const PremiumShareTheme({super.key}); + + @override + State createState() => _PremiumShareThemeState(); +} + +class _PremiumShareThemeState extends State + with TickerProviderStateMixin { + late final SettingsProvider settingsProvider; + + @override + void initState() { + super.initState(); + settingsProvider = Provider.of(context, listen: false); + } + + @override + Widget build(BuildContext context) { + return const Scaffold(); + } +} diff --git a/lib/ui/mobile/settings/submenu/calendar_sync.dart b/lib/ui/mobile/settings/submenu/calendar_sync.dart index c69d330..130d2c3 100644 --- a/lib/ui/mobile/settings/submenu/calendar_sync.dart +++ b/lib/ui/mobile/settings/submenu/calendar_sync.dart @@ -1,660 +1,660 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:io'; - -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/models/linked_account.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:refilc/providers/third_party_provider.dart'; -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_kreta_api/providers/share_provider.dart'; -import 'package:refilc_mobile_ui/common/dot.dart'; -import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; -import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:provider/provider.dart'; -import 'package:refilc_mobile_ui/common/widgets/custom_segmented_control.dart'; -import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; -import 'package:refilc_plus/models/premium_scopes.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; - -class MenuCalendarSync extends StatelessWidget { - const MenuCalendarSync({ - super.key, - this.borderRadius = const BorderRadius.vertical( - top: Radius.circular(4.0), bottom: Radius.circular(4.0)), - }); - - final BorderRadius borderRadius; - - @override - Widget build(BuildContext context) { - return PanelButton( - onPressed: () async { - // if (!Provider.of(context, listen: false) - // .hasScope(PremiumScopes.calendarSync)) { - // return PlusLockedFeaturePopup.show( - // context: context, feature: PremiumFeature.calendarSync); - // } - - // Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( - // builder: (context) => const CalendarSyncScreen())); - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Figyelem!"), - content: const Text( - "A naptár szinkronizálás csak azután fog működni, hogy a Google elfogadja az OAuth kérelmünket, addig is szíves türelmeteket kérjük! Amint ez megtörténik, értesíteni fogunk titeket Discord-on, valamint alkalmazáson belüli hírekben is."), - actions: [ - TextButton( - child: const Text( - "Vissza", - style: TextStyle(fontWeight: FontWeight.w500), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: const Text( - "Tovább", - style: TextStyle(fontWeight: FontWeight.w500), - ), - onPressed: () { - Navigator.of(context).pop(); - - if (!Provider.of(context, listen: false) - .hasScope(PremiumScopes.calendarSync)) { - return PlusLockedFeaturePopup.show( - context: context, feature: PremiumFeature.calendarSync); - } - - Navigator.of(context, rootNavigator: true).push( - CupertinoPageRoute( - builder: (context) => const CalendarSyncScreen())); - }, - ), - ], - ), - ); - }, - title: Text( - "calendar_sync".i18n, - style: TextStyle( - color: AppColors.of(context).text.withOpacity(.95), - ), - ), - leading: Icon( - FeatherIcons.calendar, - size: 22.0, - color: AppColors.of(context).text.withOpacity(.95), - ), - trailing: Icon( - FeatherIcons.chevronRight, - size: 22.0, - color: AppColors.of(context).text.withOpacity(0.95), - ), - borderRadius: borderRadius, - ); - } -} - -class CalendarSyncScreen extends StatefulWidget { - const CalendarSyncScreen({super.key}); - - @override - CalendarSyncScreenState createState() => CalendarSyncScreenState(); -} - -class CalendarSyncScreenState extends State - with SingleTickerProviderStateMixin { - late SettingsProvider settingsProvider; - late UserProvider user; - late ShareProvider shareProvider; - late ThirdPartyProvider thirdPartyProvider; - - late AnimationController _hideContainersController; - - @override - void initState() { - super.initState(); - - shareProvider = Provider.of(context, listen: false); - - _hideContainersController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 200)); - } - - @override - Widget build(BuildContext context) { - settingsProvider = Provider.of(context); - user = Provider.of(context); - thirdPartyProvider = Provider.of(context); - - return AnimatedBuilder( - animation: _hideContainersController, - builder: (context, child) => Opacity( - opacity: 1 - _hideContainersController.value, - child: Scaffold( - appBar: AppBar( - surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, - leading: BackButton(color: AppColors.of(context).text), - title: Text( - "calendar_sync".i18n, - style: TextStyle(color: AppColors.of(context).text), - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - child: Column( - children: [ - // banner - Padding( - padding: const EdgeInsets.only(top: 10), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - image: const DecorationImage( - image: AssetImage( - 'assets/images/banner_texture.png', - ), - fit: BoxFit.cover, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 40, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16.0), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 4.0, - spreadRadius: 0.01, - ), - ], - ), - height: 64, - width: 64, - child: const Icon( - Icons.calendar_month, - color: Colors.black, - size: 38.0, - ), - ), - const SizedBox(width: 10), - Icon( - Icons.sync_alt_outlined, - color: Colors.black.withOpacity( - thirdPartyProvider.linkedAccounts.isEmpty - ? 0.2 - : 0.5), - size: 20.0, - ), - const SizedBox(width: 10), - Container( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(16.0), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 4.0, - spreadRadius: 0.01, - ), - ], - ), - child: Image.asset( - 'assets/icons/ic_rounded.png', - width: 64, - height: 64, - ), - ), - ], - ), - ), - ), - ), - - const SizedBox( - height: 18.0, - ), - // choose account if not logged in - if (thirdPartyProvider.linkedAccounts.isEmpty) - Column( - children: [ - if (Platform.isAndroid) - SplittedPanel( - title: Text('choose_account'.i18n), - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - isSeparated: true, - children: [ - PanelButton( - onPressed: () async { - await Provider.of(context, - listen: false) - .googleSignIn(); - - setState(() {}); - }, - title: Text( - 'Google', - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity(.95), - ), - ), - leading: Image.asset( - 'assets/images/ext_logo/google.png', - width: 24.0, - height: 24.0, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - ], - ), - // const SizedBox( - // height: 9.0, - // ), - if (Platform.isIOS) - SplittedPanel( - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - isSeparated: true, - children: [ - PanelButton( - onPressed: null, - title: Text( - 'Apple', - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity(.55), - decoration: TextDecoration.lineThrough, - ), - ), - leading: Image.asset( - 'assets/images/ext_logo/apple.png', - width: 24.0, - height: 24.0, - ), - trailing: Text( - 'soon'.i18n, - style: const TextStyle( - fontStyle: FontStyle.italic, - fontSize: 14.0), - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - ], - ), - - const SizedBox( - height: 10.0, - ), - const Text( - "A naptár szinkronizálás csak azután fog működni, hogy a Google elfogadja az OAuth kérelmünket, addig is szíves türelmeteket kérjük! Amint ez megtörténik, értesíteni fogunk titeket Discord-on, valamint alkalmazáson belüli hírekben is."), - ], - ), - - // show options if logged in - if (thirdPartyProvider.linkedAccounts.isNotEmpty) - Column( - children: [ - SplittedPanel( - title: Text('your_account'.i18n), - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - children: [ - PanelButton( - onPressed: null, - title: Text( - thirdPartyProvider - .linkedAccounts.first.username, - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity(.95), - ), - ), - leading: Image.asset( - 'assets/images/ext_logo/${thirdPartyProvider.linkedAccounts.first.type == AccountType.google ? "google" : "apple"}.png', - width: 24.0, - height: 24.0, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - PanelButton( - onPressed: () async { - await thirdPartyProvider.signOutAll(); - setState(() {}); - }, - title: Text( - 'change_account'.i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity(.95), - ), - ), - trailing: Icon( - FeatherIcons.chevronRight, - size: 22.0, - color: AppColors.of(context) - .text - .withOpacity(0.95), - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - ], - ), - const SizedBox( - height: 18.0, - ), - SplittedPanel( - title: Text('choose_calendar'.i18n), - padding: EdgeInsets.zero, - cardPadding: EdgeInsets.zero, - isTransparent: true, - children: getCalendarList(), - ), - const SizedBox( - height: 18.0, - ), - SplittedPanel( - title: Text('room_num_location'.i18n), - padding: EdgeInsets.zero, - cardPadding: EdgeInsets.zero, - isTransparent: true, - children: [ - CustomSegmentedControl( - onChanged: (v) { - settingsProvider.update( - calSyncRoomLocation: - v == 0 ? 'location' : 'description'); - }, - value: settingsProvider.calSyncRoomLocation == - 'location' - ? 0 - : 1, - height: 45, - children: [ - Text( - 'location'.i18n, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - ), - ), - Text( - 'description'.i18n, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - ), - ), - ], - ), - ], - ), - const SizedBox( - height: 18.0, - ), - SplittedPanel( - title: Text('options'.i18n), - padding: EdgeInsets.zero, - cardPadding: EdgeInsets.zero, - isTransparent: true, - isSeparated: true, - children: [ - SplittedPanel( - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - children: [ - PanelButton( - padding: const EdgeInsets.only( - left: 14.0, right: 6.0), - onPressed: () async { - settingsProvider.update( - calSyncShowExams: - !settingsProvider.calSyncShowExams); - - setState(() {}); - }, - title: Text( - "show_exams".i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity( - settingsProvider.calSyncShowExams - ? .95 - : .25), - ), - ), - trailing: Switch( - onChanged: (v) async { - settingsProvider.update( - calSyncShowExams: v); - - setState(() {}); - }, - value: settingsProvider.calSyncShowExams, - activeColor: - Theme.of(context).colorScheme.secondary, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12.0), - bottom: Radius.circular(12.0), - ), - ), - ], - ), - SplittedPanel( - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - children: [ - PanelButton( - padding: const EdgeInsets.only( - left: 14.0, right: 6.0), - onPressed: () async { - settingsProvider.update( - calSyncShowTeacher: !settingsProvider - .calSyncShowTeacher); - - setState(() {}); - }, - title: Text( - "show_teacher".i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity(settingsProvider - .calSyncShowTeacher - ? .95 - : .25), - ), - ), - trailing: Switch( - onChanged: (v) async { - settingsProvider.update( - calSyncShowTeacher: v); - - setState(() {}); - }, - value: settingsProvider.calSyncShowTeacher, - activeColor: - Theme.of(context).colorScheme.secondary, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12.0), - bottom: Radius.circular(12.0), - ), - ), - ], - ), - SplittedPanel( - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - children: [ - PanelButton( - padding: const EdgeInsets.only( - left: 14.0, right: 6.0), - onPressed: () async { - settingsProvider.update( - calSyncRenamed: - !settingsProvider.calSyncRenamed); - - setState(() {}); - }, - title: Text( - "show_renamed".i18n, - style: TextStyle( - color: AppColors.of(context) - .text - .withOpacity( - settingsProvider.calSyncRenamed - ? .95 - : .25), - ), - ), - trailing: Switch( - onChanged: (v) async { - settingsProvider.update( - calSyncRenamed: v); - - setState(() {}); - }, - value: settingsProvider.calSyncRenamed, - activeColor: - Theme.of(context).colorScheme.secondary, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12.0), - bottom: Radius.circular(12.0), - ), - ), - ], - ), - ], - ), - ], - ), - ], - ), - ), - ), - ), - ), - ); - } - - List getCalendarList() { - // List widgets = thirdPartyProvider.googleCalendars - // .map( - // (e) => Container( - // margin: const EdgeInsets.only(bottom: 3.0), - // decoration: BoxDecoration( - // border: Border.all( - // color: Theme.of(context).colorScheme.primary.withOpacity(.25), - // width: 1.0, - // ), - // borderRadius: BorderRadius.circular(12.0), - // ), - // child: PanelButton( - // onPressed: () async { - // print((e.backgroundColor ?? '#000000').replaceAll('#', '0x')); - // setState(() {}); - // }, - // title: Text( - // e.summary ?? 'no_title'.i18n, - // style: TextStyle( - // color: AppColors.of(context).text.withOpacity(.95), - // ), - // ), - // leading: Dot( - // color: colorFromHex( - // e.backgroundColor ?? '#000', - // ) ?? - // Colors.black, - // ), - // borderRadius: const BorderRadius.vertical( - // top: Radius.circular(12), - // bottom: Radius.circular(12), - // ), - // ), - // ), - // ) - // .toList(); - - List widgets = []; - - widgets.add( - Container( - margin: const EdgeInsets.only(bottom: 3.0), - decoration: BoxDecoration( - // border: Border.all( - // color: Theme.of(context).colorScheme.primary.withOpacity(.25), - // width: 1.0, - // ), - color: AppColors.of(context).highlight, - borderRadius: BorderRadius.circular(16.0), - ), - child: PanelButton( - onPressed: null, - // onPressed: () async { - // // thirdPartyProvider.pushTimetable(context, timetable); - // setState(() {}); - // }, - title: Text( - 'reFilc - Órarend', - style: TextStyle( - color: AppColors.of(context).text.withOpacity(.95), - ), - ), - // leading: Icon( - // FeatherIcons.plus, - // size: 20.0, - // color: AppColors.of(context).text.withOpacity(0.75), - // ), - leading: Dot( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - ), - ); - - return widgets; - } -} +// ignore_for_file: use_build_context_synchronously + +import 'dart:io'; + +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/models/linked_account.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc/providers/third_party_provider.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_kreta_api/providers/share_provider.dart'; +import 'package:refilc_mobile_ui/common/dot.dart'; +import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; +import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc_mobile_ui/common/widgets/custom_segmented_control.dart'; +import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; +import 'package:refilc_plus/models/premium_scopes.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; + +class MenuCalendarSync extends StatelessWidget { + const MenuCalendarSync({ + super.key, + this.borderRadius = const BorderRadius.vertical( + top: Radius.circular(4.0), bottom: Radius.circular(4.0)), + }); + + final BorderRadius borderRadius; + + @override + Widget build(BuildContext context) { + return PanelButton( + onPressed: () async { + // if (!Provider.of(context, listen: false) + // .hasScope(PremiumScopes.calendarSync)) { + // return PlusLockedFeaturePopup.show( + // context: context, feature: PremiumFeature.calendarSync); + // } + + // Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( + // builder: (context) => const CalendarSyncScreen())); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Figyelem!"), + content: const Text( + "A naptár szinkronizálás csak azután fog működni, hogy a Google elfogadja az OAuth kérelmünket, addig is szíves türelmeteket kérjük! Amint ez megtörténik, értesíteni fogunk titeket Discord-on, valamint alkalmazáson belüli hírekben is."), + actions: [ + TextButton( + child: const Text( + "Vissza", + style: TextStyle(fontWeight: FontWeight.w500), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text( + "Tovább", + style: TextStyle(fontWeight: FontWeight.w500), + ), + onPressed: () { + Navigator.of(context).pop(); + + if (!Provider.of(context, listen: false) + .hasScope(PremiumScopes.calendarSync)) { + return PlusLockedFeaturePopup.show( + context: context, feature: PremiumFeature.calendarSync); + } + + Navigator.of(context, rootNavigator: true).push( + CupertinoPageRoute( + builder: (context) => const CalendarSyncScreen())); + }, + ), + ], + ), + ); + }, + title: Text( + "calendar_sync".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity(.95), + ), + ), + leading: Icon( + FeatherIcons.calendar, + size: 22.0, + color: AppColors.of(context).text.withOpacity(.95), + ), + trailing: Icon( + FeatherIcons.chevronRight, + size: 22.0, + color: AppColors.of(context).text.withOpacity(0.95), + ), + borderRadius: borderRadius, + ); + } +} + +class CalendarSyncScreen extends StatefulWidget { + const CalendarSyncScreen({super.key}); + + @override + CalendarSyncScreenState createState() => CalendarSyncScreenState(); +} + +class CalendarSyncScreenState extends State + with SingleTickerProviderStateMixin { + late SettingsProvider settingsProvider; + late UserProvider user; + late ShareProvider shareProvider; + late ThirdPartyProvider thirdPartyProvider; + + late AnimationController _hideContainersController; + + @override + void initState() { + super.initState(); + + shareProvider = Provider.of(context, listen: false); + + _hideContainersController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 200)); + } + + @override + Widget build(BuildContext context) { + settingsProvider = Provider.of(context); + user = Provider.of(context); + thirdPartyProvider = Provider.of(context); + + return AnimatedBuilder( + animation: _hideContainersController, + builder: (context, child) => Opacity( + opacity: 1 - _hideContainersController.value, + child: Scaffold( + appBar: AppBar( + surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, + leading: BackButton(color: AppColors.of(context).text), + title: Text( + "calendar_sync".i18n, + style: TextStyle(color: AppColors.of(context).text), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), + child: Column( + children: [ + // banner + Padding( + padding: const EdgeInsets.only(top: 10), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + image: const DecorationImage( + image: AssetImage( + 'assets/images/banner_texture.png', + ), + fit: BoxFit.cover, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 40, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 4.0, + spreadRadius: 0.01, + ), + ], + ), + height: 64, + width: 64, + child: const Icon( + Icons.calendar_month, + color: Colors.black, + size: 38.0, + ), + ), + const SizedBox(width: 10), + Icon( + Icons.sync_alt_outlined, + color: Colors.black.withOpacity( + thirdPartyProvider.linkedAccounts.isEmpty + ? 0.2 + : 0.5), + size: 20.0, + ), + const SizedBox(width: 10), + Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(16.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 4.0, + spreadRadius: 0.01, + ), + ], + ), + child: Image.asset( + 'assets/icons/ic_rounded.png', + width: 64, + height: 64, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox( + height: 18.0, + ), + // choose account if not logged in + if (thirdPartyProvider.linkedAccounts.isEmpty) + Column( + children: [ + if (Platform.isAndroid) + SplittedPanel( + title: Text('choose_account'.i18n), + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + onPressed: () async { + await Provider.of(context, + listen: false) + .googleSignIn(); + + setState(() {}); + }, + title: Text( + 'Google', + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity(.95), + ), + ), + leading: Image.asset( + 'assets/images/ext_logo/google.png', + width: 24.0, + height: 24.0, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + ], + ), + // const SizedBox( + // height: 9.0, + // ), + if (Platform.isIOS) + SplittedPanel( + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + onPressed: null, + title: Text( + 'Apple', + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity(.55), + decoration: TextDecoration.lineThrough, + ), + ), + leading: Image.asset( + 'assets/images/ext_logo/apple.png', + width: 24.0, + height: 24.0, + ), + trailing: Text( + 'soon'.i18n, + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: 14.0), + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + ], + ), + + const SizedBox( + height: 10.0, + ), + const Text( + "A naptár szinkronizálás csak azután fog működni, hogy a Google elfogadja az OAuth kérelmünket, addig is szíves türelmeteket kérjük! Amint ez megtörténik, értesíteni fogunk titeket Discord-on, valamint alkalmazáson belüli hírekben is."), + ], + ), + + // show options if logged in + if (thirdPartyProvider.linkedAccounts.isNotEmpty) + Column( + children: [ + SplittedPanel( + title: Text('your_account'.i18n), + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + children: [ + PanelButton( + onPressed: null, + title: Text( + thirdPartyProvider + .linkedAccounts.first.username, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity(.95), + ), + ), + leading: Image.asset( + 'assets/images/ext_logo/${thirdPartyProvider.linkedAccounts.first.type == AccountType.google ? "google" : "apple"}.png', + width: 24.0, + height: 24.0, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + PanelButton( + onPressed: () async { + await thirdPartyProvider.signOutAll(); + setState(() {}); + }, + title: Text( + 'change_account'.i18n, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity(.95), + ), + ), + trailing: Icon( + FeatherIcons.chevronRight, + size: 22.0, + color: AppColors.of(context) + .text + .withOpacity(0.95), + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + ], + ), + const SizedBox( + height: 18.0, + ), + SplittedPanel( + title: Text('choose_calendar'.i18n), + padding: EdgeInsets.zero, + cardPadding: EdgeInsets.zero, + isTransparent: true, + children: getCalendarList(), + ), + const SizedBox( + height: 18.0, + ), + SplittedPanel( + title: Text('room_num_location'.i18n), + padding: EdgeInsets.zero, + cardPadding: EdgeInsets.zero, + isTransparent: true, + children: [ + CustomSegmentedControl( + onChanged: (v) { + settingsProvider.update( + calSyncRoomLocation: + v == 0 ? 'location' : 'description'); + }, + value: settingsProvider.calSyncRoomLocation == + 'location' + ? 0 + : 1, + height: 45, + children: [ + Text( + 'location'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), + ), + Text( + 'description'.i18n, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), + ), + ], + ), + ], + ), + const SizedBox( + height: 18.0, + ), + SplittedPanel( + title: Text('options'.i18n), + padding: EdgeInsets.zero, + cardPadding: EdgeInsets.zero, + isTransparent: true, + isSeparated: true, + children: [ + SplittedPanel( + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + children: [ + PanelButton( + padding: const EdgeInsets.only( + left: 14.0, right: 6.0), + onPressed: () async { + settingsProvider.update( + calSyncShowExams: + !settingsProvider.calSyncShowExams); + + setState(() {}); + }, + title: Text( + "show_exams".i18n, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity( + settingsProvider.calSyncShowExams + ? .95 + : .25), + ), + ), + trailing: Switch( + onChanged: (v) async { + settingsProvider.update( + calSyncShowExams: v); + + setState(() {}); + }, + value: settingsProvider.calSyncShowExams, + activeColor: + Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), + ), + ), + ], + ), + SplittedPanel( + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + children: [ + PanelButton( + padding: const EdgeInsets.only( + left: 14.0, right: 6.0), + onPressed: () async { + settingsProvider.update( + calSyncShowTeacher: !settingsProvider + .calSyncShowTeacher); + + setState(() {}); + }, + title: Text( + "show_teacher".i18n, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity(settingsProvider + .calSyncShowTeacher + ? .95 + : .25), + ), + ), + trailing: Switch( + onChanged: (v) async { + settingsProvider.update( + calSyncShowTeacher: v); + + setState(() {}); + }, + value: settingsProvider.calSyncShowTeacher, + activeColor: + Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), + ), + ), + ], + ), + SplittedPanel( + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + children: [ + PanelButton( + padding: const EdgeInsets.only( + left: 14.0, right: 6.0), + onPressed: () async { + settingsProvider.update( + calSyncRenamed: + !settingsProvider.calSyncRenamed); + + setState(() {}); + }, + title: Text( + "show_renamed".i18n, + style: TextStyle( + color: AppColors.of(context) + .text + .withOpacity( + settingsProvider.calSyncRenamed + ? .95 + : .25), + ), + ), + trailing: Switch( + onChanged: (v) async { + settingsProvider.update( + calSyncRenamed: v); + + setState(() {}); + }, + value: settingsProvider.calSyncRenamed, + activeColor: + Theme.of(context).colorScheme.secondary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12.0), + bottom: Radius.circular(12.0), + ), + ), + ], + ), + ], + ), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } + + List getCalendarList() { + // List widgets = thirdPartyProvider.googleCalendars + // .map( + // (e) => Container( + // margin: const EdgeInsets.only(bottom: 3.0), + // decoration: BoxDecoration( + // border: Border.all( + // color: Theme.of(context).colorScheme.primary.withOpacity(.25), + // width: 1.0, + // ), + // borderRadius: BorderRadius.circular(12.0), + // ), + // child: PanelButton( + // onPressed: () async { + // print((e.backgroundColor ?? '#000000').replaceAll('#', '0x')); + // setState(() {}); + // }, + // title: Text( + // e.summary ?? 'no_title'.i18n, + // style: TextStyle( + // color: AppColors.of(context).text.withOpacity(.95), + // ), + // ), + // leading: Dot( + // color: colorFromHex( + // e.backgroundColor ?? '#000', + // ) ?? + // Colors.black, + // ), + // borderRadius: const BorderRadius.vertical( + // top: Radius.circular(12), + // bottom: Radius.circular(12), + // ), + // ), + // ), + // ) + // .toList(); + + List widgets = []; + + widgets.add( + Container( + margin: const EdgeInsets.only(bottom: 3.0), + decoration: BoxDecoration( + // border: Border.all( + // color: Theme.of(context).colorScheme.primary.withOpacity(.25), + // width: 1.0, + // ), + color: AppColors.of(context).highlight, + borderRadius: BorderRadius.circular(16.0), + ), + child: PanelButton( + onPressed: null, + // onPressed: () async { + // // thirdPartyProvider.pushTimetable(context, timetable); + // setState(() {}); + // }, + title: Text( + 'reFilc - Órarend', + style: TextStyle( + color: AppColors.of(context).text.withOpacity(.95), + ), + ), + // leading: Icon( + // FeatherIcons.plus, + // size: 20.0, + // color: AppColors.of(context).text.withOpacity(0.75), + // ), + leading: Dot( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + ), + ); + + return widgets; + } +} diff --git a/lib/ui/mobile/settings/submenu/grade_exporting.dart b/lib/ui/mobile/settings/submenu/grade_exporting.dart index da966b5..4ecebb0 100644 --- a/lib/ui/mobile/settings/submenu/grade_exporting.dart +++ b/lib/ui/mobile/settings/submenu/grade_exporting.dart @@ -1,363 +1,363 @@ -// ignore_for_file: use_build_context_synchronously - -import 'dart:convert'; -import 'dart:io'; - -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:refilc/providers/third_party_provider.dart'; -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_kreta_api/models/grade.dart'; -import 'package:refilc_kreta_api/providers/grade_provider.dart'; -import 'package:refilc_kreta_api/providers/share_provider.dart'; -import 'package:refilc_mobile_ui/common/dot.dart'; -import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; -import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:provider/provider.dart'; -import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; -import 'package:refilc_plus/models/premium_scopes.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:refilc_mobile_ui/common/chips/new_chip.dart'; - -class MenuGradeExporting extends StatelessWidget { - const MenuGradeExporting({ - super.key, - this.borderRadius = const BorderRadius.vertical( - top: Radius.circular(4.0), bottom: Radius.circular(4.0)), - }); - - final BorderRadius borderRadius; - - @override - Widget build(BuildContext context) { - return PanelButton( - onPressed: () async { - // if (!Provider.of(context, listen: false) - // .hasScope(PremiumScopes.calendarSync)) { - // return PlusLockedFeaturePopup.show( - // context: context, feature: PremiumFeature.calendarSync); - // } - - // Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( - // builder: (context) => const CalendarSyncScreen())); - // showDialog( - // context: context, - // builder: (context) => AlertDialog( - // title: const Text("Figyelem!"), - // content: const Text( - // "Az exportált jegyek jelenleg még nem megtekinthetők a reFilc-ben, csak te magad tudod átnézni őket JSON formátumban. A jövőben ez a funkció bővülni fog, és a jegyeket meg is tekintheted majd a reFilc felületén."), - // actions: [ - // // TextButton( - // // child: const Text( - // // "Vissza", - // // style: TextStyle(fontWeight: FontWeight.w500), - // // ), - // // onPressed: () { - // // Navigator.of(context).pop(); - // // }, - // // ), - // TextButton( - // child: const Text( - // "Tovább", - // style: TextStyle(fontWeight: FontWeight.w500), - // ), - // onPressed: () { - // Navigator.of(context).pop(); - - Provider.of(context, listen: false).update( - unseenNewFeatures: List.from( - Provider.of(context, listen: false) - .unseenNewFeatures - ..remove('grade_exporting'), - ), - ); - // Provider.of(context, listen: false).update( - // unseenNewFeatures: ['grade_exporting'], - // ); - - if (!Provider.of(context, listen: false) - .hasScope(PremiumScopes.gradeExporting)) { - return PlusLockedFeaturePopup.show( - context: context, feature: PremiumFeature.gradeExporting); - } - - Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( - builder: (context) => const GradeExportingScreen())); - // }, - // ), - // ], - // ), - // ); - }, - title: Text( - "grade_exporting".i18n, - style: TextStyle( - color: AppColors.of(context).text.withOpacity(.95), - ), - ), - leading: Icon( - Icons.toll_rounded, - size: 22.0, - color: AppColors.of(context).text.withOpacity(.95), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (Provider.of(context) - .unseenNewFeatures - .contains('grade_exporting')) - const NewChip(), - Icon( - FeatherIcons.chevronRight, - size: 22.0, - color: AppColors.of(context).text.withOpacity(0.95), - ) - ], - ), - borderRadius: borderRadius, - ); - } -} - -class GradeExportingScreen extends StatefulWidget { - const GradeExportingScreen({super.key}); - - @override - CalendarSyncScreenState createState() => CalendarSyncScreenState(); -} - -class CalendarSyncScreenState extends State - with SingleTickerProviderStateMixin { - late SettingsProvider settingsProvider; - late UserProvider user; - late ShareProvider shareProvider; - late ThirdPartyProvider thirdPartyProvider; - - late AnimationController _hideContainersController; - - @override - void initState() { - super.initState(); - - shareProvider = Provider.of(context, listen: false); - - _hideContainersController = AnimationController( - vsync: this, duration: const Duration(milliseconds: 200)); - } - - @override - Widget build(BuildContext context) { - settingsProvider = Provider.of(context); - user = Provider.of(context); - thirdPartyProvider = Provider.of(context); - - return AnimatedBuilder( - animation: _hideContainersController, - builder: (context, child) => Opacity( - opacity: 1 - _hideContainersController.value, - child: Scaffold( - appBar: AppBar( - surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, - leading: BackButton(color: AppColors.of(context).text), - title: Text( - "grade_exporting".i18n, - style: TextStyle(color: AppColors.of(context).text), - ), - ), - body: SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - child: Column( - children: [ - // choose export method - Column( - children: [ - SplittedPanel( - title: Text('export_method'.i18n), - padding: EdgeInsets.zero, - cardPadding: const EdgeInsets.all(4.0), - isSeparated: true, - children: [ - PanelButton( - onPressed: () async { - // get all grades - List grades = Provider.of( - context, - listen: false) - .grades; - - // gmake a list of grades in json format - List> gradesList = [ - for (Grade grade in grades) - // { - // '"subject"': '"${grade.subject.name}"', - // '"value"': grade.value.value, - // '"value_name"': - // '"${grade.value.valueName}"', - // '"date"': - // '"${grade.date.toIso8601String()}"', - // '"weight"': grade.value.weight, - // '"type"': '"${grade.type.name}"', - // '"description"': '"${grade.description}"', - // '"teacher"': '"${grade.teacher.name}"', - // } - grade.json ?? {}, - ]; - - // convert list to json file - final directory = await getTemporaryDirectory(); - - File file = File('${directory.path}/grades.json'); - file.writeAsStringSync( - jsonEncode(gradesList), - ); - - // convert json to bytes - final jsonBytes = file.readAsBytesSync(); - - // get current study year - final now = DateTime.now(); - String studyYearStr = ''; - if (now.month <= 8) { - studyYearStr = '${now.year - 1}_${now.year}'; - } else { - studyYearStr = '${now.year}_${now.year + 1}'; - } - - // open the share popup with the json file - Share.shareXFiles( - [ - XFile.fromData( - jsonBytes, - name: 'refilc_grades_$studyYearStr', - mimeType: 'application/json', - ), - ], - subject: - 'reFilc Jegyek - ${studyYearStr.replaceAll('_', '/')}', - ); - }, - title: Text( - 'JSON', - style: TextStyle( - color: - AppColors.of(context).text.withOpacity(.95), - ), - ), - // leading: Image.asset( - // 'assets/images/ext_logo/google.png', - // width: 24.0, - // height: 24.0, - // ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - ], - ), - // const SizedBox( - // height: 10.0, - // ), - // const Text( - // "Az exportált jegyek jelenleg még nem megtekinthetők a reFilc-ben, csak te magad tudod átnézni őket JSON formátumban. A jövőben ez a funkció bővülni fog, és a jegyeket meg is tekintheted majd a reFilc felületén."), - ], - ), - ], - ), - ), - ), - ), - ), - ); - } - - List getCalendarList() { - // List widgets = thirdPartyProvider.googleCalendars - // .map( - // (e) => Container( - // margin: const EdgeInsets.only(bottom: 3.0), - // decoration: BoxDecoration( - // border: Border.all( - // color: Theme.of(context).colorScheme.primary.withOpacity(.25), - // width: 1.0, - // ), - // borderRadius: BorderRadius.circular(12.0), - // ), - // child: PanelButton( - // onPressed: () async { - // print((e.backgroundColor ?? '#000000').replaceAll('#', '0x')); - // setState(() {}); - // }, - // title: Text( - // e.summary ?? 'no_title'.i18n, - // style: TextStyle( - // color: AppColors.of(context).text.withOpacity(.95), - // ), - // ), - // leading: Dot( - // color: colorFromHex( - // e.backgroundColor ?? '#000', - // ) ?? - // Colors.black, - // ), - // borderRadius: const BorderRadius.vertical( - // top: Radius.circular(12), - // bottom: Radius.circular(12), - // ), - // ), - // ), - // ) - // .toList(); - - List widgets = []; - - widgets.add( - Container( - margin: const EdgeInsets.only(bottom: 3.0), - decoration: BoxDecoration( - // border: Border.all( - // color: Theme.of(context).colorScheme.primary.withOpacity(.25), - // width: 1.0, - // ), - color: AppColors.of(context).highlight, - borderRadius: BorderRadius.circular(16.0), - ), - child: PanelButton( - onPressed: null, - // onPressed: () async { - // // thirdPartyProvider.pushTimetable(context, timetable); - // setState(() {}); - // }, - title: Text( - 'reFilc - Órarend', - style: TextStyle( - color: AppColors.of(context).text.withOpacity(.95), - ), - ), - // leading: Icon( - // FeatherIcons.plus, - // size: 20.0, - // color: AppColors.of(context).text.withOpacity(0.75), - // ), - leading: Dot( - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - bottom: Radius.circular(12), - ), - ), - ), - ); - - return widgets; - } -} +// ignore_for_file: use_build_context_synchronously + +import 'dart:convert'; +import 'dart:io'; + +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc/providers/third_party_provider.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_kreta_api/models/grade.dart'; +import 'package:refilc_kreta_api/providers/grade_provider.dart'; +import 'package:refilc_kreta_api/providers/share_provider.dart'; +import 'package:refilc_mobile_ui/common/dot.dart'; +import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; +import 'package:refilc_mobile_ui/common/splitted_panel/splitted_panel.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:provider/provider.dart'; +import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; +import 'package:refilc_plus/models/premium_scopes.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:refilc_mobile_ui/common/chips/new_chip.dart'; + +class MenuGradeExporting extends StatelessWidget { + const MenuGradeExporting({ + super.key, + this.borderRadius = const BorderRadius.vertical( + top: Radius.circular(4.0), bottom: Radius.circular(4.0)), + }); + + final BorderRadius borderRadius; + + @override + Widget build(BuildContext context) { + return PanelButton( + onPressed: () async { + // if (!Provider.of(context, listen: false) + // .hasScope(PremiumScopes.calendarSync)) { + // return PlusLockedFeaturePopup.show( + // context: context, feature: PremiumFeature.calendarSync); + // } + + // Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( + // builder: (context) => const CalendarSyncScreen())); + // showDialog( + // context: context, + // builder: (context) => AlertDialog( + // title: const Text("Figyelem!"), + // content: const Text( + // "Az exportált jegyek jelenleg még nem megtekinthetők a reFilc-ben, csak te magad tudod átnézni őket JSON formátumban. A jövőben ez a funkció bővülni fog, és a jegyeket meg is tekintheted majd a reFilc felületén."), + // actions: [ + // // TextButton( + // // child: const Text( + // // "Vissza", + // // style: TextStyle(fontWeight: FontWeight.w500), + // // ), + // // onPressed: () { + // // Navigator.of(context).pop(); + // // }, + // // ), + // TextButton( + // child: const Text( + // "Tovább", + // style: TextStyle(fontWeight: FontWeight.w500), + // ), + // onPressed: () { + // Navigator.of(context).pop(); + + Provider.of(context, listen: false).update( + unseenNewFeatures: List.from( + Provider.of(context, listen: false) + .unseenNewFeatures + ..remove('grade_exporting'), + ), + ); + // Provider.of(context, listen: false).update( + // unseenNewFeatures: ['grade_exporting'], + // ); + + if (!Provider.of(context, listen: false) + .hasScope(PremiumScopes.gradeExporting)) { + return PlusLockedFeaturePopup.show( + context: context, feature: PremiumFeature.gradeExporting); + } + + Navigator.of(context, rootNavigator: true).push(CupertinoPageRoute( + builder: (context) => const GradeExportingScreen())); + // }, + // ), + // ], + // ), + // ); + }, + title: Text( + "grade_exporting".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity(.95), + ), + ), + leading: Icon( + Icons.toll_rounded, + size: 22.0, + color: AppColors.of(context).text.withOpacity(.95), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (Provider.of(context) + .unseenNewFeatures + .contains('grade_exporting')) + const NewChip(), + Icon( + FeatherIcons.chevronRight, + size: 22.0, + color: AppColors.of(context).text.withOpacity(0.95), + ) + ], + ), + borderRadius: borderRadius, + ); + } +} + +class GradeExportingScreen extends StatefulWidget { + const GradeExportingScreen({super.key}); + + @override + CalendarSyncScreenState createState() => CalendarSyncScreenState(); +} + +class CalendarSyncScreenState extends State + with SingleTickerProviderStateMixin { + late SettingsProvider settingsProvider; + late UserProvider user; + late ShareProvider shareProvider; + late ThirdPartyProvider thirdPartyProvider; + + late AnimationController _hideContainersController; + + @override + void initState() { + super.initState(); + + shareProvider = Provider.of(context, listen: false); + + _hideContainersController = AnimationController( + vsync: this, duration: const Duration(milliseconds: 200)); + } + + @override + Widget build(BuildContext context) { + settingsProvider = Provider.of(context); + user = Provider.of(context); + thirdPartyProvider = Provider.of(context); + + return AnimatedBuilder( + animation: _hideContainersController, + builder: (context, child) => Opacity( + opacity: 1 - _hideContainersController.value, + child: Scaffold( + appBar: AppBar( + surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, + leading: BackButton(color: AppColors.of(context).text), + title: Text( + "grade_exporting".i18n, + style: TextStyle(color: AppColors.of(context).text), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), + child: Column( + children: [ + // choose export method + Column( + children: [ + SplittedPanel( + title: Text('export_method'.i18n), + padding: EdgeInsets.zero, + cardPadding: const EdgeInsets.all(4.0), + isSeparated: true, + children: [ + PanelButton( + onPressed: () async { + // get all grades + List grades = Provider.of( + context, + listen: false) + .grades; + + // gmake a list of grades in json format + List> gradesList = [ + for (Grade grade in grades) + // { + // '"subject"': '"${grade.subject.name}"', + // '"value"': grade.value.value, + // '"value_name"': + // '"${grade.value.valueName}"', + // '"date"': + // '"${grade.date.toIso8601String()}"', + // '"weight"': grade.value.weight, + // '"type"': '"${grade.type.name}"', + // '"description"': '"${grade.description}"', + // '"teacher"': '"${grade.teacher.name}"', + // } + grade.json ?? {}, + ]; + + // convert list to json file + final directory = await getTemporaryDirectory(); + + File file = File('${directory.path}/grades.json'); + file.writeAsStringSync( + jsonEncode(gradesList), + ); + + // convert json to bytes + final jsonBytes = file.readAsBytesSync(); + + // get current study year + final now = DateTime.now(); + String studyYearStr = ''; + if (now.month <= 8) { + studyYearStr = '${now.year - 1}_${now.year}'; + } else { + studyYearStr = '${now.year}_${now.year + 1}'; + } + + // open the share popup with the json file + Share.shareXFiles( + [ + XFile.fromData( + jsonBytes, + name: 'refilc_grades_$studyYearStr', + mimeType: 'application/json', + ), + ], + subject: + 'reFilc Jegyek - ${studyYearStr.replaceAll('_', '/')}', + ); + }, + title: Text( + 'JSON', + style: TextStyle( + color: + AppColors.of(context).text.withOpacity(.95), + ), + ), + // leading: Image.asset( + // 'assets/images/ext_logo/google.png', + // width: 24.0, + // height: 24.0, + // ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + ], + ), + // const SizedBox( + // height: 10.0, + // ), + // const Text( + // "Az exportált jegyek jelenleg még nem megtekinthetők a reFilc-ben, csak te magad tudod átnézni őket JSON formátumban. A jövőben ez a funkció bővülni fog, és a jegyeket meg is tekintheted majd a reFilc felületén."), + ], + ), + ], + ), + ), + ), + ), + ), + ); + } + + List getCalendarList() { + // List widgets = thirdPartyProvider.googleCalendars + // .map( + // (e) => Container( + // margin: const EdgeInsets.only(bottom: 3.0), + // decoration: BoxDecoration( + // border: Border.all( + // color: Theme.of(context).colorScheme.primary.withOpacity(.25), + // width: 1.0, + // ), + // borderRadius: BorderRadius.circular(12.0), + // ), + // child: PanelButton( + // onPressed: () async { + // print((e.backgroundColor ?? '#000000').replaceAll('#', '0x')); + // setState(() {}); + // }, + // title: Text( + // e.summary ?? 'no_title'.i18n, + // style: TextStyle( + // color: AppColors.of(context).text.withOpacity(.95), + // ), + // ), + // leading: Dot( + // color: colorFromHex( + // e.backgroundColor ?? '#000', + // ) ?? + // Colors.black, + // ), + // borderRadius: const BorderRadius.vertical( + // top: Radius.circular(12), + // bottom: Radius.circular(12), + // ), + // ), + // ), + // ) + // .toList(); + + List widgets = []; + + widgets.add( + Container( + margin: const EdgeInsets.only(bottom: 3.0), + decoration: BoxDecoration( + // border: Border.all( + // color: Theme.of(context).colorScheme.primary.withOpacity(.25), + // width: 1.0, + // ), + color: AppColors.of(context).highlight, + borderRadius: BorderRadius.circular(16.0), + ), + child: PanelButton( + onPressed: null, + // onPressed: () async { + // // thirdPartyProvider.pushTimetable(context, timetable); + // setState(() {}); + // }, + title: Text( + 'reFilc - Órarend', + style: TextStyle( + color: AppColors.of(context).text.withOpacity(.95), + ), + ), + // leading: Icon( + // FeatherIcons.plus, + // size: 20.0, + // color: AppColors.of(context).text.withOpacity(0.75), + // ), + leading: Dot( + color: Theme.of(context).colorScheme.primary, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(12), + bottom: Radius.circular(12), + ), + ), + ), + ); + + return widgets; + } +} diff --git a/lib/ui/mobile/settings/welcome_message.dart b/lib/ui/mobile/settings/welcome_message.dart index b769236..685e893 100644 --- a/lib/ui/mobile/settings/welcome_message.dart +++ b/lib/ui/mobile/settings/welcome_message.dart @@ -1,157 +1,157 @@ -import 'package:refilc/api/providers/user_provider.dart'; -import 'package:refilc/models/settings.dart'; -import 'package:refilc/theme/colors/colors.dart'; -import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; -import 'package:refilc_plus/models/premium_scopes.dart'; -import 'package:refilc_plus/providers/plus_provider.dart'; -import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_feather_icons/flutter_feather_icons.dart'; -import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; -import 'package:provider/provider.dart'; -import 'package:i18n_extension/i18n_extension.dart'; - -// ignore: must_be_immutable -class WelcomeMessagePanelButton extends StatelessWidget { - late SettingsProvider settingsProvider; - late UserProvider user; - - WelcomeMessagePanelButton(this.settingsProvider, this.user, {super.key}); - - @override - Widget build(BuildContext context) { - String finalName = ((user.nickname ?? '') != '' - ? user.nickname - : (user.displayName ?? '') != '' - ? user.displayName - : 'János') ?? - 'János'; - - return PanelButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => WelcomeMessageEditor(settingsProvider)); - }, - title: Text( - "welcome_msg".i18n, - style: TextStyle( - color: AppColors.of(context).text.withOpacity(.95), - ), - ), - leading: Icon( - FeatherIcons.smile, - size: 22.0, - color: AppColors.of(context).text.withOpacity(.95), - ), - trailing: Container( - constraints: const BoxConstraints(maxWidth: 100), - child: Text( - settingsProvider.welcomeMessage.replaceAll(' ', '') != '' - ? localizeFill( - settingsProvider.welcomeMessage, - [finalName], - ) - : 'default'.i18n, - style: const TextStyle(fontSize: 14.0), - textAlign: TextAlign.end, - softWrap: true, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } -} - -// ignore: must_be_immutable -class WelcomeMessageEditor extends StatefulWidget { - late SettingsProvider settingsProvider; - - WelcomeMessageEditor(this.settingsProvider, {super.key}); - - @override - State createState() => _WelcomeMessageEditorState(); -} - -class _WelcomeMessageEditorState extends State { - final _welcomeMsg = TextEditingController(); - - @override - void initState() { - super.initState(); - _welcomeMsg.text = - widget.settingsProvider.welcomeMessage.replaceAll('%s', '%name%'); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text("edit_welcome_msg".i18n), - content: TextField( - controller: _welcomeMsg, - autofocus: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - label: Text('welcome_msg'.i18n), - suffixIcon: IconButton( - icon: const Icon(FeatherIcons.x), - onPressed: () { - setState(() { - _welcomeMsg.text = ""; - }); - }, - ), - ), - ), - actions: [ - TextButton( - child: Text( - "cancel".i18n, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - onPressed: () { - Navigator.of(context).maybePop(); - }, - ), - TextButton( - child: Text( - "done".i18n, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - onPressed: () { - // var trimmed = _welcomeMsg.text.trim(); - - // var defLen = trimmed.length; - // var replacedLen = trimmed.replaceAll('%s', '').length; - - // if (defLen - 2 > replacedLen) { - // print('fuck yourself rn'); - // } - var finalText = _welcomeMsg.text - .trim() - .replaceFirst('%name%', '\$s') - .replaceFirst('%user%', '\$s') - .replaceFirst('%username%', '\$s') - .replaceFirst('%me%', '\$s') - .replaceFirst('%profile%', '\$s') - .replaceAll('%', '') - .replaceFirst('\$s', '%s'); - // .replaceAll('\$s', 's'); - - if (!Provider.of(context, listen: false) - .hasScope(PremiumScopes.welcomeMessage) && - finalText.replaceAll(' ', '') != '') { - PlusLockedFeaturePopup.show( - context: context, feature: PremiumFeature.welcomeMessage); - return; - } - - widget.settingsProvider - .update(welcomeMessage: finalText, store: true); - Navigator.of(context).pop(true); - }, - ), - ], - ); - } -} +import 'package:refilc/api/providers/user_provider.dart'; +import 'package:refilc/models/settings.dart'; +import 'package:refilc/theme/colors/colors.dart'; +import 'package:refilc_mobile_ui/common/panel/panel_button.dart'; +import 'package:refilc_plus/models/premium_scopes.dart'; +import 'package:refilc_plus/providers/plus_provider.dart'; +import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_feather_icons/flutter_feather_icons.dart'; +import 'package:refilc_mobile_ui/screens/settings/settings_screen.i18n.dart'; +import 'package:provider/provider.dart'; +import 'package:i18n_extension/i18n_extension.dart'; + +// ignore: must_be_immutable +class WelcomeMessagePanelButton extends StatelessWidget { + late SettingsProvider settingsProvider; + late UserProvider user; + + WelcomeMessagePanelButton(this.settingsProvider, this.user, {super.key}); + + @override + Widget build(BuildContext context) { + String finalName = ((user.nickname ?? '') != '' + ? user.nickname + : (user.displayName ?? '') != '' + ? user.displayName + : 'János') ?? + 'János'; + + return PanelButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => WelcomeMessageEditor(settingsProvider)); + }, + title: Text( + "welcome_msg".i18n, + style: TextStyle( + color: AppColors.of(context).text.withOpacity(.95), + ), + ), + leading: Icon( + FeatherIcons.smile, + size: 22.0, + color: AppColors.of(context).text.withOpacity(.95), + ), + trailing: Container( + constraints: const BoxConstraints(maxWidth: 100), + child: Text( + settingsProvider.welcomeMessage.replaceAll(' ', '') != '' + ? localizeFill( + settingsProvider.welcomeMessage, + [finalName], + ) + : 'default'.i18n, + style: const TextStyle(fontSize: 14.0), + textAlign: TextAlign.end, + softWrap: true, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } +} + +// ignore: must_be_immutable +class WelcomeMessageEditor extends StatefulWidget { + late SettingsProvider settingsProvider; + + WelcomeMessageEditor(this.settingsProvider, {super.key}); + + @override + State createState() => _WelcomeMessageEditorState(); +} + +class _WelcomeMessageEditorState extends State { + final _welcomeMsg = TextEditingController(); + + @override + void initState() { + super.initState(); + _welcomeMsg.text = + widget.settingsProvider.welcomeMessage.replaceAll('%s', '%name%'); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("edit_welcome_msg".i18n), + content: TextField( + controller: _welcomeMsg, + autofocus: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + label: Text('welcome_msg'.i18n), + suffixIcon: IconButton( + icon: const Icon(FeatherIcons.x), + onPressed: () { + setState(() { + _welcomeMsg.text = ""; + }); + }, + ), + ), + ), + actions: [ + TextButton( + child: Text( + "cancel".i18n, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + onPressed: () { + Navigator.of(context).maybePop(); + }, + ), + TextButton( + child: Text( + "done".i18n, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + onPressed: () { + // var trimmed = _welcomeMsg.text.trim(); + + // var defLen = trimmed.length; + // var replacedLen = trimmed.replaceAll('%s', '').length; + + // if (defLen - 2 > replacedLen) { + // print('fuck yourself rn'); + // } + var finalText = _welcomeMsg.text + .trim() + .replaceFirst('%name%', '\$s') + .replaceFirst('%user%', '\$s') + .replaceFirst('%username%', '\$s') + .replaceFirst('%me%', '\$s') + .replaceFirst('%profile%', '\$s') + .replaceAll('%', '') + .replaceFirst('\$s', '%s'); + // .replaceAll('\$s', 's'); + + if (!Provider.of(context, listen: false) + .hasScope(PremiumScopes.welcomeMessage) && + finalText.replaceAll(' ', '') != '') { + PlusLockedFeaturePopup.show( + context: context, feature: PremiumFeature.welcomeMessage); + return; + } + + widget.settingsProvider + .update(welcomeMessage: finalText, store: true); + Navigator.of(context).pop(true); + }, + ), + ], + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 76dce3b..d700e66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,52 +1,52 @@ -name: refilc_plus -publish_to: "none" - -environment: - sdk: ^3.6.0 - -dependencies: - flutter: - sdk: flutter - cupertino_icons: ^1.0.2 - - # reFilc Main - refilc: - path: ../refilc/ - # e-KRETA API (kreten) client - refilc_kreta_api: - path: ../refilc_kreta_api/ - # reFilc Mobile UI - refilc_mobile_ui: - path: "../refilc_mobile_ui/" - - provider: ^6.1.1 - flutter_feather_icons: ^2.0.0+1 - uni_links: ^0.5.1 - url_launcher: ^6.2.5 - dropdown_button2: ^2.3.9 - home_widget: - git: - url: https://github.com/refilc/home_widget.git - ref: flutter-beta - image_picker: ^1.0.7 - image_crop: - git: - url: https://github.com/kimaah/image_crop.git - lottie: ^3.1.0 - animations: ^2.0.11 - flutter_svg: ^2.0.10+1 - flutter_dynamic_icon: ^2.1.0 - android_dynamic_icon: ^2.0.0 - i18n_extension: ^12.0.1 - http: ^1.2.0 - fl_chart: ^0.68.0 - flutter_dynamic_icon_plus: ^1.1.2 - share_plus: ^10.0.3 - path_provider: ^2.1.3 - file_picker: ^8.0.5 - -dev_dependencies: - flutter_lints: ^4.0.0 - -flutter: - uses-material-design: true +name: refilc_plus +publish_to: "none" + +environment: + sdk: ^3.6.0 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + + # reFilc Main + refilc: + path: ../refilc/ + # e-KRETA API (kreten) client + refilc_kreta_api: + path: ../refilc_kreta_api/ + # reFilc Mobile UI + refilc_mobile_ui: + path: "../refilc_mobile_ui/" + + provider: ^6.1.1 + flutter_feather_icons: ^2.0.0+1 + uni_links: ^0.5.1 + url_launcher: ^6.2.5 + dropdown_button2: ^2.3.9 + home_widget: + git: + url: https://github.com/refilc/home_widget.git + ref: flutter-beta + image_picker: ^1.0.7 + image_crop: + git: + url: https://github.com/kimaah/image_crop.git + lottie: ^3.1.0 + animations: ^2.0.11 + flutter_svg: ^2.0.10+1 + flutter_dynamic_icon: ^2.1.0 + # android_dynamic_icon: ^2.0.0 + i18n_extension: ^12.0.1 + http: ^1.2.0 + fl_chart: ^0.68.0 + flutter_dynamic_icon_plus: ^1.1.2 + share_plus: ^10.0.3 + path_provider: ^2.1.3 + file_picker: ^8.0.5 + +dev_dependencies: + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true