commit 336443eeaec403bb6a7517691d0a859c6bf8e050 Author: Varakh Date: Tue Feb 2 15:33:23 2021 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..554d913 --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +**/out/** + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +build/ +*.g.dart +flutter_export_environment.sh +.flutter-plugins-dependencies + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/**/gen/ + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..101035a --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: fbe11671a4b78a707d9856588d956c0d3c39a6d5 + channel: master + +project_type: app diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..53d1f3d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb9a4d0 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# README + +A mobile flutter app for [FileBin](https://github.com/Bluewind/filebin). + +## Getting Started + +This project is a starting point for a Flutter application. + +* In [Intellij](https://www.jetbrains.com/idea/) or [Android Studio](https://developer.android.com/studio/) (recommended), install the flutter and dart (dependency of flutter) plugins +* Set up Android SDK: + * Install via IDE in a folder of your choice (you probably want it not be to installed as superuser) + * Set the `ANDROID_HOME` variable so that IDE can detect it automatically + + ``` + export ANDROID_HOME="$HOME/.android_sdk" + export PATH=$PATH:$ANDROID_HOME/tools/bin + export PATH=$PATH:$ANDROID_HOME/platform-tools + ``` + +* Set up [flutter SDK](https://flutter.dev/docs/get-started/install), you probably want this also on your `PATH` like `ANDROID_HOME` + + ``` + export PATH="$HOME/.flutter_sdk/bin/:$PATH" + ``` + +* In the IDE, set the correct SDK paths (e.g. to flutter, dart, Android) + +Start by installing dependencies and generating entities! + +### Working versions for SDK + +``` +[✓] Flutter (Channel stable, 1.22.6, on Linux, locale en_US.UTF-8) +[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3) +``` + +## Dependencies +* Run `flutter packages pub get` in project root folder to get dependencies or open the `pubspec.yaml` and click on the buttons provided by the IDE plugins + +## JSON Serialization Files +Generate required entities by using these commands: + +* Build once: `flutter packages pub run build_runner build --delete-conflicting-outputs` +* File watcher: `flutter packages pub run build_runner watch` + +## Architecture + +* Views have models +* Model classes use services +* Service classes use data repositories +* Data repositories use the `Api` class + +Resulting data workflow: `USER -> view -> model -> service -> repository -> API`. + +* *NEVER* use services in views (except for `NavigationService` or `DialogService` if needed)! Create a model function instead. The UI is then fully decoupled from any business logic. +* *DON'T* swallow exceptions with `catch (_/e)` +* *ONLY* handle `ServiceException`! Other exceptions are serious errors and should therefore result in an error (or get caught by a global catcher) +* *ONLY* handle `ServiceException` in *model* classes to show the user a result +* *ALWAYS* use the `DialogService` (also possible from within a service/model) and *DON'T* create separate dialogs +* *ALWAYS* use the `NavigationService` for navigation and *NOT* `Navigation.of(...)` + +## Build & Release + +### Release with Fastlane + +You need [fastlane](https://fastlane.tools/) installed locally. Look at the initial setup on how to do that. + +Fastlane is used to manage the app store presentation and automatic uploading to the Play Store and +to the App Store. With fastlane you can do common tasks in a collaborative way, e.g. publishing +to or adapting the texts for the different stores from the commandline. + +Before using fastlane you need to properly setup signing, otherwise building will not work. + +Initial setup should already be done but this link helps for initial project setup: +[https://flutter.dev/docs/deployment/cd](https://flutter.dev/docs/deployment/cd). + +#### Signing & store access setup + +You need to setup individually on your machine for signing and afterwards publish an app for the +different platforms. + +##### Android + +* Extract the encrypted ZIP file from `_secrets/` into the same folder with GPG. Ask for the password if necessary. +* Copy `android/key.properties.example` to `android/key.properties` +* Adjust properties matching your setup and folder structure +* Point to the `_secrets/key.jks` you just extracted and ask for the store password. +* Copy the `api-xxx.json` file into `android/fastlane/` + +##### iOS + +fastlane's [match](https://docs.fastlane.tools/actions/match/) capability helps with Apple's +singing, secure keys, and profiles. Use `fastlane match` in the `ios` folder to download existing +profiles. They're stored in a separate git repository and are encrypted. + +You need access to the git repository in which those private files reside. + +#### Usage + +Go into the platform directory you want to build for, e.g. `ios/` or `android/` and then look into the +`Fastlane` file which lanes are present. Run a lane via `fastlane `, e.g. use the +following to build for Android `fastlane android build`. + +##### Android + +Use `fastlane android beta` to build and upload a new beta version to the Play Store. + +##### iOS + +For iOS you need to execute `fastlane ios build` before uploading to testflight with +`fastlane ios beta`. + +### Release manually (not recommended) + +See the following links on how to setup: +* [https://flutter.dev/docs/deployment/android](https://flutter.dev/docs/deployment/android) +* [https://flutter.dev/docs/deployment/ios](https://flutter.dev/docs/deployment/ios) + +To have a clean environment, when building please follow the steps precisely: + +* Clean your local setup with `flutter clean` +* Fetch dependencies with `flutter pub get` +* Generate model files with `flutter packages pub run build_runner build --delete-conflicting-outputs` +* Increase version in `pubspec.yaml` if needed or not already done +* Build Android and iOS apps + * For **Android** generate an app bundle with `flutter build appbundle` or `flutter build apk`. + * For **iOS** use `flutter build ios --release --no-codesign` + +### Debug + +You should use an emulator or real device and Android Studio's internal capability to communicate +and to deploy on it. If you want to build a plain debug version, ensure to have a clean environment +like mentioned above and then execute the following: + +``` +flutter build apk --debug +flutter build ios --debug +``` + +## Troubleshooting + +##### Seeing something like the following? Remove your `.gradle/` folder! + +``` +java.util.concurrent.ExecutionException: com.android.builder.internal.aapt.v2.Aapt2InternalException: AAPT2 aapt2-3.2.1-4818971-linux Daemon #0: Daemon startup failed +This should not happen under normal circumstances, please file an issue if it does. +``` + +##### Flutter problems? + +Ensure to be on the version mentioned above which should be in the stable branch. If everything +breaks, start from fresh via `flutter clean` and maybe re-do all necessary steps to get the app +working in the first place. \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..bc2100d --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,7 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..6f71423 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,60 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "de.varakh.fbmobile" + minSdkVersion 16 + targetSdkVersion 29 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..feb9edf --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1a53060 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/android/app/src/main/java/de/varakh/fbmobile/MainActivity.java b/android/app/src/main/java/de/varakh/fbmobile/MainActivity.java new file mode 100644 index 0000000..fb98e6e --- /dev/null +++ b/android/app/src/main/java/de/varakh/fbmobile/MainActivity.java @@ -0,0 +1,13 @@ +package de.varakh.fbmobile; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class MainActivity extends FlutterActivity { + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..515968c Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..6dd87e7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..b257020 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..4f352fc Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..ddf4746 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..feb9edf --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..205da3d --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.0.1' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/fastlane/buildAndroid.sh b/android/fastlane/buildAndroid.sh new file mode 100755 index 0000000..e6a08df --- /dev/null +++ b/android/fastlane/buildAndroid.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +cd ../../; +flutter clean && \ +flutter pub get && +flutter packages pub run build_runner build --delete-conflicting-outputs; + +flutter build appbundle; \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..38c8d45 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..493072b --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/assets/i18n/en.json b/assets/i18n/en.json new file mode 100644 index 0000000..0af637b --- /dev/null +++ b/assets/i18n/en.json @@ -0,0 +1,116 @@ +{ + "api": { + "forbidden": "You're not allowed to access this resource", + "bad_request": "Bad request: {reason}", + "socket_error": "Not a valid host or no internet connection", + "socket_timeout": "Request timed out - please ensure your internet connection is stable and that you're connecting to a valid FileBin instance and that it's not under heavy load", + "general_rest_error": "An unknown error occurred during communication", + "general_rest_error_payload": "An unknown error occurred during communication: {message}" + }, + "app": { + "title": "FileBin Mobile", + "unknown_error": "An unknown error occurred, please try again" + }, + "titles": { + "login": "Login", + "history": "History", + "profile": "Profile", + "about": "About", + "upload": "Upload" + }, + "tabs": { + "login": "Login", + "history": "History", + "profile": "Profile", + "upload": "Upload" + }, + "upload": { + "open_file_explorer": "Select...", + "clear_temporary_files": "Clear", + "text_to_be_pasted": "Text...", + "upload": "Upload", + "uploading_now": "Uploading...", + "file_explorer_open": "Selecting files...", + "multipaste": "multipaste", + "errors": { + "not_found": "Not found" + } + }, + "startup": { + "init": "Initializing...", + "start_services": "Starting services..." + }, + "login": { + "help": "Login", + "compatibility_dialog": { + "title": "How to login?", + "body": "A FileBin instance >= 3.5.0 and a valid API key with at least access-level 'apikey' is required." + }, + "url_placeholder": "https://paste.domain.tld", + "apikey_placeholder": "API key", + "button": "Login", + "errors": { + "empty_url": "Please provide a FileBin URL", + "no_protocol": "URLs need to include a valid protocol like http:// or https://", + "invalid_url": "Please provide a valid FileBin URL", + "empty_apikey": "Please provide an API key", + "wrong_credentials": "Credentials are invalid", + "forbidden": "You're not allowed to access this instance" + } + }, + "history": { + "no_items": "No pastes found", + "filename": "Filename", + "id": "ID", + "filesize": "Filesize", + "link": "Link", + "date": "Date", + "open_link": "Open in browser", + "delete": "Delete permanently", + "multipaste_element": "Included as multipaste item", + "errors": { + "not_found": "No pastes found" + }, + "delete_dialog": { + "title": "Are you sure?", + "description": "Paste '{id}' will be deleted permanently.", + "accept": "Yes", + "deny": "Rather not" + } + }, + "about": { + "headline": "Welcome to FileBin mobile!", + "description": "This application is a mobile client for FileBin and it's open source. It helps you to manage your pastes.\n\nIn order to use the application, you need access to a FileBin instance and an API key to sign in. It's recommended that the API key has at least 'apikey' access-level.", + "contact_us": "Feedback? Issues?", + "website": "Main application: https://github.com/Bluewind/filebin\n\nMobile: https://github.com/v4rakh/fbmobile" + }, + "profile": { + "welcome": "Hi!", + "connection": "You're currently connected to:\n\nURL: {url}", + "config": "Instance configuration:\n\nUpload max size: {uploadMaxSize}\n\nMax files per request: {maxFilesPerRequest}\n\nMax inputs vars: {maxInputVars}\n\nRequest max size: {requestMaxSize}", + "reveal_api_key": "Reveal API key", + "revealed_api_key": { + "title": "API key", + "description": "{apiKey}" + } + }, + "logout": { + "title": "Logout", + "confirm": "Are you sure?", + "yes": "Yes", + "no": "No" + }, + "link": { + "dialog": { + "title": "Link opening failed", + "description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types." + } + }, + "dialog": { + "confirm": "OK", + "cancel": "Cancel" + }, + "dev": { + "no_route": "No route defined for {route}" + } +} \ No newline at end of file diff --git a/assets/logo_caption.png b/assets/logo_caption.png new file mode 100644 index 0000000..0283fe4 Binary files /dev/null and b/assets/logo_caption.png differ diff --git a/assets/logo_caption.svg b/assets/logo_caption.svg new file mode 100644 index 0000000..9156e47 --- /dev/null +++ b/assets/logo_caption.svg @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6b4c0f7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..ca4bca0 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,2 @@ +target 'Runner' do + use_frameworks! \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bbcba27 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,518 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = de.varakh.fbmobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = de.varakh.fbmobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = de.varakh.fbmobile; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a28140c --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000..e712af3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..3c2d3e4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..40a3a40 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..8ea471c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000..af62b95 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..3d31b69 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..348a478 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..78152d3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..5e3dff3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..133172b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..109a320 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000..f776bd8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..87ebe0e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..2a075e9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..cbef96b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000..704fb67 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..39ce162 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..4401841 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..ac7cd7a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..65b74d7 --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..9618a80 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,54 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + FileBin + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UIBackgroundModes + + fetch + remote-notification + + NSAppleMusicUsageDescription + Allow to select music files and upload them via the app + NSPhotoLibraryUsageDescription + Allow to select photos and upload them via the app + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..7335fdf --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..f57cc34 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:flutter_translate/localization_provider.dart'; +import 'package:flutter_translate/localized_app.dart'; +import 'package:provider/provider.dart'; + +import 'core/manager/dialog_manager.dart'; +import 'core/manager/lifecycle_manager.dart'; +import 'core/models/session.dart'; +import 'core/services/dialog_service.dart'; +import 'core/services/navigation_service.dart'; +import 'core/services/session_service.dart'; +import 'locator.dart'; +import 'ui/app_router.dart'; +import 'ui/shared/app_colors.dart'; +import 'ui/views/startup_view.dart'; + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + var localizationDelegate = LocalizedApp.of(context).delegate; + + return LocalizationProvider( + state: LocalizationProvider.of(context).state, + child: StreamProvider( + initialData: Session.initial(), + create: (context) => locator().sessionController.stream, + child: LifeCycleManager( + child: MaterialApp( + title: translate('app.title'), + builder: (context, child) => Navigator( + key: locator().dialogNavigationKey, + onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)), + ), + theme: ThemeData( + brightness: Brightness.light, primarySwatch: primaryAccentColor, primaryColor: primaryAccentColor), + onGenerateRoute: AppRouter.generateRoute, + navigatorKey: locator().navigationKey, + home: StartUpView(), + supportedLocales: localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + )), + )); + } +} diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..7d42486 --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,4 @@ +class Constants { + static const int apiRequestTimeoutLimit = 8; + static const String apiUrlSuffix = '/api/v2.2.0'; +} diff --git a/lib/core/datamodels/dialog_request.dart b/lib/core/datamodels/dialog_request.dart new file mode 100644 index 0000000..44f0903 --- /dev/null +++ b/lib/core/datamodels/dialog_request.dart @@ -0,0 +1,13 @@ +class DialogRequest { + final String title; + final String description; + final String buttonTitleAccept; + final String buttonTitleDeny; + + DialogRequest({ + this.title, + this.description, + this.buttonTitleAccept, + this.buttonTitleDeny, + }); +} diff --git a/lib/core/datamodels/dialog_response.dart b/lib/core/datamodels/dialog_response.dart new file mode 100644 index 0000000..8143350 --- /dev/null +++ b/lib/core/datamodels/dialog_response.dart @@ -0,0 +1,7 @@ +class DialogResponse { + final bool confirmed; + + DialogResponse({ + this.confirmed, + }); +} diff --git a/lib/core/enums/error_code.dart b/lib/core/enums/error_code.dart new file mode 100644 index 0000000..d490724 --- /dev/null +++ b/lib/core/enums/error_code.dart @@ -0,0 +1,12 @@ +/// Enums for error codes +enum ErrorCode { + /// A generic error + GENERAL_ERROR, + + /// Errors related to connections + SOCKET_ERROR, + SOCKET_TIMEOUT, + + /// A REST error (response code wasn't 200 or 204) + REST_ERROR, +} diff --git a/lib/core/enums/viewstate.dart b/lib/core/enums/viewstate.dart new file mode 100644 index 0000000..7b60208 --- /dev/null +++ b/lib/core/enums/viewstate.dart @@ -0,0 +1 @@ +enum ViewState { Idle, Busy } diff --git a/lib/core/error/rest_service_exception.dart b/lib/core/error/rest_service_exception.dart new file mode 100644 index 0000000..18d02c5 --- /dev/null +++ b/lib/core/error/rest_service_exception.dart @@ -0,0 +1,14 @@ +import '../enums/error_code.dart'; +import 'service_exception.dart'; + +class RestServiceException extends ServiceException { + final int statusCode; + final dynamic responseBody; + + RestServiceException(this.statusCode, {this.responseBody, String message}) + : super(code: ErrorCode.REST_ERROR, message: message); + + String toString() { + return "$code $statusCode $message"; + } +} diff --git a/lib/core/error/service_exception.dart b/lib/core/error/service_exception.dart new file mode 100644 index 0000000..a190869 --- /dev/null +++ b/lib/core/error/service_exception.dart @@ -0,0 +1,12 @@ +import '../enums/error_code.dart'; + +class ServiceException implements Exception { + final ErrorCode code; + final String message; + + ServiceException({this.code = ErrorCode.GENERAL_ERROR, this.message = ''}); + + String toString() { + return "$code: $message"; + } +} diff --git a/lib/core/manager/dialog_manager.dart b/lib/core/manager/dialog_manager.dart new file mode 100644 index 0000000..f84637b --- /dev/null +++ b/lib/core/manager/dialog_manager.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import '../../locator.dart'; +import '../datamodels/dialog_request.dart'; +import '../datamodels/dialog_response.dart'; +import '../services/dialog_service.dart'; + +class DialogManager extends StatefulWidget { + final Widget child; + + DialogManager({Key key, this.child}) : super(key: key); + + _DialogManagerState createState() => _DialogManagerState(); +} + +class _DialogManagerState extends State { + DialogService _dialogService = locator(); + + @override + void initState() { + super.initState(); + _dialogService.registerDialogListener(_showDialog); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } + + void _showDialog(DialogRequest request) { + List actions = []; + + if (request.buttonTitleDeny != null && request.buttonTitleDeny.isNotEmpty) { + Widget denyBtn = FlatButton( + child: Text(request.buttonTitleDeny), + onPressed: () { + _dialogService.dialogComplete(DialogResponse(confirmed: false)); + }, + ); + actions.add(denyBtn); + } + + Widget confirmBtn = FlatButton( + child: Text(request.buttonTitleAccept), + onPressed: () { + _dialogService.dialogComplete(DialogResponse(confirmed: true)); + }, + ); + actions.add(confirmBtn); + + AlertDialog alert = AlertDialog( + title: Text(request.title), + content: Text(request.description), + actions: actions, + ); + + showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + ); + } +} diff --git a/lib/core/manager/lifecycle_manager.dart b/lib/core/manager/lifecycle_manager.dart new file mode 100644 index 0000000..d7a0a04 --- /dev/null +++ b/lib/core/manager/lifecycle_manager.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; + +import '../../core/services/session_service.dart'; +import '../../locator.dart'; +import '../services/stoppable_service.dart'; +import '../util/logger.dart'; + +/// Stop and start long running services +class LifeCycleManager extends StatefulWidget { + final Widget child; + + LifeCycleManager({Key key, this.child}) : super(key: key); + + _LifeCycleManagerState createState() => _LifeCycleManagerState(); +} + +class _LifeCycleManagerState extends State with WidgetsBindingObserver { + final Logger logger = getLogger(); + + List servicesToManage = [locator()]; + + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + super.dispose(); + WidgetsBinding.instance.removeObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + logger.d('LifeCycle event ${state.toString()}'); + super.didChangeAppLifecycleState(state); + + servicesToManage.forEach((service) { + if (state == AppLifecycleState.resumed) { + service.start(); + } else { + service.stop(); + } + }); + } +} diff --git a/lib/core/models/rest/config.dart b/lib/core/models/rest/config.dart new file mode 100644 index 0000000..4444344 --- /dev/null +++ b/lib/core/models/rest/config.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'config.g.dart'; + +@JsonSerializable() +class Config { + @JsonKey(name: "upload_max_size", required: true) + final num uploadMaxSize; + + @JsonKey(name: "max_files_per_request", required: true) + final num maxFilesPerRequest; + + @JsonKey(name: "max_input_vars", required: true) + final num maxInputVars; + + @JsonKey(name: "request_max_size", required: true) + final num requestMaxSize; + + Config({this.uploadMaxSize, this.maxFilesPerRequest, this.maxInputVars, this.requestMaxSize}); + + // JSON Init + factory Config.fromJson(Map json) => _$ConfigFromJson(json); + + // JSON Export + Map toJson() => _$ConfigToJson(this); +} diff --git a/lib/core/models/rest/config_response.dart b/lib/core/models/rest/config_response.dart new file mode 100644 index 0000000..014e58f --- /dev/null +++ b/lib/core/models/rest/config_response.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'config.dart'; + +part 'config_response.g.dart'; + +@JsonSerializable() +class ConfigResponse { + @JsonKey(required: true) + final String status; + + @JsonKey(required: true) + final Config data; + + ConfigResponse({this.status, this.data}); + + // JSON Init + factory ConfigResponse.fromJson(Map json) => _$ConfigResponseFromJson(json); + + // JSON Export + Map toJson() => _$ConfigResponseToJson(this); +} diff --git a/lib/core/models/rest/history.dart b/lib/core/models/rest/history.dart new file mode 100644 index 0000000..edf7a5e --- /dev/null +++ b/lib/core/models/rest/history.dart @@ -0,0 +1,26 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'history_item.dart'; +import 'history_multipaste_item.dart'; + +part 'history.g.dart'; + +@JsonSerializable() +class History { + @JsonKey(name: "items") + final Map items; + + @JsonKey(name: "multipaste_items") + final Map multipasteItems; + + @JsonKey(name: "total_size") + final String totalSize; + + History({this.items, this.multipasteItems, this.totalSize}); + + // JSON Init + factory History.fromJson(Map json) => _$HistoryFromJson(json); + + // JSON Export + Map toJson() => _$HistoryToJson(this); +} diff --git a/lib/core/models/rest/history_item.dart b/lib/core/models/rest/history_item.dart new file mode 100644 index 0000000..add2130 --- /dev/null +++ b/lib/core/models/rest/history_item.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'history_item.g.dart'; + +@JsonSerializable() +class HistoryItem { + @JsonKey(required: true) + final String id; + final String date; + final String filename; + final String filesize; + final String hash; + final String mimetype; + final String thumbnail; + + HistoryItem({this.date, this.filename, this.filesize, this.hash, this.id, this.mimetype, this.thumbnail}); + + // JSON Init + factory HistoryItem.fromJson(Map json) => _$HistoryItemFromJson(json); + + // JSON Export + Map toJson() => _$HistoryItemToJson(this); +} diff --git a/lib/core/models/rest/history_multipaste_item.dart b/lib/core/models/rest/history_multipaste_item.dart new file mode 100644 index 0000000..f0d9b24 --- /dev/null +++ b/lib/core/models/rest/history_multipaste_item.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'history_item.dart'; + +part 'history_multipaste_item.g.dart'; + +@JsonSerializable() +class HistoryMultipasteItem { + final String date; + final Map items; + + @JsonKey(name: "url_id") + final String urlId; + + HistoryMultipasteItem({this.date, this.items, this.urlId}); + + // JSON Init + factory HistoryMultipasteItem.fromJson(Map json) => _$HistoryMultipasteItemFromJson(json); + + // JSON Export + Map toJson() => _$HistoryMultipasteItemToJson(this); +} diff --git a/lib/core/models/rest/history_response.dart b/lib/core/models/rest/history_response.dart new file mode 100644 index 0000000..b891242 --- /dev/null +++ b/lib/core/models/rest/history_response.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'history.dart'; + +part 'history_response.g.dart'; + +@JsonSerializable() +class HistoryResponse { + @JsonKey(required: true) + final String status; + + @JsonKey(required: true) + final History data; + + HistoryResponse({this.status, this.data}); + + // JSON Init + factory HistoryResponse.fromJson(Map json) => _$HistoryResponseFromJson(json); + + // JSON Export + Map toJson() => _$HistoryResponseToJson(this); +} diff --git a/lib/core/models/rest/rest_error.dart b/lib/core/models/rest/rest_error.dart new file mode 100644 index 0000000..4c13200 --- /dev/null +++ b/lib/core/models/rest/rest_error.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'rest_error.g.dart'; + +@JsonSerializable() +class RestError { + final String status; + final String message; + @JsonKey(name: "error_id") + final String errorId; + + RestError({ + this.status, + this.message, + this.errorId, + }); // JSON Init + + factory RestError.fromJson(Map json) => _$RestErrorFromJson(json); + + // JSON Export + Map toJson() => _$RestErrorToJson(this); +} diff --git a/lib/core/models/rest/uploaded.dart b/lib/core/models/rest/uploaded.dart new file mode 100644 index 0000000..81b9e84 --- /dev/null +++ b/lib/core/models/rest/uploaded.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'uploaded.g.dart'; + +@JsonSerializable() +class Uploaded { + @JsonKey(required: true) + final List ids; + + @JsonKey(required: true) + final List urls; + + Uploaded({this.ids, this.urls}); + + // JSON Init + factory Uploaded.fromJson(Map json) => _$UploadedFromJson(json); + + // JSON Export + Map toJson() => _$UploadedToJson(this); +} diff --git a/lib/core/models/rest/uploaded_response.dart b/lib/core/models/rest/uploaded_response.dart new file mode 100644 index 0000000..1a63a1b --- /dev/null +++ b/lib/core/models/rest/uploaded_response.dart @@ -0,0 +1,22 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'uploaded.dart'; + +part 'uploaded_response.g.dart'; + +@JsonSerializable() +class UploadedResponse { + @JsonKey(required: true) + final String status; + + @JsonKey(required: true) + final Uploaded data; + + UploadedResponse({this.status, this.data}); + + // JSON Init + factory UploadedResponse.fromJson(Map json) => _$UploadedResponseFromJson(json); + + // JSON Export + Map toJson() => _$UploadedResponseToJson(this); +} diff --git a/lib/core/models/session.dart b/lib/core/models/session.dart new file mode 100644 index 0000000..8227b93 --- /dev/null +++ b/lib/core/models/session.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'rest/config.dart'; + +part 'session.g.dart'; + +@JsonSerializable() +class Session { + final String url; + final String apiKey; + final Config config; + + Session({this.url, this.apiKey, this.config}); + + Session.initial() + : url = '', + apiKey = '', + config = null; + + factory Session.fromJson(Map json) => _$SessionFromJson(json); + + Map toJson() => _$SessionToJson(this); +} diff --git a/lib/core/models/uploaded_paste.dart b/lib/core/models/uploaded_paste.dart new file mode 100644 index 0000000..4575fd9 --- /dev/null +++ b/lib/core/models/uploaded_paste.dart @@ -0,0 +1,33 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'uploaded_paste.g.dart'; + +@JsonSerializable() +class UploadedPaste { + final DateTime date; + final String filename; + final num filesize; + final String hash; + final String id; + final String mimetype; + final String thumbnail; + final bool isMulti; + final List items; + + UploadedPaste( + {this.date, + this.filename, + this.filesize, + this.hash, + this.id, + this.mimetype, + this.thumbnail, + this.isMulti, + this.items}); + + // JSON Init + factory UploadedPaste.fromJson(Map json) => _$UploadedPasteFromJson(json); + + // JSON Export + Map toJson() => _$UploadedPasteToJson(this); +} diff --git a/lib/core/repositories/file_repository.dart b/lib/core/repositories/file_repository.dart new file mode 100644 index 0000000..5363852 --- /dev/null +++ b/lib/core/repositories/file_repository.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; +import 'dart:io'; + +import '../../locator.dart'; +import '../models/rest/config.dart'; +import '../models/rest/config_response.dart'; +import '../models/rest/history.dart'; +import '../models/rest/history_response.dart'; +import '../models/rest/uploaded_response.dart'; +import '../services/api.dart'; + +class FileRepository { + Api _api = locator(); + + Future getHistory() async { + var response = await _api.post('/file/history'); + var parsedResponse = HistoryResponse.fromJson(json.decode(response.body)); + return parsedResponse.data; + } + + Future getConfig(String url) async { + _api.setUrl(url); + + var response = await _api.fetch('/file/get_config'); + var parsedResponse = ConfigResponse.fromJson(json.decode(response.body)); + return parsedResponse.data; + } + + Future delete(String id) async { + await _api.post('/file/delete', fields: {'ids[1]': id}); + } + + Future upload(List files, Map additionalFiles) async { + var response = await _api.post('/file/upload', files: files, additionalFiles: additionalFiles); + return UploadedResponse.fromJson(json.decode(response.body)); + } + + Future createMulti(List ids) async { + Map multiPasteIds = Map(); + + ids.forEach((element) { + multiPasteIds.putIfAbsent("ids[${ids.indexOf(element) + 1}]", () => element); + }); + + await _api.post('/file/create_multipaste', fields: multiPasteIds); + } +} diff --git a/lib/core/services/api.dart b/lib/core/services/api.dart new file mode 100644 index 0000000..4aaa1e9 --- /dev/null +++ b/lib/core/services/api.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:logger/logger.dart'; + +import '../../constants.dart'; +import '../../core/enums/error_code.dart'; +import '../../core/error/rest_service_exception.dart'; +import '../../core/error/service_exception.dart'; +import '../../core/services/api_error_converter.dart'; +import '../models/rest/rest_error.dart'; +import '../util/logger.dart'; + +class Api implements ApiErrorConverter { + final Logger _logger = getLogger(); + + static const String _errorNoConnection = 'No internet connection'; + static const String _errorTimeout = 'Request timed out'; + static const String _formDataApiKey = 'apikey'; + static const String _applicationJson = "application/json"; + + String _url = ""; + String _apiKey = ""; + + Map _headers = {"Content-Type": _applicationJson, "Accept": _applicationJson}; + Duration _timeout = Duration(seconds: Constants.apiRequestTimeoutLimit); + + Future fetch(String route) async { + try { + _logger + .d("Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'"); + var response = await http.get(_url + route, headers: _headers).timeout(_timeout); + handleRestErrors(response); + return response; + } on TimeoutException { + throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout); + } on SocketException { + throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection); + } + } + + Future post(String route, + {Map fields, List files, Map additionalFiles}) async { + try { + var uri = Uri.parse(_url + route); + var request = http.MultipartRequest('POST', uri) + ..headers['Content-Type'] = _applicationJson + ..headers["Accept"] = _applicationJson; + + if (_apiKey.isNotEmpty) { + request.fields[_formDataApiKey] = _apiKey; + } + + if (fields != null && fields.isNotEmpty) { + request.fields.addAll(fields); + } + + if (files != null && files.isNotEmpty) { + files.forEach((element) async { + request.files.add(await http.MultipartFile.fromPath('file[${files.indexOf(element) + 1}]', element.path)); + }); + } + + if (additionalFiles != null && additionalFiles.length > 0) { + List keys = additionalFiles.keys.toList(); + additionalFiles.forEach((key, value) { + var index = files != null ? files.length + keys.indexOf(key) + 1 : keys.indexOf(key) + 1; + request.files.add(http.MultipartFile.fromString('file[$index]', value, filename: key)); + }); + } + + _logger.d("Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files"); + var multiResponse = await request.send(); + var response = await http.Response.fromStream(multiResponse); + handleRestErrors(response); + return response; + } on TimeoutException { + throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout); + } on SocketException { + throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection); + } + } + + void setUrl(String url) { + _url = url + Constants.apiUrlSuffix; + } + + void removeUrl() { + _url = ""; + } + + void setTimeout(Duration timeout) { + _timeout = timeout; + } + + void addApiKeyAuthorization(apiKey) { + _apiKey = apiKey; + } + + void removeApiKeyAuthorization() { + _apiKey = ""; + } + + /// if there's a JSON response body in error case, the RestServiceException will + /// have a json decoded object. Replace this with a custom + /// conversion method by overwriting the interface if needed + void handleRestErrors(http.Response response) { + if (response != null) { + if (response.statusCode != HttpStatus.ok && response.statusCode != HttpStatus.noContent) { + if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) { + ContentType responseContentType = ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]); + + if (ContentType.json.primaryType == responseContentType.primaryType && + ContentType.json.subType == responseContentType.subType) { + var parsedBody = convert(response); + throw new RestServiceException(response.statusCode, responseBody: parsedBody); + } + } + + throw new RestServiceException(response.statusCode); + } + } + } + + @override + convert(http.Response response) { + return RestError.fromJson(json.decode(response.body)); + } +} diff --git a/lib/core/services/api_error_converter.dart b/lib/core/services/api_error_converter.dart new file mode 100644 index 0000000..e6e2115 --- /dev/null +++ b/lib/core/services/api_error_converter.dart @@ -0,0 +1,5 @@ +import 'package:http/http.dart'; + +class ApiErrorConverter { + dynamic convert(Response response) {} +} diff --git a/lib/core/services/dialog_service.dart b/lib/core/services/dialog_service.dart new file mode 100644 index 0000000..2769a50 --- /dev/null +++ b/lib/core/services/dialog_service.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../datamodels/dialog_request.dart'; +import '../datamodels/dialog_response.dart'; + +class DialogService { + GlobalKey _dialogNavigationKey = GlobalKey(); + Function(DialogRequest) _showDialogListener; + Completer _dialogCompleter; + + GlobalKey get dialogNavigationKey => _dialogNavigationKey; + + void registerDialogListener(Function(DialogRequest) showDialogListener) { + _showDialogListener = showDialogListener; + } + + Future showDialog({ + String title, + String description, + String buttonTitleAccept, + }) { + _dialogCompleter = Completer(); + _showDialogListener(DialogRequest( + title: title, + description: description, + buttonTitleAccept: + buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept)); + return _dialogCompleter.future; + } + + Future showConfirmationDialog( + {String title, String description, String buttonTitleAccept, String buttonTitleDeny}) { + _dialogCompleter = Completer(); + _showDialogListener(DialogRequest( + title: title, + description: description, + buttonTitleAccept: + buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept, + buttonTitleDeny: + buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny)); + return _dialogCompleter.future; + } + + void dialogComplete(DialogResponse response) { + _dialogNavigationKey.currentState.pop(); + _dialogCompleter.complete(response); + _dialogCompleter = null; + } +} diff --git a/lib/core/services/file_service.dart b/lib/core/services/file_service.dart new file mode 100644 index 0000000..9eca1c2 --- /dev/null +++ b/lib/core/services/file_service.dart @@ -0,0 +1,29 @@ +import 'dart:async'; +import 'dart:io'; + +import '../../core/repositories/file_repository.dart'; +import '../../locator.dart'; + +class FileService { + final FileRepository _fileRepository = locator(); + + Future getConfig(String url) async { + return await _fileRepository.getConfig(url); + } + + Future getHistory() async { + return await _fileRepository.getHistory(); + } + + Future deletePaste(String id) async { + return await _fileRepository.delete(id); + } + + Future upload(List files, Map additionalFiles) async { + return await _fileRepository.upload(files, additionalFiles); + } + + Future createMulti(List ids) async { + return await _fileRepository.createMulti(ids); + } +} diff --git a/lib/core/services/link_service.dart b/lib/core/services/link_service.dart new file mode 100644 index 0000000..5bbeff1 --- /dev/null +++ b/lib/core/services/link_service.dart @@ -0,0 +1,23 @@ +import 'package:flutter_translate/global.dart'; +import 'package:logger/logger.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../core/services/dialog_service.dart'; +import '../../core/util/logger.dart'; +import '../../locator.dart'; + +class LinkService { + final Logger _logger = getLogger(); + final DialogService _dialogService = locator(); + + Future open(String link) async { + if (await canLaunch(link)) { + await launch(link); + } else { + _logger.e('Could not launch link $link'); + _dialogService.showDialog( + title: translate('link.dialog.title'), + description: translate('link.dialog.description', args: {'link': link})); + } + } +} diff --git a/lib/core/services/navigation_service.dart b/lib/core/services/navigation_service.dart new file mode 100644 index 0000000..6a0d948 --- /dev/null +++ b/lib/core/services/navigation_service.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; + +import '../util/logger.dart'; + +class NavigationService { + GlobalKey _navigationKey = GlobalKey(); + + GlobalKey get navigationKey => _navigationKey; + + final Logger logger = getLogger(); + + void pop() { + logger.d('NavigationService: pop'); + _navigationKey.currentState.pop(); + } + + Future navigateTo(String routeName, {dynamic arguments}) { + logger.d('NavigationService: navigateTo $routeName'); + return _navigationKey.currentState.pushNamed(routeName, arguments: arguments); + } + + Future navigateAndReplaceTo(String routeName, {dynamic arguments}) { + logger.d('NavigationService: navigateAndReplaceTo $routeName'); + return _navigationKey.currentState.pushReplacementNamed(routeName, arguments: arguments); + } +} diff --git a/lib/core/services/session_service.dart b/lib/core/services/session_service.dart new file mode 100644 index 0000000..760b364 --- /dev/null +++ b/lib/core/services/session_service.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:logger/logger.dart'; + +import '../../core/services/stoppable_service.dart'; +import '../../locator.dart'; +import '../models/rest/config.dart'; +import '../models/session.dart'; +import '../services/storage_service.dart'; +import '../util/logger.dart'; +import 'api.dart'; + +class SessionService extends StoppableService { + final Logger _logger = getLogger(); + final StorageService _storageService = locator(); + final Api _api = locator(); + + StreamController sessionController = StreamController(); + + Future login(String url, String apiKey, Config config) async { + _api.setUrl(url); + _api.addApiKeyAuthorization(apiKey); + + var session = new Session(url: url, apiKey: apiKey, config: config); + sessionController.add(session); + await _storageService.storeSession(session); + _logger.d('Session created'); + return true; + } + + Future logout() async { + _api.removeApiKeyAuthorization(); + _api.removeUrl(); + + sessionController.add(null); + _logger.d('Session destroyed'); + return await _storageService.removeSession(); + } + + Future restoreSession() async { + bool hasSession = await _storageService.hasSession(); + + if (hasSession) { + Session session = await _storageService.retrieveSession(); + + _api.setUrl(session.url); + _api.addApiKeyAuthorization(session.apiKey); + + sessionController.add(session); + _logger.d('Session restored'); + } + } + + @override + Future start() async { + super.start(); + await restoreSession(); + _logger.d('SessionService started'); + } + + @override + void stop() { + super.stop(); + _logger.d('SessionService stopped'); + } +} diff --git a/lib/core/services/stoppable_service.dart b/lib/core/services/stoppable_service.dart new file mode 100644 index 0000000..52df89e --- /dev/null +++ b/lib/core/services/stoppable_service.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +abstract class StoppableService { + bool _serviceStopped = false; + + bool get serviceStopped => _serviceStopped; + + @mustCallSuper + void stop() { + _serviceStopped = true; + } + + @mustCallSuper + void start() { + _serviceStopped = false; + } +} diff --git a/lib/core/services/storage_service.dart b/lib/core/services/storage_service.dart new file mode 100644 index 0000000..5a7f597 --- /dev/null +++ b/lib/core/services/storage_service.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../models/session.dart'; + +class StorageService { + static const _SESSION_KEY = 'session'; + static const _LAST_URL_KEY = 'last_url'; + + Future storeLastUrl(String url) { + return _store(_LAST_URL_KEY, url); + } + + Future retrieveLastUrl() async { + return await _retrieve(_LAST_URL_KEY); + } + + Future hasLastUrl() async { + return await _exists(_LAST_URL_KEY); + } + + Future storeSession(Session session) { + return _store(_SESSION_KEY, json.encode(session)); + } + + Future retrieveSession() async { + var retrieve = await _retrieve(_SESSION_KEY); + return Session.fromJson(json.decode(retrieve)); + } + + Future hasSession() { + return _exists(_SESSION_KEY); + } + + Future removeSession() { + return _remove(_SESSION_KEY); + } + + Future _exists(String key) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.containsKey(key); + } + + Future _remove(String key) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.remove(key); + } + + Future _retrieve(String key) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.getString(key); + } + + Future _store(String key, String value) async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + return prefs.setString(key, value); + } +} diff --git a/lib/core/util/formatter_util.dart b/lib/core/util/formatter_util.dart new file mode 100644 index 0000000..d88f126 --- /dev/null +++ b/lib/core/util/formatter_util.dart @@ -0,0 +1,18 @@ +import 'dart:math'; + +import 'package:intl/intl.dart'; + +class FormatterUtil { + /// Format epoch timestamp + static String formatEpoch(num millis) { + DateFormat dateFormat = DateFormat().add_yMEd().add_Hm(); + return dateFormat.format(DateTime.fromMillisecondsSinceEpoch(millis)); + } + + static String formatBytes(int bytes, int decimals) { + if (bytes <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + var i = (log(bytes) / log(1024)).floor(); + return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i]; + } +} diff --git a/lib/core/util/logger.dart b/lib/core/util/logger.dart new file mode 100644 index 0000000..7e14797 --- /dev/null +++ b/lib/core/util/logger.dart @@ -0,0 +1,9 @@ +import 'package:logger/logger.dart'; + +void setupLogger(Level level) { + Logger.level = level; +} + +Logger getLogger() { + return Logger(printer: SimplePrinter(colors: false)); +} diff --git a/lib/core/viewmodels/about_model.dart b/lib/core/viewmodels/about_model.dart new file mode 100644 index 0000000..4d1c88c --- /dev/null +++ b/lib/core/viewmodels/about_model.dart @@ -0,0 +1,11 @@ +import '../../core/services/link_service.dart'; +import '../../locator.dart'; +import 'base_model.dart'; + +class AboutModel extends BaseModel { + final LinkService _linkService = locator(); + + void openLink(String link) { + _linkService.open(link); + } +} diff --git a/lib/core/viewmodels/base_model.dart b/lib/core/viewmodels/base_model.dart new file mode 100644 index 0000000..6cf9627 --- /dev/null +++ b/lib/core/viewmodels/base_model.dart @@ -0,0 +1,40 @@ +import 'package:flutter/widgets.dart'; +import 'package:logger/logger.dart'; + +import '../../core/util/logger.dart'; +import '../enums/viewstate.dart'; + +class BaseModel extends ChangeNotifier { + final Logger _logger = getLogger(); + + bool _isDisposed = false; + + ViewState _state = ViewState.Idle; + String _stateMessage; + + ViewState get state => _state; + + String get stateMessage => _stateMessage; + + void setState(ViewState viewState) { + _state = viewState; + if (!_isDisposed) { + notifyListeners(); + _logger.d("Notified state change '${viewState.toString()}'"); + } + } + + void setStateMessage(String stateMessage) { + _stateMessage = stateMessage; + if (!_isDisposed) { + notifyListeners(); + _logger.d("Notified state message change '$stateMessage'"); + } + } + + @override + void dispose() { + super.dispose(); + _isDisposed = true; + } +} diff --git a/lib/core/viewmodels/history_model.dart b/lib/core/viewmodels/history_model.dart new file mode 100644 index 0000000..220c06e --- /dev/null +++ b/lib/core/viewmodels/history_model.dart @@ -0,0 +1,151 @@ +import 'dart:io'; + +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:logger/logger.dart'; + +import '../../locator.dart'; +import '../datamodels/dialog_response.dart'; +import '../enums/error_code.dart'; +import '../enums/viewstate.dart'; +import '../error/rest_service_exception.dart'; +import '../error/service_exception.dart'; +import '../models/rest/history.dart'; +import '../models/rest/rest_error.dart'; +import '../models/uploaded_paste.dart'; +import '../services/dialog_service.dart'; +import '../services/file_service.dart'; +import '../services/link_service.dart'; +import '../util/logger.dart'; +import 'base_model.dart'; + +class HistoryModel extends BaseModel { + final Logger _logger = getLogger(); + final FileService _fileService = locator(); + final LinkService _linkService = locator(); + final DialogService _dialogService = locator(); + + String errorMessage; + + List pastes = []; + + Future getHistory() async { + setState(ViewState.Busy); + + try { + pastes.clear(); + History _history = await _fileService.getHistory(); + if (_history.items != null) { + _history.items.forEach((key, value) { + var millisecondsSinceEpoch = int.parse(value.date) * 1000; + pastes.add( + UploadedPaste( + id: key, + date: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch), + filename: value.filename, + filesize: int.parse(value.filesize), + hash: value.hash, + mimetype: value.mimetype, + isMulti: false, + items: [], + thumbnail: value.thumbnail), + ); + }); + } + + if (_history.multipasteItems != null) { + _history.multipasteItems.forEach((key, multiPaste) { + var millisecondsSinceEpoch = int.parse(multiPaste.date) * 1000; + pastes.add(UploadedPaste( + id: key, + date: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch), + isMulti: true, + items: multiPaste.items.entries.map((e) => e.value.id).toList())); + }); + } + + pastes.sort((a, b) => a.date.compareTo(b.date)); + + errorMessage = null; + } catch (e) { + if (e is RestServiceException) { + if (e.statusCode == HttpStatus.notFound) { + errorMessage = translate('history.errors.not_found'); + } else if (e.statusCode == HttpStatus.forbidden) { + errorMessage = translate('api.forbidden'); + } else if (e.statusCode != HttpStatus.notFound && + e.statusCode != HttpStatus.forbidden && + e.responseBody is RestError && + e.responseBody.message != null) { + if (e.statusCode == HttpStatus.badRequest) { + errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); + } else { + errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); + } + } else { + errorMessage = translate('api.general_rest_error'); + } + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { + errorMessage = translate('api.socket_error'); + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { + errorMessage = translate('api.socket_timeout'); + } else { + errorMessage = translate('app.unknown_error'); + setState(ViewState.Idle); + _logger.e('An unknown error occurred', e); + throw e; + } + } + + setState(ViewState.Idle); + } + + Future deletePaste(String id) async { + DialogResponse res = await _dialogService.showConfirmationDialog( + title: translate('history.delete_dialog.title'), + description: translate('history.delete_dialog.description', args: {'id': id}), + buttonTitleAccept: translate('history.delete_dialog.accept'), + buttonTitleDeny: translate('history.delete_dialog.deny')); + + if (!res.confirmed) { + return; + } + + setState(ViewState.Busy); + + try { + await _fileService.deletePaste(id); + await getHistory(); + errorMessage = null; + } catch (e) { + if (e is RestServiceException) { + if (e.statusCode == HttpStatus.notFound) { + errorMessage = translate('project.errors.not_found'); + } else if (e.statusCode == HttpStatus.forbidden) { + errorMessage = translate('api.forbidden'); + } else if (e.statusCode != HttpStatus.notFound && + e.statusCode != HttpStatus.forbidden && + e.responseBody is RestError && + e.responseBody.message != null) { + errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); + } else { + errorMessage = translate('api.general_rest_error'); + } + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { + errorMessage = translate('api.socket_error'); + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { + errorMessage = translate('api.socket_timeout'); + } else { + errorMessage = translate('app.unknown_error'); + setState(ViewState.Idle); + _logger.e('An unknown error occurred', e); + throw e; + } + } + + setState(ViewState.Idle); + } + + void openLink(String link) { + _linkService.open(link); + } +} diff --git a/lib/core/viewmodels/home_model.dart b/lib/core/viewmodels/home_model.dart new file mode 100644 index 0000000..fc140e8 --- /dev/null +++ b/lib/core/viewmodels/home_model.dart @@ -0,0 +1,3 @@ +import 'base_model.dart'; + +class HomeModel extends BaseModel {} diff --git a/lib/core/viewmodels/login_model.dart b/lib/core/viewmodels/login_model.dart new file mode 100644 index 0000000..f2eadc9 --- /dev/null +++ b/lib/core/viewmodels/login_model.dart @@ -0,0 +1,119 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:logger/logger.dart'; +import 'package:validators/sanitizers.dart'; +import 'package:validators/validators.dart'; + +import '../../core/services/file_service.dart'; +import '../../core/services/session_service.dart'; +import '../../core/services/storage_service.dart'; +import '../../locator.dart'; +import '../enums/error_code.dart'; +import '../enums/viewstate.dart'; +import '../error/rest_service_exception.dart'; +import '../error/service_exception.dart'; +import '../models/rest/config.dart'; +import '../util/logger.dart'; +import 'base_model.dart'; + +class LoginModel extends BaseModel { + TextEditingController _uriController = new TextEditingController(); + final TextEditingController _apiKeyController = new TextEditingController(); + + TextEditingController get uriController => _uriController; + + TextEditingController get apiKeyController => _apiKeyController; + + final SessionService _sessionService = locator(); + final StorageService _storageService = locator(); + final FileService _configService = locator(); + final Logger _logger = getLogger(); + + String errorMessage; + + void init() async { + bool hasLastUrl = await _storageService.hasLastUrl(); + + if (hasLastUrl) { + setState(ViewState.Busy); + var s = await _storageService.retrieveLastUrl(); + + if (s.isNotEmpty) { + _uriController = new TextEditingController(text: s); + } + + setState(ViewState.Idle); + } + } + + Future login(String url, String apiKey) async { + setState(ViewState.Busy); + + url = trim(url); + + if (url.isEmpty) { + errorMessage = translate('login.errors.empty_url'); + setState(ViewState.Idle); + return false; + } + + if (!url.contains("https://") && !url.contains("http://")) { + errorMessage = translate('login.errors.no_protocol'); + setState(ViewState.Idle); + return false; + } + + bool validUri = Uri.parse(url).isAbsolute; + if (!validUri || !isURL(url)) { + errorMessage = translate('login.errors.invalid_url'); + setState(ViewState.Idle); + return false; + } + + if (apiKey.isEmpty) { + errorMessage = translate('login.errors.empty_apikey'); + setState(ViewState.Idle); + return false; + } + + var success = false; + try { + Config config = await _configService.getConfig(url); + success = await _sessionService.login(url, apiKey, config); + errorMessage = null; + } catch (e) { + if (e is RestServiceException) { + if (e.statusCode == HttpStatus.unauthorized) { + errorMessage = translate('login.errors.wrong_credentials'); + } else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) { + errorMessage = translate('login.errors.forbidden'); + } else if (e.statusCode == HttpStatus.notFound) { + errorMessage = translate('api.incompatible_error_not_found'); + } else { + errorMessage = translate('api.general_rest_error'); + } + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { + errorMessage = translate('api.socket_error'); + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { + errorMessage = translate('api.socket_timeout'); + } else { + errorMessage = translate('app.unknown_error'); + _sessionService.logout(); + setState(ViewState.Idle); + _logger.e('An unknown error occurred', e); + throw e; + } + + if (errorMessage.isNotEmpty) { + _sessionService.logout(); + } + + setState(ViewState.Idle); + return success; + } + + return success; + } +} diff --git a/lib/core/viewmodels/profile_model.dart b/lib/core/viewmodels/profile_model.dart new file mode 100644 index 0000000..0868160 --- /dev/null +++ b/lib/core/viewmodels/profile_model.dart @@ -0,0 +1,32 @@ +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../core/services/session_service.dart'; +import '../../locator.dart'; +import '../services/dialog_service.dart'; +import '../services/link_service.dart'; +import 'base_model.dart'; + +class ProfileModel extends BaseModel { + final SessionService _sessionService = locator(); + final DialogService _dialogService = locator(); + final LinkService _linkService = locator(); + + Future logout() async { + var dialogResult = await _dialogService.showConfirmationDialog( + title: translate('logout.title'), description: translate('logout.confirm')); + + if (dialogResult.confirmed) { + await _sessionService.logout(); + } + } + + Future revealApiKey(String apiKey) async { + await _dialogService.showDialog( + title: translate('profile.revealed_api_key.title'), + description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey})); + } + + void openLink(String link) { + _linkService.open(link); + } +} diff --git a/lib/core/viewmodels/startup_model.dart b/lib/core/viewmodels/startup_model.dart new file mode 100644 index 0000000..e8a2a91 --- /dev/null +++ b/lib/core/viewmodels/startup_model.dart @@ -0,0 +1,27 @@ +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../locator.dart'; +import '../../ui/views/home_view.dart'; +import '../enums/viewstate.dart'; +import '../services/navigation_service.dart'; +import '../services/session_service.dart'; +import 'base_model.dart'; + +class StartUpViewModel extends BaseModel { + final SessionService _sessionService = locator(); + final NavigationService _navigationService = locator(); + + Future handleStartUpLogic() async { + setState(ViewState.Busy); + setStateMessage(translate('startup.init')); + await Future.delayed(Duration(milliseconds: 500)); + + setStateMessage(translate('startup.start_services')); + await _sessionService.start(); + await Future.delayed(Duration(milliseconds: 500)); + + _navigationService.navigateAndReplaceTo(HomeView.routeName); + + setState(ViewState.Idle); + } +} diff --git a/lib/core/viewmodels/upload_model.dart b/lib/core/viewmodels/upload_model.dart new file mode 100644 index 0000000..3b80fd1 --- /dev/null +++ b/lib/core/viewmodels/upload_model.dart @@ -0,0 +1,142 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:logger/logger.dart'; + +import '../../locator.dart'; +import '../enums/error_code.dart'; +import '../enums/viewstate.dart'; +import '../error/rest_service_exception.dart'; +import '../error/service_exception.dart'; +import '../models/rest/rest_error.dart'; +import '../models/rest/uploaded_response.dart'; +import '../services/file_service.dart'; +import '../util/logger.dart'; +import 'base_model.dart'; + +class UploadModel extends BaseModel { + final Logger _logger = getLogger(); + final FileService _fileService = locator(); + TextEditingController _pasteTextController = TextEditingController(); + bool createMulti = false; + + String fileName; + List paths; + String _extension; + bool loadingPath = false; + String errorMessage; + + TextEditingController get pasteTextController => _pasteTextController; + + void toggleCreateMulti() { + setState(ViewState.Busy); + createMulti = !createMulti; + setState(ViewState.Idle); + } + + void openFileExplorer() async { + setState(ViewState.Busy); + setStateMessage(translate('upload.file_explorer_open')); + loadingPath = true; + + try { + paths = (await FilePicker.platform.pickFiles( + type: FileType.any, + allowMultiple: true, + withData: false, + withReadStream: true, + allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null, + )) + ?.files; + } on PlatformException catch (e) { + _logger.e('Unsupported operation', e); + } catch (ex) { + _logger.e('An unknown error occurred', ex); + } + + loadingPath = false; + fileName = paths != null ? paths.map((e) => e.name).toString() : '...'; + + setStateMessage(null); + setState(ViewState.Idle); + } + + void clearCachedFiles() async { + setState(ViewState.Busy); + await FilePicker.platform.clearTemporaryFiles(); + paths = null; + fileName = null; + errorMessage = null; + setState(ViewState.Idle); + } + + void upload() async { + setState(ViewState.Busy); + setStateMessage(translate('upload.uploading_now')); + + try { + List files; + Map additionalFiles; + + if (pasteTextController.text != null && pasteTextController.text.isNotEmpty) { + additionalFiles = Map.from( + {'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text}); + } + + if (paths != null && paths.length > 0) { + files = paths.map((e) => new File(e.path)).toList(); + } + + UploadedResponse response = await _fileService.upload(files, additionalFiles); + + if (createMulti && response.data.ids.length > 1) { + await _fileService.createMulti(response.data.ids); + } + + clearCachedFiles(); + _pasteTextController.clear(); + errorMessage = null; + } catch (e) { + if (e is RestServiceException) { + if (e.statusCode == HttpStatus.notFound) { + errorMessage = translate('upload.errors.not_found'); + } else if (e.statusCode == HttpStatus.forbidden) { + errorMessage = translate('api.forbidden'); + } else if (e.statusCode != HttpStatus.notFound && + e.statusCode != HttpStatus.forbidden && + e.responseBody is RestError && + e.responseBody.message != null) { + if (e.statusCode == HttpStatus.badRequest) { + errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); + } else { + errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); + } + } else { + errorMessage = translate('api.general_rest_error'); + } + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { + errorMessage = translate('api.socket_error'); + } else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { + errorMessage = translate('api.socket_timeout'); + } else { + errorMessage = translate('app.unknown_error'); + setStateMessage(null); + setState(ViewState.Idle); + _logger.e('An unknown error occurred', e); + throw e; + } + } + + setStateMessage(null); + setState(ViewState.Idle); + } + + @override + void dispose() { + _pasteTextController.dispose(); + super.dispose(); + } +} diff --git a/lib/locator.dart b/lib/locator.dart new file mode 100644 index 0000000..ab7c88b --- /dev/null +++ b/lib/locator.dart @@ -0,0 +1,45 @@ +import 'package:get_it/get_it.dart'; + +import 'core/repositories/file_repository.dart'; +import 'core/services/api.dart'; +import 'core/services/dialog_service.dart'; +import 'core/services/file_service.dart'; +import 'core/services/link_service.dart'; +import 'core/services/navigation_service.dart'; +import 'core/services/session_service.dart'; +import 'core/services/storage_service.dart'; +import 'core/viewmodels/about_model.dart'; +import 'core/viewmodels/history_model.dart'; +import 'core/viewmodels/home_model.dart'; +import 'core/viewmodels/login_model.dart'; +import 'core/viewmodels/profile_model.dart'; +import 'core/viewmodels/startup_model.dart'; +import 'core/viewmodels/upload_model.dart'; + +GetIt locator = GetIt.instance; + +void setupLocator() { + /// app helper services + locator.registerLazySingleton(() => NavigationService()); + locator.registerLazySingleton(() => StorageService()); + locator.registerLazySingleton(() => DialogService()); + + /// api + data repositories + locator.registerLazySingleton(() => Api()); + + locator.registerLazySingleton(() => FileRepository()); + + /// services + locator.registerLazySingleton(() => SessionService()); + locator.registerLazySingleton(() => FileService()); + locator.registerLazySingleton(() => LinkService()); + + /// view models + locator.registerFactory(() => StartUpViewModel()); + locator.registerFactory(() => LoginModel()); + locator.registerFactory(() => AboutModel()); + locator.registerFactory(() => HomeModel()); + locator.registerFactory(() => UploadModel()); + locator.registerFactory(() => HistoryModel()); + locator.registerFactory(() => ProfileModel()); +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..9ab230d --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:logger/logger.dart'; + +import 'app.dart'; +import 'core/util/logger.dart'; +import 'locator.dart'; + +/// main entry point used to configure log level, locales, ... +void main() async { + setupLogger(Level.info); +// setupLogger(Level.debug); + setupLocator(); + + var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en']); + + runApp(LocalizedApp(delegate, MyApp())); +} diff --git a/lib/ui/app_router.dart b/lib/ui/app_router.dart new file mode 100644 index 0000000..35bc7ce --- /dev/null +++ b/lib/ui/app_router.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../ui/views/tabbar_container_view.dart'; +import 'views/about_view.dart'; +import 'views/home_view.dart'; +import 'views/login_view.dart'; +import 'views/profile_view.dart'; +import 'views/startup_view.dart'; + +const String initialRoute = "login"; + +class AppRouter { + static Route generateRoute(RouteSettings settings) { + switch (settings.name) { + case StartUpView.routeName: + return MaterialPageRoute(builder: (_) => StartUpView()); + case AboutView.routeName: + return MaterialPageRoute(builder: (_) => AboutView()); + case HomeView.routeName: + return MaterialPageRoute(builder: (_) => TabBarContainerView()); + case LoginView.routeName: + return MaterialPageRoute(builder: (_) => LoginView()); + case ProfileView.routeName: + return MaterialPageRoute(builder: (_) => ProfileView()); + default: + return MaterialPageRoute( + builder: (_) => Scaffold( + body: Center( + child: Text(translate('dev.no_route', args: {'route': settings.name})), + ), + )); + } + } +} diff --git a/lib/ui/shared/app_colors.dart b/lib/ui/shared/app_colors.dart new file mode 100644 index 0000000..f390fe8 --- /dev/null +++ b/lib/ui/shared/app_colors.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +const Color backgroundColor = Colors.white; + +/// Colors +const Color primaryBackgroundColor = Colors.white; + +const Map colors = { + 50: Color.fromRGBO(63, 69, 75, .1), + 100: Color.fromRGBO(63, 69, 75, .2), + 200: Color.fromRGBO(63, 69, 75, .3), + 300: Color.fromRGBO(63, 69, 75, .4), + 400: Color.fromRGBO(63, 69, 75, .5), + 500: Color.fromRGBO(63, 69, 75, .6), + 600: Color.fromRGBO(63, 69, 75, .7), + 700: Color.fromRGBO(63, 69, 75, .8), + 800: Color.fromRGBO(63, 69, 75, .9), + 900: Color.fromRGBO(63, 69, 75, 1), +}; +const MaterialColor myColor = MaterialColor(0xFF3F454B, colors); +const Color primaryAccentColor = myColor; +const Color buttonBackgroundColor = primaryAccentColor; +const Color buttonForegroundColor = Colors.white; diff --git a/lib/ui/shared/text_styles.dart b/lib/ui/shared/text_styles.dart new file mode 100644 index 0000000..d7d2139 --- /dev/null +++ b/lib/ui/shared/text_styles.dart @@ -0,0 +1,4 @@ +import 'package:flutter/material.dart'; + +const headerStyle = TextStyle(fontSize: 35, fontWeight: FontWeight.w900); +const subHeaderStyle = TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500); diff --git a/lib/ui/shared/ui_helpers.dart b/lib/ui/shared/ui_helpers.dart new file mode 100644 index 0000000..cbf8ed6 --- /dev/null +++ b/lib/ui/shared/ui_helpers.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class UIHelper { + static const double _VerticalSpaceSmall = 10.0; + static const double _VerticalSpaceMedium = 20.0; + static const double _VerticalSpaceLarge = 60.0; + + static const double _HorizontalSpaceSmall = 10.0; + static const double _HorizontalSpaceMedium = 20.0; + static const double HorizontalSpaceLarge = 60.0; + + /// Returns a vertical space with height set to [_VerticalSpaceSmall] + static Widget verticalSpaceSmall() { + return verticalSpace(_VerticalSpaceSmall); + } + + /// Returns a vertical space with height set to [_VerticalSpaceMedium] + static Widget verticalSpaceMedium() { + return verticalSpace(_VerticalSpaceMedium); + } + + /// Returns a vertical space with height set to [_VerticalSpaceLarge] + static Widget verticalSpaceLarge() { + return verticalSpace(_VerticalSpaceLarge); + } + + /// Returns a vertical space equal to the [height] supplied + static Widget verticalSpace(double height) { + return Container(height: height); + } + + /// Returns a vertical space with height set to [_HorizontalSpaceSmall] + static Widget horizontalSpaceSmall() { + return horizontalSpace(_HorizontalSpaceSmall); + } + + /// Returns a vertical space with height set to [_HorizontalSpaceMedium] + static Widget horizontalSpaceMedium() { + return horizontalSpace(_HorizontalSpaceMedium); + } + + /// Returns a vertical space with height set to [HorizontalSpaceLarge] + static Widget horizontalSpaceLarge() { + return horizontalSpace(HorizontalSpaceLarge); + } + + /// Returns a vertical space equal to the [width] supplied + static Widget horizontalSpace(double width) { + return Container(width: width); + } +} diff --git a/lib/ui/views/about_view.dart b/lib/ui/views/about_view.dart new file mode 100644 index 0000000..d8d03b3 --- /dev/null +++ b/lib/ui/views/about_view.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../core/enums/viewstate.dart'; +import '../../core/viewmodels/about_model.dart'; +import '../../ui/shared/text_styles.dart'; +import '../../ui/shared/ui_helpers.dart'; +import '../shared/app_colors.dart'; +import '../widgets/my_appbar.dart'; +import 'base_view.dart'; + +class AboutView extends StatelessWidget { + static const routeName = '/about'; + + @override + Widget build(BuildContext context) { + final logo = Hero( + tag: 'hero', + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 96.0, + child: Image.asset('assets/logo_caption.png'), + ), + ); + + return BaseView( + builder: (context, model, child) => Scaffold( + appBar: MyAppBar( + title: Text(translate('titles.about')), + enableAbout: false, + ), + backgroundColor: backgroundColor, + body: model.state == ViewState.Busy + ? Center(child: CircularProgressIndicator()) + : Container( + padding: EdgeInsets.all(0), + child: ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + Center(child: logo), + Center( + child: Text( + translate(('about.description')), + )), + UIHelper.verticalSpaceMedium(), + Center( + child: Text( + translate(('about.contact_us')), + style: subHeaderStyle, + )), + UIHelper.verticalSpaceSmall(), + Center( + child: Linkify( + text: translate('about.website'), + options: LinkifyOptions(humanize: false), + onOpen: (link) => model.openLink(link.url), + ), + ) + ], + ))), + ); + } +} diff --git a/lib/ui/views/base_view.dart b/lib/ui/views/base_view.dart new file mode 100644 index 0000000..6e9d805 --- /dev/null +++ b/lib/ui/views/base_view.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:logger/logger.dart'; +import 'package:provider/provider.dart'; + +import '../../core/util/logger.dart'; +import '../../core/viewmodels/base_model.dart'; +import '../../locator.dart'; + +class BaseView extends StatefulWidget { + final Widget Function(BuildContext context, T model, Widget child) builder; + final Function(T) onModelReady; + + BaseView({this.builder, this.onModelReady}); + + @override + _BaseViewState createState() => _BaseViewState(); +} + +class _BaseViewState extends State> { + final Logger _logger = getLogger(); + + T model = locator(); + + @override + void initState() { + if (widget.onModelReady != null) { + widget.onModelReady(model); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider(create: (context) => model, child: Consumer(builder: widget.builder)); + } + + @override + void dispose() { + Type type = T; + _logger.d("Disposing '${type.toString()}'"); + super.dispose(); + } +} diff --git a/lib/ui/views/history_view.dart b/lib/ui/views/history_view.dart new file mode 100644 index 0000000..34ba876 --- /dev/null +++ b/lib/ui/views/history_view.dart @@ -0,0 +1,158 @@ +import 'package:expandable/expandable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:provider/provider.dart'; +import 'package:share/share.dart'; + +import '../../core/enums/viewstate.dart'; +import '../../core/models/session.dart'; +import '../../core/util/formatter_util.dart'; +import '../../core/viewmodels/history_model.dart'; +import '../../ui/widgets/centered_error_row.dart'; +import '../shared/app_colors.dart'; +import '../widgets/my_appbar.dart'; +import 'base_view.dart'; + +class HistoryView extends StatelessWidget { + static const routeName = '/history'; + + @override + Widget build(BuildContext context) { + var url = Provider.of(context).url; + + return BaseView( + onModelReady: (model) => model.getHistory(), + builder: (context, model, child) => Scaffold( + appBar: MyAppBar(title: Text(translate('titles.history'))), + backgroundColor: backgroundColor, + body: model.state == ViewState.Busy + ? Center(child: CircularProgressIndicator()) + : (model.errorMessage == null + ? Container( + padding: EdgeInsets.all(0), + child: RefreshIndicator(onRefresh: () => model.getHistory(), child: _render(model, url))) + : Container( + padding: EdgeInsets.all(25), + child: CenteredErrorRow( + model.errorMessage, + retryCallback: () => model.getHistory(), + )))), + ); + } + + Widget _renderOpenInBrowser(HistoryModel model, String url) { + return IconButton( + icon: Icon(Icons.open_in_new, color: Colors.blue, textDirection: TextDirection.ltr), + onPressed: () { + return model.openLink(url); + }); + } + + Widget _render(HistoryModel model, String url) { + List cards = List(); + + if (model.pastes.length > 0) { + model.pastes.reversed.forEach((paste) { + List widgets = []; + + var fullPasteUrl = '$url/${paste.id}'; + var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl); + + var dateWidget = ListTile( + title: Text(FormatterUtil.formatEpoch(paste.date.millisecondsSinceEpoch)), + subtitle: Text(translate('history.date')), + ); + + var linkWidget = ListTile( + title: Text(translate('history.open_link')), + trailing: openInBrowserButton, + ); + + var deleteWidget = ListTile( + title: Text(translate('history.delete')), + trailing: IconButton( + icon: Icon(Icons.delete, color: Colors.red), + onPressed: () { + return model.deletePaste(paste.id); + })); + + if (!paste.isMulti) { + var titleWidget = ListTile( + title: Text(paste.filename ?? paste.id), + subtitle: Text(translate('history.filename')), + ); + var fileSizeWidget = ListTile( + title: Text(FormatterUtil.formatBytes(paste.filesize, 2)), + subtitle: Text(translate('history.filesize')), + ); + var idWidget = ListTile( + title: Text(paste.id), + subtitle: Text(translate('history.id')), + ); + + widgets.add(titleWidget); + + widgets.add(fileSizeWidget); + widgets.add(idWidget); + } else { + paste.items.forEach((element) { + widgets.add(ListTile( + title: Text(element), + subtitle: Text(translate('history.multipaste_element')), + trailing: _renderOpenInBrowser(model, '$url/$element'), + )); + }); + } + + widgets.add(dateWidget); + widgets.add(linkWidget); + widgets.add(deleteWidget); + + var expandable = ExpandableTheme( + data: ExpandableThemeData( + iconColor: Colors.blue, + tapHeaderToExpand: true, + iconPlacement: ExpandablePanelIconPlacement.right, + headerAlignment: ExpandablePanelHeaderAlignment.center, + hasIcon: true, + ), + child: ExpandablePanel( + header: Text( + paste.id, + textAlign: TextAlign.left, + style: TextStyle(color: primaryAccentColor), + ), + expanded: Column( + mainAxisSize: MainAxisSize.min, + children: widgets, + ), + ), + ); + + cards.add(Card( + child: ListTile( + title: expandable, + trailing: IconButton( + icon: Icon(Icons.share, color: Colors.blue, textDirection: TextDirection.ltr), + onPressed: () { + return Share.share(fullPasteUrl); + }), + subtitle: Text(!paste.isMulti ? paste.filename : ''), + ), + )); + }); + } else { + cards.add(Card( + child: ListTile( + title: Text(translate('history.no_items')), + ), + )); + } + + return ListView( + padding: const EdgeInsets.all(8), + children: cards, + physics: AlwaysScrollableScrollPhysics(), + ); + } +} diff --git a/lib/ui/views/home_view.dart b/lib/ui/views/home_view.dart new file mode 100644 index 0000000..816027b --- /dev/null +++ b/lib/ui/views/home_view.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../core/enums/viewstate.dart'; +import '../../core/viewmodels/home_model.dart'; +import '../shared/app_colors.dart'; +import '../widgets/my_appbar.dart'; +import 'base_view.dart'; + +class HomeView extends StatelessWidget { + static const routeName = '/home'; + + @override + Widget build(BuildContext context) { + return BaseView( + builder: (context, model, child) => Scaffold( + appBar: MyAppBar(title: Text(translate('app.title'))), + backgroundColor: backgroundColor, + body: model.state == ViewState.Busy ? Center(child: CircularProgressIndicator()) : Container()), + ); + } +} diff --git a/lib/ui/views/login_view.dart b/lib/ui/views/login_view.dart new file mode 100644 index 0000000..30f67cd --- /dev/null +++ b/lib/ui/views/login_view.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../core/enums/viewstate.dart'; +import '../../core/services/dialog_service.dart'; +import '../../core/services/navigation_service.dart'; +import '../../core/viewmodels/login_model.dart'; +import '../../locator.dart'; +import '../../ui/shared/text_styles.dart'; +import '../../ui/views/home_view.dart'; +import '../../ui/widgets/my_appbar.dart'; +import '../shared/app_colors.dart'; +import '../widgets/login_header.dart'; +import 'base_view.dart'; + +class LoginView extends StatefulWidget { + static const routeName = '/login'; + + @override + _LoginViewState createState() => _LoginViewState(); +} + +class _LoginViewState extends State { + final NavigationService _navigationService = locator(); + final DialogService _dialogService = locator(); + + @override + Widget build(BuildContext context) { + final logo = Hero( + tag: 'hero', + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 96.0, + child: Image.asset('assets/logo_caption.png'), + ), + ); + + return BaseView( + onModelReady: (model) => model.init(), + builder: (context, model, child) => Scaffold( + appBar: MyAppBar(title: Text(translate('titles.login'))), + backgroundColor: backgroundColor, + body: model.state == ViewState.Busy + ? Center(child: CircularProgressIndicator()) + : ListView( + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.0, right: 24.0), + children: [ + Center(child: logo), + Center( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.center, + children: [ + Text( + translate('login.help'), + style: subHeaderStyle, + ), + InkWell( + child: Icon(Icons.help, color: buttonBackgroundColor), + onTap: () { + _dialogService.showDialog( + title: translate('login.compatibility_dialog.title'), + description: translate('login.compatibility_dialog.body')); + }, + ) + ])), + LoginHeaders( + validationMessage: model.errorMessage, + uriController: model.uriController, + apiKeyController: model.apiKeyController, + ), + RaisedButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + padding: EdgeInsets.all(12), + color: primaryAccentColor, + child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)), + onPressed: () async { + var loginSuccess = await model.login(model.uriController.text, model.apiKeyController.text); + if (loginSuccess) { + _navigationService.navigateAndReplaceTo(HomeView.routeName); + } + }, + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart new file mode 100644 index 0000000..609bb50 --- /dev/null +++ b/lib/ui/views/profile_view.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:provider/provider.dart'; + +import '../../core/enums/viewstate.dart'; +import '../../core/models/session.dart'; +import '../../core/util/formatter_util.dart'; +import '../../core/viewmodels/profile_model.dart'; +import '../shared/app_colors.dart'; +import '../shared/text_styles.dart'; +import '../shared/ui_helpers.dart'; +import '../widgets/my_appbar.dart'; +import 'base_view.dart'; + +class ProfileView extends StatelessWidget { + static const routeName = '/profile'; + + @override + Widget build(BuildContext context) { + var url = Provider.of(context).url; + var apiKey = Provider.of(context).apiKey; + var config = Provider.of(context).config; + + return BaseView( + builder: (context, model, child) => Scaffold( + appBar: MyAppBar(title: Text(translate('titles.profile'))), + floatingActionButton: FloatingActionButton( + heroTag: "logoutButton", + child: Icon(Icons.exit_to_app), + backgroundColor: primaryAccentColor, + onPressed: () { + model.logout(); + }, + ), + backgroundColor: backgroundColor, + body: model.state == ViewState.Busy + ? Center(child: CircularProgressIndicator()) + : ListView( + children: [ + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + translate('profile.welcome'), + style: headerStyle, + ), + ), + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Linkify( + onOpen: (link) => model.openLink(link.url), + text: translate('profile.connection', args: {'url': url}), + options: LinkifyOptions(humanize: false), + )), + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0), + child: RaisedButton.icon( + icon: Icon(Icons.remove_red_eye, color: Colors.blue), + label: Text( + translate('profile.reveal_api_key'), + style: TextStyle(color: buttonForegroundColor), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + color: primaryAccentColor, + onPressed: () { + return model.revealApiKey(apiKey); + })), + UIHelper.verticalSpaceMedium(), + Padding( + padding: const EdgeInsets.only(left: 25.0), + child: Text( + translate('profile.config', args: { + 'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize, 2), + 'maxFilesPerRequest': config.maxFilesPerRequest, + 'maxInputVars': config.maxInputVars, + 'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize, 2) + }), + )), + ], + )), + ); + } +} diff --git a/lib/ui/views/startup_view.dart b/lib/ui/views/startup_view.dart new file mode 100644 index 0000000..a5e2e64 --- /dev/null +++ b/lib/ui/views/startup_view.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:provider_architecture/provider_architecture.dart'; + +import '../../core/enums/viewstate.dart'; +import '../../core/viewmodels/startup_model.dart'; + +class StartUpView extends StatelessWidget { + static const routeName = '/'; + + @override + Widget build(BuildContext context) { + return ViewModelProvider.withConsumer( + viewModelBuilder: () => StartUpViewModel(), + onModelReady: (model) => model.handleStartUpLogic(), + builder: (context, model, child) => Scaffold( + backgroundColor: Colors.white, + body: model.state == ViewState.Busy + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircularProgressIndicator(), + (model.stateMessage.isNotEmpty ? Text(model.stateMessage) : Container()) + ])) + : Container())); + } +} diff --git a/lib/ui/views/tabbar_anonymous.dart b/lib/ui/views/tabbar_anonymous.dart new file mode 100644 index 0000000..bd82b15 --- /dev/null +++ b/lib/ui/views/tabbar_anonymous.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../shared/app_colors.dart'; +import 'login_view.dart'; + +class AnonymousTabBarView extends StatefulWidget { + @override + AnonymousTabBarState createState() => AnonymousTabBarState(); +} + +class AnonymousTabBarState extends State with SingleTickerProviderStateMixin { + TabController _tabController; + int _currentTabIndex = 0; + + List _realPages = [LoginView()]; + List _tabPages = [LoginView()]; + List _hasInit = [true]; + + List _tabsButton = [Tab(icon: Icon(Icons.person_outline), text: translate('tabs.login'))]; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: _realPages.length, vsync: this) + ..addListener(() { + int selectedIndex = _tabController.index; + if (_currentTabIndex != selectedIndex) { + if (!_hasInit[selectedIndex]) { + _tabPages[selectedIndex] = _realPages[selectedIndex]; + _hasInit[selectedIndex] = true; + } + setState(() => _currentTabIndex = selectedIndex); + } + }); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack(index: _currentTabIndex, children: _tabPages), + bottomNavigationBar: BottomAppBar( + child: TabBar( + labelColor: primaryAccentColor, + tabs: _tabsButton, + controller: _tabController, + ), + ), + ); + } +} diff --git a/lib/ui/views/tabbar_authenticated.dart b/lib/ui/views/tabbar_authenticated.dart new file mode 100644 index 0000000..6867463 --- /dev/null +++ b/lib/ui/views/tabbar_authenticated.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../shared/app_colors.dart'; +import 'history_view.dart'; +import 'profile_view.dart'; +import 'upload_view.dart'; + +class AuthenticatedTabBarView extends StatefulWidget { + @override + AuthenticatedTabBarState createState() => AuthenticatedTabBarState(); +} + +class AuthenticatedTabBarState extends State with SingleTickerProviderStateMixin { + TabController _tabController; + int _currentTabIndex = 0; + + List _realPages = [UploadView(), HistoryView(), ProfileView()]; + List _tabPages = [ + UploadView(), + Container(), + Container(), + ]; + List _hasInit = [true, false, false]; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: _realPages.length, vsync: this) + ..addListener(() { + int selectedIndex = _tabController.index; + if (_currentTabIndex != selectedIndex) { + if (!_hasInit[selectedIndex]) { + _tabPages[selectedIndex] = _realPages[selectedIndex]; + _hasInit[selectedIndex] = true; + } + setState(() => _currentTabIndex = selectedIndex); + } + }); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + double width = MediaQuery.of(context).size.width; + double yourWidth = width / 3; + double yourHeight = 70; + + List _tabsButton = [ + Container( + width: yourWidth, + height: yourHeight, + alignment: Alignment.center, + child: Tab(icon: Icon(Icons.upload_rounded), text: translate('tabs.upload'))), + Container( + width: yourWidth, + height: yourHeight, + alignment: Alignment.center, + child: Tab(icon: Icon(Icons.history), text: translate('tabs.history'))), + Container( + width: yourWidth, + height: yourHeight, + alignment: Alignment.center, + child: Tab(icon: Icon(Icons.person), text: translate('tabs.profile'))), + ]; + + return Scaffold( + body: IndexedStack(index: _currentTabIndex, children: _tabPages), + bottomNavigationBar: BottomAppBar( + child: TabBar( + indicatorSize: TabBarIndicatorSize.label, + labelColor: primaryAccentColor, + labelPadding: EdgeInsets.all(0), + tabs: _tabsButton, + isScrollable: true, + controller: _tabController, + )), + ); + } +} diff --git a/lib/ui/views/tabbar_container_view.dart b/lib/ui/views/tabbar_container_view.dart new file mode 100644 index 0000000..294431a --- /dev/null +++ b/lib/ui/views/tabbar_container_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../core/models/session.dart'; +import 'tabbar_anonymous.dart'; +import 'tabbar_authenticated.dart'; + +class TabBarContainerView extends StatelessWidget { + @override + Widget build(BuildContext context) { + Session currentSession = Provider.of(context); + bool isAuthenticated = currentSession != null && currentSession.apiKey.isNotEmpty; + + if (isAuthenticated) { + return AuthenticatedTabBarView(); + } + + return AnonymousTabBarView(); + } +} diff --git a/lib/ui/views/upload_view.dart b/lib/ui/views/upload_view.dart new file mode 100644 index 0000000..4c558e4 --- /dev/null +++ b/lib/ui/views/upload_view.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../core/enums/viewstate.dart'; +import '../../core/viewmodels/upload_model.dart'; +import '../shared/app_colors.dart'; +import '../widgets/centered_error_row.dart'; +import '../widgets/my_appbar.dart'; +import 'base_view.dart'; + +class UploadView extends StatelessWidget { + static const routeName = '/upload'; + + @override + Widget build(BuildContext context) { + return BaseView( + builder: (context, model, child) => Scaffold( + appBar: MyAppBar(title: Text(translate('titles.upload'))), + backgroundColor: backgroundColor, + body: model.state == ViewState.Busy + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircularProgressIndicator(), + (model.stateMessage != null && model.stateMessage.isNotEmpty + ? Text(model.stateMessage) + : Container()) + ])) + : ListView(children: [ + Padding( + padding: const EdgeInsets.only(left: 25.0, right: 25.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), + child: TextFormField( + minLines: 1, + maxLines: 7, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.text_snippet, + color: buttonBackgroundColor, + ), + suffixIcon: IconButton( + onPressed: () => model.pasteTextController.clear(), + icon: Icon(Icons.clear), + ), + hintText: translate('upload.text_to_be_pasted'), + contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)), + ), + controller: model.pasteTextController)), + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + RaisedButton.icon( + icon: Icon(Icons.file_copy_sharp, color: Colors.blue), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + color: primaryAccentColor, + onPressed: () => model.openFileExplorer(), + label: Text( + translate('upload.open_file_explorer'), + style: TextStyle(color: buttonForegroundColor), + )), + RaisedButton.icon( + icon: Icon(Icons.cancel, color: Colors.orange), + onPressed: model.paths != null && model.paths.length > 0 + ? () => model.clearCachedFiles() + : null, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + color: primaryAccentColor, + label: Text( + translate('upload.clear_temporary_files'), + style: TextStyle(color: buttonForegroundColor), + )), + ], + )), + Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + RaisedButton.icon( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + color: primaryAccentColor, + onPressed: () => model.upload(), + icon: Icon(Icons.upload_rounded, color: Colors.green), + label: Text( + translate('upload.upload'), + style: TextStyle(color: buttonForegroundColor), + )), + Row( + children: [ + Checkbox( + value: model.createMulti, + onChanged: (v) => model.toggleCreateMulti(), + ), + Text(translate('upload.multipaste')), + ], + ) + ])), + model.errorMessage != null && model.errorMessage.isNotEmpty + ? (Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), + child: CenteredErrorRow(model.errorMessage))) + : Container(), + Builder( + builder: (BuildContext context) => model.loadingPath + ? Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: const CircularProgressIndicator(), + ) + : model.paths != null + ? Container( + padding: const EdgeInsets.only(bottom: 30.0), + height: MediaQuery.of(context).size.height * 0.50, + child: ListView.separated( + itemCount: + model.paths != null && model.paths.isNotEmpty ? model.paths.length : 1, + itemBuilder: (BuildContext context, int index) { + final bool isMultiPath = model.paths != null && model.paths.isNotEmpty; + final String name = (isMultiPath + ? model.paths.map((e) => e.name).toList()[index] + : model.fileName ?? '...'); + final path = model.paths.length > 0 + ? model.paths.map((e) => e.path).toList()[index].toString() + : ''; + + return Card( + child: ListTile( + title: Text( + name, + ), + subtitle: Text(path), + )); + }, + separatorBuilder: (BuildContext context, int index) => const Divider(), + ), + ) + : Container(), + ), + ], + )) + ]))); + } +} diff --git a/lib/ui/widgets/about_iconbutton.dart b/lib/ui/widgets/about_iconbutton.dart new file mode 100644 index 0000000..57a39aa --- /dev/null +++ b/lib/ui/widgets/about_iconbutton.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import '../../core/services/navigation_service.dart'; +import '../../locator.dart'; +import '../../ui/views/about_view.dart'; + +class AboutIconButton extends StatelessWidget { + AboutIconButton(); + + final NavigationService _navigationService = locator(); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(Icons.help), + color: Colors.white, + onPressed: () { + _navigationService.navigateTo(AboutView.routeName); + }); + } +} diff --git a/lib/ui/widgets/centered_error_row.dart b/lib/ui/widgets/centered_error_row.dart new file mode 100644 index 0000000..35ea040 --- /dev/null +++ b/lib/ui/widgets/centered_error_row.dart @@ -0,0 +1,45 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../shared/app_colors.dart'; + +class CenteredErrorRow extends StatelessWidget { + final Function retryCallback; + final String message; + + CenteredErrorRow(this.message, {this.retryCallback}); + + @override + Widget build(BuildContext context) { + if (message == null) { + return Container(); + } + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: Center(child: Text(message, style: TextStyle(color: Colors.red)))), + ], + ), + (retryCallback != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: IconButton( + icon: Icon(Icons.refresh), + color: primaryAccentColor, + onPressed: () { + retryCallback(); + }, + )) + ]) + : Container()) + ], + ); + } +} diff --git a/lib/ui/widgets/login_header.dart b/lib/ui/widgets/login_header.dart new file mode 100644 index 0000000..64f329b --- /dev/null +++ b/lib/ui/widgets/login_header.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +class LoginHeaders extends StatelessWidget { + final TextEditingController uriController; + final TextEditingController apiKeyController; + + final String validationMessage; + + LoginHeaders({@required this.uriController, @required this.apiKeyController, this.validationMessage}); + + @override + Widget build(BuildContext context) { + return Column(children: [ + this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: Colors.red)) : Container(), + LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link), + keyboardType: TextInputType.url), + LoginTextField(apiKeyController, translate('login.apikey_placeholder'), Icon(Icons.vpn_key), obscureText: true), + ]); + } +} + +class LoginTextField extends StatelessWidget { + final TextEditingController controller; + final String placeHolder; + final TextInputType keyboardType; + final bool obscureText; + final Widget prefixIcon; + + LoginTextField(this.controller, this.placeHolder, this.prefixIcon, + {this.keyboardType = TextInputType.text, this.obscureText = false}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10.0), + margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), + height: 50.0, + alignment: Alignment.centerLeft, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10.0)), + child: TextFormField( + keyboardType: keyboardType, + obscureText: obscureText, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () => controller.clear(), + icon: Icon(Icons.clear), + ), + prefixIcon: prefixIcon, + hintText: placeHolder, + contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)), + ), + controller: controller), + ); + } +} diff --git a/lib/ui/widgets/my_appbar.dart b/lib/ui/widgets/my_appbar.dart new file mode 100644 index 0000000..a047ec1 --- /dev/null +++ b/lib/ui/widgets/my_appbar.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import '../widgets/about_iconbutton.dart'; + +class MyAppBar extends AppBar { + static final List aboutEnabledWidgets = [AboutIconButton()]; + static final List aboutDisabledWidgets = []; + + MyAppBar({Key key, Widget title, List actionWidgets, bool enableAbout = true}) + : super(key: key, title: Row(children: [title]), actions: _renderIconButtons(actionWidgets, enableAbout)); + + static List _renderIconButtons(List actionWidgets, bool aboutEnabled) { + if (actionWidgets == null) { + actionWidgets = []; + } + + List widgets = [...actionWidgets]; + + if (aboutEnabled) { + widgets.add(AboutIconButton()); + } + + return widgets; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..6170b6a --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,88 @@ +name: fbmobile +description: A mobile client for FileBin. + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: 1.0.0 + flutter_localizations: + sdk: flutter + flutter_translate: 1.6.0 + provider: 4.3.3 + provider_architecture: 1.1.1+1 + get_it: 3.1.0 # major changes in 4.x + logger: 0.9.4 + shared_preferences: 0.5.12+4 + http: 0.12.2 + json_annotation: 3.1.1 + validators: 2.0.0+1 + flutter_linkify: 4.0.2 + url_launcher: 5.7.2 + expandable: 4.1.4 + share: 0.6.5+4 + file_picker: 2.1.6 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: 1.10.2 + built_value_generator: 7.1.0 + json_serializable: 3.5.1 + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/ + - assets/i18n/ + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages \ No newline at end of file