diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml new file mode 100644 index 0000000..c3edbb9 --- /dev/null +++ b/.idea/libraries/Dart_SDK.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/KotlinJavaRuntime.xml b/.idea/libraries/KotlinJavaRuntime.xml new file mode 100644 index 0000000..2b96ac4 --- /dev/null +++ b/.idea/libraries/KotlinJavaRuntime.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..451feaa --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/runConfigurations/main_dart.xml b/.idea/runConfigurations/main_dart.xml new file mode 100644 index 0000000..aab7b5c --- /dev/null +++ b/.idea/runConfigurations/main_dart.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..5b3388c --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..b95fa4d --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# 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: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: web + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + 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 +. \ No newline at end of file diff --git a/README.md b/README.md index 77d56c9..91222e3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ -# DMZJ_F -动漫之家Flutter版本,尝试将动漫之家进行复活的项目 \ No newline at end of file +

动漫之家Flutter

+ +

+使用Flutter编写的动漫之家跨平台第三方客户端,动漫之家的复活项目 +

+ +## 支持平台 + +- [x] Android +- [x] Windows `Beta` +- [x] Linux `Beta` diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..60f33c1 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,91 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('zai_key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace "com.akasei.zmhf" + compileSdk 36 + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.akasei.zmhf" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { + debug { + if (keystorePropertiesFile.exists()) { + signingConfig signingConfigs.release + } + } + profile { + if (keystorePropertiesFile.exists()) { + signingConfig signingConfigs.release + } + } + release { + if (keystorePropertiesFile.exists()) { + signingConfig signingConfigs.release + } + } + } +} + +flutter { + source '../..' +} + +dependencies { +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..f69fdde --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b661eeb --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/akasei/zmhf/MainActivity.kt b/android/app/src/main/kotlin/com/akasei/zmhf/MainActivity.kt new file mode 100644 index 0000000..3e7d693 --- /dev/null +++ b/android/app/src/main/kotlin/com/akasei/zmhf/MainActivity.kt @@ -0,0 +1,5 @@ +package com.akasei.zmhf + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + 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..c67c537 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-ldpi/ic_launcher.png b/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 0000000..0c588d1 Binary files /dev/null and b/android/app/src/main/res/mipmap-ldpi/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..dca07f4 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..a5a4b45 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..61ecc46 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..d06170b 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/playstore-icon.png b/android/app/src/main/res/playstore-icon.png new file mode 100644 index 0000000..c4cd210 Binary files /dev/null and b/android/app/src/main/res/playstore-icon.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..af6500e --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,19 @@ + + + + + + + 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..91313ba --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..2439f15 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..f69fdde --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..bc157bd --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +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..9162f10 --- /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-8.11.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..9e2daa1 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.9.1" apply false + id "org.jetbrains.kotlin.android" version "2.0.0" apply false +} + +include ":app" diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..36083d8 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/logo_dmzj.png b/assets/images/logo_dmzj.png new file mode 100644 index 0000000..21731fb Binary files /dev/null and b/assets/images/logo_dmzj.png differ diff --git a/assets/images/vip.png b/assets/images/vip.png new file mode 100644 index 0000000..f4639a5 Binary files /dev/null and b/assets/images/vip.png differ diff --git a/assets/images/vip_chapter.png b/assets/images/vip_chapter.png new file mode 100644 index 0000000..e293ffa Binary files /dev/null and b/assets/images/vip_chapter.png differ diff --git a/assets/images/vip_comic.png b/assets/images/vip_comic.png new file mode 100644 index 0000000..7e6cff7 Binary files /dev/null and b/assets/images/vip_comic.png differ diff --git a/assets/lotties/empty.json b/assets/lotties/empty.json new file mode 100644 index 0000000..36507ad --- /dev/null +++ b/assets/lotties/empty.json @@ -0,0 +1 @@ +{"v":"4.7.0","fr":25,"ip":0,"op":50,"w":120,"h":120,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ruoi","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0,"y":0},"n":"0p833_0p833_0_0","t":0,"s":[57.361,61.016,0],"e":[57.699,41.796,0],"to":[-4.67500305175781,-4.12800598144531,0],"ti":[-13.9099960327148,5.27300262451172,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10.219,"s":[57.699,41.796,0],"e":[79.084,33.982,0],"to":[12.8159942626953,-4.85800170898438,0],"ti":[-4.54498291015625,3.73400115966797,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.445,"s":[79.084,33.982,0],"e":[59.691,9.121,0],"to":[6.61601257324219,-5.43799591064453,0],"ti":[20.0290069580078,1.20700073242188,0]},{"t":35}]},"a":{"a":0,"k":[60.531,10.945,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.994,0],[0,-0.994],[0.995,0],[0,0.994]],"o":[[0.995,0],[0,0.994],[-0.994,0],[0,-0.994]],"v":[[-0.001,-1.801],[1.801,-0.001],[-0.001,1.801],[-1.801,-0.001]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.4235294117647059,0.4235294117647059,0.4235294117647059,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[62.4,13.144],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.422,0],[0,-1.422],[1.421,0],[0,1.422]],"o":[[1.421,0],[0,1.422],[-1.422,0],[0,-1.422]],"v":[[0.001,-2.574],[2.574,0],[0.001,2.574],[-2.574,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.4235294117647059,0.4235294117647059,0.4235294117647059,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[64.145,9.606],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.996,0],[0,-1.996],[1.996,0],[0,1.996]],"o":[[1.996,0],[0,1.996],[-1.996,0],[0,-1.996]],"v":[[0,-3.614],[3.614,0],[0,3.614],[-3.614,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.4235294117647059,0.4235294117647059,0.4235294117647059,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[57.957,10.552],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"tr","p":{"a":0,"k":[60.531,10.941],"ix":2},"a":{"a":0,"k":[60.531,10.941],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"ruoi","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[-0.75,-0.75,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-13.91,5.273],[-4.545,3.734],[20.029,1.207]],"o":[[-4.675,-4.128],[12.816,-4.858],[6.616,-5.438],[0,0]],"v":[[-7.383,24.76],[-7.046,5.54],[14.34,-2.273],[-3.178,-24.76]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.34901960784313724,0.3686274509803922,0.4,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1},"lc":2,"lj":2,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":2.028}},{"n":"g","nm":"gap","v":{"a":0,"k":2.028}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[67.87,37.631],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.953]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p953_0p167_0p033"],"t":0,"s":[0],"e":[100]},{"t":35}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"im_emptyBox Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[60,60,0]},"a":{"a":0,"k":[60,60,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-0.001,-16.607],[-32.143,-0.002],[-0.001,16.607],[32.144,-0.002]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8,0.82,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,55.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.856,-23.249],[0,-16.605],[-12.857,-23.249],[-45,-6.641],[-32.144,0.001],[-45,6.645],[-12.857,23.249],[0,16.609],[12.856,23.249],[45,6.645],[32.143,0.001],[45,-6.641]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9372549019607843,0.9372549019607843,0.9372549019607843,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,55.748],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-16.072,24.171],[16.072,11.312],[16.072,-24.171],[-16.072,-24.171]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.9529411764705882,0.9529411764705882,0.9529411764705882,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[76.072,83.33],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-32.143,-24.171],[-32.143,11.311],[-0.001,24.171],[32.144,11.311],[32.144,-24.171]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8,0.82,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,83.33],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group"},{"ty":"tr","p":{"a":0,"k":[60,60.186],"ix":2},"a":{"a":0,"k":[60,60.186],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1}]} \ No newline at end of file diff --git a/assets/lotties/error.json b/assets/lotties/error.json new file mode 100644 index 0000000..400e7a6 --- /dev/null +++ b/assets/lotties/error.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":29.9700012207031,"ip":0,"op":301.000012259981,"w":1080,"h":900,"nm":"Composition 1","ddd":0,"assets":[{"id":"comp_0","nm":"Nuage","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[54,54],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-22,-239],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 12","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[544.675,315.475,0],"ix":2,"l":2},"a":{"a":0,"k":[-54.5,-226.5,0],"ix":1,"l":2},"s":{"a":0,"k":[80,80,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[31,31],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-54.5,-226.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Calque de forme 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[31,31],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-54.5,-226.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Calque de forme 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[100,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":20,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-26,-216],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0}]},{"id":"comp_1","nm":"éolienne","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Hélices","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.137],"y":[1]},"o":{"x":[0.567],"y":[0]},"t":1,"s":[0]},{"t":300.00001221925,"s":[1440]}],"ix":10},"p":{"a":0,"k":[800,440,0],"ix":2,"l":2},"a":{"a":0,"k":[800,440,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Socle eolienne","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[264.75,-95],[257,-95],[242,252],[288,251]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0}]},{"id":"comp_2","nm":"Hélices","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 11","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-239.6,"ix":10},"p":{"a":0,"k":[803,442,0],"ix":2,"l":2},"a":{"a":0,"k":[263,-98,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[310,-98],[284,-92],[263,-96],[439,10],[423,-17]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-122.747,"ix":10},"p":{"a":0,"k":[803,442,0],"ix":2,"l":2},"a":{"a":0,"k":[263,-98,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[310,-98],[284,-92],[263,-96],[439,10],[423,-17]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Calque de forme 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[803,442,0],"ix":2,"l":2},"a":{"a":0,"k":[263,-98,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[310,-98],[284,-92],[263,-96],[439,10],[423,-17]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Calque de forme 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[800.5,439.5,0],"ix":2,"l":2},"a":{"a":0,"k":[266.5,-94.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[27,27],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[266.5,-94.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0}]},{"id":"comp_3","nm":"plot base","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[541,387.5,0],"ix":2,"l":2},"a":{"a":0,"k":[1,-152.5,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[13,-223],[-13,-223],[-52,-82],[54,-82]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9019607843137255,0.3764705882352941,0.27058823529411763,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,502,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-38,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[58,-56],[-59,-56],[-70,-20],[70,-20]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9019607843137255,0.3764705882352941,0.27058823529411763,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Calque de forme 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[539.75,606.25,0],"ix":2,"l":2},"a":{"a":0,"k":[-0.25,66.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[256.5,28.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.7647058823529411,0.3215686274509804,0.23137254901960785,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-0.25,66.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Calque de forme 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,572,0],"ix":2,"l":2},"a":{"a":0,"k":[0,32,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[78,3],[-76,3],[-93,60],[93,61]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9019607843137255,0.3764705882352941,0.27058823529411763,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0}]},{"id":"comp_4","nm":"Tree","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 10","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-171.5,-79.5],[-152.5,-45]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980451995,0.84313731474,0.84313731474,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 9","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-154.5,-23.5],[-137.5,-48.5]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980451995,0.84313731474,0.84313731474,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Calque de forme 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[386,620,0],"ix":2,"l":2},"a":{"a":0,"k":[-154,80,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-154,256],[-154,-96]],"c":false},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.844306078144,0.844306078144,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Contour 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Calque de forme 8","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[384,475,0],"ix":2,"l":2},"a":{"a":0,"k":[-156,-65,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[96,222],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":62,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.949019667682,0.949019667682,0.949019667682,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-156,-65],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0}]},{"id":"comp_5","nm":"ground","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Calque de forme 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[515,795.5,0],"ix":2,"l":2},"a":{"a":0,"k":[-273.5,78,0],"ix":1,"l":2},"s":{"a":0,"k":[662,317,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[59,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2371,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333333333,0.145098039216,0.235294132607,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-273.5,78],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[817,795.5,0],"ix":2,"l":2},"a":{"a":0,"k":[-273.5,78,0],"ix":1,"l":2},"s":{"a":0,"k":[78,317,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[59,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2371,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333333333,0.145098039216,0.235294132607,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-273.5,78],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Calque de forme 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[752,795.5,0],"ix":2,"l":2},"a":{"a":0,"k":[-273.5,78,0],"ix":1,"l":2},"s":{"a":0,"k":[89,317,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[59,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2371,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333333333,0.145098039216,0.235294132607,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-273.5,78],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Calque de forme 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[270,795.5,0],"ix":2,"l":2},"a":{"a":0,"k":[-273.5,78,0],"ix":1,"l":2},"s":{"a":0,"k":[102,317,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[59,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2371,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333333333,0.145098039216,0.235294132607,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-273.5,78],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Nuage","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":51,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":234,"s":[100]},{"t":299.00001217852,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.284,"y":1},"o":{"x":0.387,"y":0},"t":0,"s":[704,380,0],"to":[-36.667,0,0],"ti":[36.667,0,0]},{"t":299.00001217852,"s":[484,380,0]}],"ix":2,"l":2},"a":{"a":0,"k":[514,308,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Calque de forme 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.5,-0.5],[0,0.5],[0,-1],[0,-0.5],[0,0],[0.5,-0.5],[0,0],[0.5,-0.5],[-0.5,-0.5]],"o":[[-19,-5],[-34,-13],[-18.5,1.5],[-17,10.5],[0,0],[17,-19.5],[6.5,-4],[-3.5,-13],[11.5,-19.5]],"v":[[263.5,176],[242.5,196],[218,213],[209.5,232.5],[215,254.5],[297,253.5],[289.5,225],[294,210],[271.5,204]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.83137254902,0.821591725069,0.821591725069,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"éolienne","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[804,796,0],"ix":2,"l":2},"a":{"a":0,"k":[804,796,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"plot base","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":7.368,"s":[6]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14.736,"s":[-5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22.264,"s":[4]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":29.633,"s":[-2]},{"t":37.0000015070409,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[540,792,0],"to":[0,-2.667,0],"ti":[0,2,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":7.368,"s":[540,776,0],"to":[0,-2,0],"ti":[0,-1.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":14.736,"s":[540,780,0],"to":[0,1.333,0],"ti":[0,-1.333,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":22.264,"s":[540,784,0],"to":[0,1.333,0],"ti":[0,-2,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":29.633,"s":[540,788,0],"to":[0,2,0],"ti":[0,-1.333,0]},{"t":37.0000015070409,"s":[540,796,0]}],"ix":2,"l":2},"a":{"a":0,"k":[540,616,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"éolienne","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[670,796,0],"ix":2,"l":2},"a":{"a":0,"k":[804,796,0],"ix":1,"l":2},"s":{"a":0,"k":[63,63,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"Tree","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[280,790,0],"ix":2,"l":2},"a":{"a":0,"k":[384,790,0],"ix":1,"l":2},"s":{"a":0,"k":[82,82,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"Tree","refId":"comp_4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[540,540,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Arbuste","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[68,540,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.5,-0.5],[0,0.5],[0,-1],[0,-0.5],[0,0],[0.5,-0.5],[0,0],[0.5,-0.5],[-0.5,-0.5]],"o":[[-19,-5],[-34,-13],[-18.5,1.5],[-17,10.5],[0,0],[17,-19.5],[6.5,-4],[-3.5,-13],[11.5,-19.5]],"v":[[263.5,176],[242.5,196],[218,213],[209.5,232.5],[215,254.5],[297,253.5],[289.5,225],[294,210],[271.5,204]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.901960784314,0.901960784314,0.901960784314,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Forme 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"ground","refId":"comp_5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[540,540,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"Nuage","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":51,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":234,"s":[100]},{"t":299.00001217852,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[193,606,0],"to":[56.5,0,0],"ti":[-56.5,0,0]},{"t":299.00001217852,"s":[532,606,0]}],"ix":2,"l":2},"a":{"a":0,"k":[514,308,0],"ix":1,"l":2},"s":{"a":0,"k":[60,60,100],"ix":6,"l":2}},"ao":0,"w":1080,"h":900,"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Calque de forme 6","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[572,712,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[784,628],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Tracé rectangulaire 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.909803921569,0.909803921569,0.909803921569,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-36,-234],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Calque de forme 5","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,716,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[728,728],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Tracé d'ellipse 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.976470588235,0.976470588235,0.976470588235,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[8,-140],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":630.000025660426,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":1,"nm":"Blanc uni 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2,"l":2},"a":{"a":0,"k":[540,540,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sw":1080,"sh":900,"ip":0,"op":630.000025660426,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/lotties/loadding.json b/assets/lotties/loadding.json new file mode 100644 index 0000000..e41a454 --- /dev/null +++ b/assets/lotties/loadding.json @@ -0,0 +1,1755 @@ +{ + "v": "5.5.8", + "fr": 50, + "ip": 0, + "op": 147, + "w": 800, + "h": 600, + "nm": "Paperplane", + "ddd": 0, + "assets": [ + { + "id": "comp_0", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "planete Outlines - Group 4", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 0, + "s": [ + 0 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 38, + "s": [ + 50 + ] + }, + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 88, + "s": [ + 50 + ] + }, + { + "t": 120, + "s": [ + 0 + ] + } + ], + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 468.336, + 323.378, + 0 + ], + "to": [ + -29, + 0, + 0 + ], + "ti": [ + 29, + 0, + 0 + ] + }, + { + "t": 102, + "s": [ + 294.336, + 323.378, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 453.672, + 304.756, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 50, + 50, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 6.742, + 0 + ], + [ + 0.741, + -0.14 + ], + [ + 0, + 0.074 + ], + [ + 13.484, + 0 + ], + [ + 1.669, + -0.361 + ], + [ + 19.79, + 0 + ], + [ + 3.317, + -19.082 + ], + [ + 2.691, + 0 + ], + [ + 0, + -13.484 + ], + [ + -0.048, + -0.629 + ], + [ + 2.405, + 0 + ], + [ + 0, + -6.742 + ], + [ + -6.742, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 6.743 + ] + ], + "o": [ + [ + -0.781, + 0 + ], + [ + 0.001, + -0.074 + ], + [ + 0, + -13.484 + ], + [ + -1.778, + 0 + ], + [ + -3.594, + -18.742 + ], + [ + -20.03, + 0 + ], + [ + -2.421, + -0.804 + ], + [ + -13.485, + 0 + ], + [ + 0, + 0.642 + ], + [ + -1.89, + -1.199 + ], + [ + -6.742, + 0 + ], + [ + 0, + 6.743 + ], + [ + 0, + 0 + ], + [ + 6.742, + 0 + ], + [ + 0, + -6.742 + ] + ], + "v": [ + [ + 75.134, + 16.175 + ], + [ + 72.85, + 16.396 + ], + [ + 72.856, + 16.175 + ], + [ + 48.44, + -8.241 + ], + [ + 43.262, + -7.685 + ], + [ + 3.406, + -40.591 + ], + [ + -36.571, + -6.995 + ], + [ + -44.269, + -8.241 + ], + [ + -68.685, + 16.175 + ], + [ + -68.604, + 18.079 + ], + [ + -75.133, + 16.175 + ], + [ + -87.341, + 28.383 + ], + [ + -75.133, + 40.592 + ], + [ + 75.134, + 40.592 + ], + [ + 87.342, + 28.383 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.815686334348, + 0.823529471603, + 0.827451040231, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 453.672, + 304.756 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 4", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 151, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Merged Shape Layer", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.547 + ], + "y": [ + 0 + ] + }, + "t": 0, + "s": [ + 0 + ] + }, + { + "i": { + "x": [ + 0.845 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 77, + "s": [ + 35 + ] + }, + { + "t": 150, + "s": [ + 0 + ] + } + ], + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.667, + "y": 1 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 0, + "s": [ + 390.319, + 298.2, + 0 + ], + "to": [ + 0, + -2.583, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.667, + "y": 1 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 44, + "s": [ + 390.319, + 282.7, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "i": { + "x": 0.667, + "y": 1 + }, + "o": { + "x": 0.333, + "y": 0 + }, + "t": 110, + "s": [ + 390.319, + 319.25, + 0 + ], + "to": [ + 0, + 0, + 0 + ], + "ti": [ + 0, + 0, + 0 + ] + }, + { + "t": 150, + "s": [ + 390.319, + 298.2, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 664.319, + 256.2, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + 18.967, + -3.189 + ], + [ + -18.967, + 19.935 + ], + [ + -0.949, + -19.935 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.223528981209, + 0.192156970501, + 0.674510002136, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 236.879, + 292.737 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 633.939, + 275.369 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 236.879, + 292.737 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 50, + 50 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "planete Outlines - Group 1", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -98.335, + 64.79 + ], + [ + -105.619, + 4.984 + ], + [ + 105.619, + -64.79 + ], + [ + -80.316, + 24.919 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.278430998325, + 0.294117987156, + 0.847059011459, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 316.247, + 247.882 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 673.623, + 252.941 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 316.247, + 247.882 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 50, + 50 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "planete Outlines - Group 2", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "o": [ + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 0 + ] + ], + "v": [ + [ + -133.812, + -42.171 + ], + [ + 133.812, + -75.141 + ], + [ + 5.765, + 75.141 + ], + [ + -61.708, + 18.402 + ], + [ + 124.227, + -71.307 + ], + [ + -87.011, + -1.534 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.365000009537, + 0.407999992371, + 0.976000010967, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 297.638, + 254.4 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 3", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 664.319, + 256.2 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 297.638, + 254.4 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 50, + 50 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "planete Outlines - Group 3", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 3, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 151, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "planete Outlines - Group 5", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 0, + "s": [ + 0 + ] + }, + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 45, + "s": [ + 100 + ] + }, + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 102, + "s": [ + 100 + ] + }, + { + "t": 150, + "s": [ + 0 + ] + } + ], + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 1, + "k": [ + { + "i": { + "x": 0.833, + "y": 0.833 + }, + "o": { + "x": 0.167, + "y": 0.167 + }, + "t": 0, + "s": [ + 327.38, + 267.583, + 0 + ], + "to": [ + 25.833, + 0, + 0 + ], + "ti": [ + -25.833, + 0, + 0 + ] + }, + { + "t": 150, + "s": [ + 482.38, + 267.583, + 0 + ] + } + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 171.76, + 193.166, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 50, + 50, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 13.485, + 0 + ], + [ + 4.38, + -4.171 + ], + [ + 21.913, + 0 + ], + [ + 3.575, + -18.765 + ], + [ + 1.851, + 0 + ], + [ + 0, + -13.484 + ], + [ + -0.011, + -0.291 + ], + [ + 1.599, + 0 + ], + [ + 0, + -6.743 + ], + [ + -6.742, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 13.485 + ] + ], + "o": [ + [ + -6.526, + 0 + ], + [ + -0.793, + -21.719 + ], + [ + -19.806, + 0 + ], + [ + -1.734, + -0.391 + ], + [ + -13.485, + 0 + ], + [ + 0, + 0.293 + ], + [ + -1.4, + -0.559 + ], + [ + -6.742, + 0 + ], + [ + 0, + 6.742 + ], + [ + 0, + 0 + ], + [ + 13.485, + 0 + ], + [ + 0, + -13.484 + ] + ], + "v": [ + [ + 59.669, + -8.242 + ], + [ + 42.84, + -1.506 + ], + [ + 2.287, + -40.592 + ], + [ + -37.576, + -7.638 + ], + [ + -42.962, + -8.242 + ], + [ + -67.378, + 16.174 + ], + [ + -67.356, + 17.049 + ], + [ + -71.878, + 16.174 + ], + [ + -84.086, + 28.383 + ], + [ + -71.878, + 40.591 + ], + [ + 59.669, + 40.591 + ], + [ + 84.086, + 16.174 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.816000007181, + 0.823999980852, + 0.827000038297, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 171.76, + 193.166 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 5", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 151, + "st": 0, + "bm": 0 + } + ] + } + ], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 0, + "nm": "Pre-comp 1", + "refId": "comp_0", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 406, + 306, + 0 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 400, + 300, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 179, + 179, + 100 + ], + "ix": 6 + } + }, + "ao": 0, + "w": 800, + "h": 600, + "ip": 0, + "op": 147, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} \ No newline at end of file diff --git a/assets/proto/comic.proto b/assets/proto/comic.proto new file mode 100644 index 0000000..3e0ff16 --- /dev/null +++ b/assets/proto/comic.proto @@ -0,0 +1,134 @@ +syntax = "proto3"; +// 该文件使用ChatGPT辅助生成 + +message ComicChapterDetailProto{ + int64 chapterId = 1; + int64 comicId = 2; + string title = 3; + int32 chapterOrder = 4; + int32 direction = 5; + repeated string pageUrl = 6; + int32 picnum = 7; + repeated string pageUrlHD = 8; + int32 commentCount = 9; +} +message ComicChapterInfoProto { + int64 chapterId = 1; + string chapterTitle = 2; + int64 updateTime = 3; + int32 fileSize = 4; + int32 chapterOrder = 5; + int32 isFee = 6; +} +message ComicChapterResponseProto { + int32 errno = 1; + string errmsg = 2; + ComicChapterDetailProto data = 3; +} + +message ComicChapterListProto { + string title = 1; + repeated ComicChapterInfoProto data = 2; +} + +message ComicDetailResponseProto { + int32 errno = 1; + string errmsg = 2; + ComicDetailProto data = 3; +} + +message ComicDetailProto { + int64 id = 1; + string title = 2; + int32 direction = 3; + int32 islong = 4; + int32 isDmzj = 5; + string cover = 6; + string description = 7; + int64 lastUpdatetime = 8; + string lastUpdateChapterName = 9; + int32 copyright = 10; + string firstLetter = 11; + string comicPy = 12; + int32 hidden = 13; + int64 hotNum = 14; + int64 hitNum = 15; + int64 uid = 16; + int32 isLock = 17; + int32 lastUpdateChapterId = 18; + repeated ComicTagProto types = 19; + repeated ComicTagProto status = 20; + repeated ComicTagProto authors = 21; + int64 subscribeNum = 22; + repeated ComicChapterListProto chapters = 23; + int32 isNeedLogin = 24; + repeated ComicDetailUrlLinkProto urlLinks = 25; + int32 isHideChapter = 26; + repeated ComicDetailUrlLinkProto dhUrlLinks = 27; + string cornerMark = 28; + int32 isFee = 29; +} + +message ComicTagProto { + int64 tagId = 1; + string tagName = 2; +} + +message ComicDetailUrlLinkProto { + string title = 1; + repeated ComicDetailUrlProto list = 2; +} +message ComicDetailUrlProto { + int64 id = 1; + string title = 2; + string url = 3; + string icon = 4; + string packageName = 5; + string dUrl = 6; + int32 btype = 7; +} + +message ComicRankListResponseProto { + int32 errno = 1; + string errmsg = 2; + repeated ComicRankListInfoProto data = 3; +} +message ComicRankListInfoProto { + int64 comic_id = 1; + string title = 2; + string authors = 3; + string status = 4; + string cover = 5; + string types = 6; + int64 last_updatetime = 7; + string last_update_chapter_name = 8; + string comic_py = 9; + int64 num = 10; + int32 tag_id = 11; + string chapter_name = 12; + int64 chapter_id = 13; +} +message RankTypeFilterResponseProto { + int32 errno = 1; + string errmsg = 2; + repeated ComicTagProto data = 3; +} + +message ComicUpdateListResponseProto { + int32 errno = 1; + string errmsg = 2; + repeated ComicUpdateListInfoProto data = 3; +} + +message ComicUpdateListInfoProto { + int64 comicId = 1; + string title = 2; + int32 islong = 3; + string authors = 4; + string types = 5; + string cover = 6; + string status = 7; + string lastUpdateChapterName = 8; + int64 lastUpdateChapterId = 9; + int64 lastUpdatetime = 10; +} \ No newline at end of file diff --git a/assets/proto/news.proto b/assets/proto/news.proto new file mode 100644 index 0000000..6d713dd --- /dev/null +++ b/assets/proto/news.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; +// 该文件使用ChatGPT辅助生成 + +message NewsListResponseProto { + int32 errno = 1; + string errmsg = 2; + repeated NewsListInfoProto data = 3; +} + +message NewsListInfoProto { + int64 articleId = 1; + string title = 2; + string fromName = 3; + string fromUrl = 4; + int64 createTime = 5; + int32 isForeign = 6; + string foreignUrl = 7; + string intro = 8; + int64 authorId = 9; + int32 status = 10; + string rowPicUrl = 11; + string colPicUrl = 12; + int32 qchatShow = 13; + string pageUrl = 14; + int64 commentAmount = 15; + int64 authorUid = 16; + string cover = 17; + string nickname = 18; + int64 moodAmount = 19; +} + diff --git a/assets/proto/novel.proto b/assets/proto/novel.proto new file mode 100644 index 0000000..4feb5cc --- /dev/null +++ b/assets/proto/novel.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; +// 该文件使用ChatGPT辅助生成 + +message NovelChapterDetailProto { + int64 chapterId = 1; + string chapterName = 2; + int32 chapterOrder = 3; +} + +message NovelVolumeProto { + int64 volume_id = 1; + int64 lnovel_id = 2; + string volume_name = 3; + int32 volume_order = 4; + int64 addtime = 5; + int32 sum_chapters = 6; +} +message NovelChapterResponseProto { + int32 errno = 1; + string errmsg = 2; + repeated NovelVolumeDetailProto data = 3; +} +message NovelVolumeDetailProto { + int64 volume_id = 1; + string volume_name = 2; + int32 volume_order = 3; + repeated NovelChapterDetailProto chapters = 4; +} + +message NovelDetailProto { + int64 novel_id = 1; + string name = 2; + string zone = 3; + string status = 4; + string last_update_volume_name = 5; + string last_update_chapter_name = 6; + int64 last_update_volume_id = 7; + int64 last_update_chapter_id = 8; + int64 last_update_time = 9; + string cover = 10; + int64 hot_hits = 11; + string introduction = 12; + repeated string types = 13; + string authors = 14; + string first_letter = 15; + int64 subscribe_num = 16; + int64 redis_update_time = 17; + repeated NovelVolumeProto volume = 18; +} +message NovelDetailResponseProto { + int32 errno = 1; + string errmsg = 2; + NovelDetailProto data = 3; +} \ No newline at end of file diff --git a/assets/statement.txt b/assets/statement.txt new file mode 100644 index 0000000..0faa973 --- /dev/null +++ b/assets/statement.txt @@ -0,0 +1,15 @@ +在使用本软件之前,请您仔细阅读以下内容,并确保您充分理解并同意以下条款: + +1、本软件为开源的第三方软件,可以免费下载用于测试相关功能,在测试完毕后应及时卸载本软件。 + +2、本软件为第三方开源软件,不与动漫之家(zaimanhua.com)有任何关联。软件内所有内容均来自动漫之家(zaimanhua.com)公开在互联网的资源,仅供用户参考和学习使用,不得用于商业和非法用途。对于使用本软件所造成的任何后果,本软件作者概不负责。 + +3、如果本软件存在侵犯您的合法权益的情况,请及时与作者联系,作者将会及时删除有关内容。 + +4、本软件不会收集、存储、使用任何用户的个人信息,包括但不限于姓名、地址、电子邮件地址、电话号码等。在使用本软件过程中,不会进行任何形式的个人信息采集。如用户提供任何个人信息,将被视为用户已自愿提供,并且用户将自行承担由此产生的所有法律责任。 + +5、本软件使用者应遵守国家相关法律法规和使用规范,不得利用本软件从事任何违法违规行为。如因使用本软件而导致的违法行为,使用者应承担相应的法律责任。 + +6、本软件作者保留对免责声明的最终解释权。 + +如您不同意本免责声明中的任何内容,请勿使用本软件。使用本软件即代表您已完全理解并同意上述内容。 \ No newline at end of file diff --git a/distribute_options.yaml b/distribute_options.yaml new file mode 100644 index 0000000..c06aab1 --- /dev/null +++ b/distribute_options.yaml @@ -0,0 +1 @@ +output: build/dist/ \ No newline at end of file diff --git a/document/RELEASE.txt b/document/RELEASE.txt new file mode 100644 index 0000000..78ee60f --- /dev/null +++ b/document/RELEASE.txt @@ -0,0 +1,3 @@ +## 再漫画X(ZAI-X) + +1. 修复漫画下载失败 \ No newline at end of file diff --git a/document/app_version.json b/document/app_version.json new file mode 100644 index 0000000..acae9a6 --- /dev/null +++ b/document/app_version.json @@ -0,0 +1,7 @@ +{ + "version": "1.0.3-test", + "version_num": 10003, + "version_desc": "1. 修复漫画下载失败", + "prerelease":true, + "download_url": "https://github.com/xiaoyaocz/flutter_dmzj/releases" +} \ No newline at end of file diff --git a/document/logo.png b/document/logo.png new file mode 100644 index 0000000..bcf2d64 Binary files /dev/null and b/document/logo.png differ diff --git a/document/screenshot_dark.jpg b/document/screenshot_dark.jpg new file mode 100644 index 0000000..b466596 Binary files /dev/null and b/document/screenshot_dark.jpg differ diff --git a/document/screenshot_light.jpg b/document/screenshot_light.jpg new file mode 100644 index 0000000..de604fd Binary files /dev/null and b/document/screenshot_light.jpg differ diff --git a/flutter_dmzj.iml b/flutter_dmzj.iml new file mode 100644 index 0000000..f66303d --- /dev/null +++ b/flutter_dmzj.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.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/ephemeral/ +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..9625e10 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..26f32db --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,92 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + # Start of the permission_handler configuration + target.build_configurations.each do |config| + + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.calendar + # 'PERMISSION_EVENTS=1', + + ## dart: PermissionGroup.reminders + # 'PERMISSION_REMINDERS=1', + + ## dart: PermissionGroup.contacts + # 'PERMISSION_CONTACTS=1', + + ## dart: PermissionGroup.camera + # 'PERMISSION_CAMERA=1', + + ## dart: PermissionGroup.microphone + # 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.speech + # 'PERMISSION_SPEECH_RECOGNIZER=1', + + ## dart: PermissionGroup.photos + 'PERMISSION_PHOTOS=1', + + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + # 'PERMISSION_LOCATION=1', + + ## dart: PermissionGroup.notification + # 'PERMISSION_NOTIFICATIONS=1', + + ## dart: PermissionGroup.mediaLibrary + 'PERMISSION_MEDIA_LIBRARY=1', + + ## dart: PermissionGroup.sensors + # 'PERMISSION_SENSORS=1', + + ## dart: PermissionGroup.bluetooth + # 'PERMISSION_BLUETOOTH=1', + + ## dart: PermissionGroup.appTrackingTransparency + # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', + + ## dart: PermissionGroup.criticalAlerts + # 'PERMISSION_CRITICAL_ALERTS=1' + ] + + end + # End of the permission_handler configuration + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..2a7b7a3 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,78 @@ +PODS: + - battery_plus (1.0.0): + - Flutter + - connectivity_plus (0.0.1): + - Flutter + - ReachabilitySwift + - Flutter (1.0.0) + - image_gallery_saver (1.5.0): + - Flutter + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.0.4): + - Flutter + - ReachabilitySwift (5.0.0) + - share_plus (0.0.1): + - Flutter + - url_launcher_ios (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + +DEPENDENCIES: + - battery_plus (from `.symlinks/plugins/battery_plus/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - Flutter (from `Flutter`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + +SPEC REPOS: + trunk: + - ReachabilitySwift + +EXTERNAL SOURCES: + battery_plus: + :path: ".symlinks/plugins/battery_plus/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + Flutter: + :path: Flutter + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + +SPEC CHECKSUMS: + battery_plus: 7851ab482c336dd101ae735dc9363f01e1223c7c + connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + image_gallery_saver: 259eab68fb271cfd57d599904f7acdc7832e7ef2 + package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a + +PODFILE CHECKSUM: 1b31a9485eb065f6ddb2d52b30821014da53f47c + +COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..058e0be --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,575 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 1AFF947029C1978C00F3E68E /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1AFF947229C1978C00F3E68E /* InfoPlist.strings */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8F307A352361EDE5BABDC073 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C73EA037D0467E2EB9E3A60 /* Pods_Runner.framework */; }; + 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 = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0C73EA037D0467E2EB9E3A60 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 1AFF946C29C1971200F3E68E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + 1AFF946D29C1971200F3E68E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 1AFF947129C1978C00F3E68E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + 1AFF947329C1984200F3E68E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 49A03E1F5AD2E9DACBAED9EA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; 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 = ""; }; + 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 = ""; }; + 9AD1B2FE02040D4034AA22EC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + C942B140FBE01FE9F279C39A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8F307A352361EDE5BABDC073 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 335A8CF08CF155B5A144E865 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0C73EA037D0467E2EB9E3A60 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + B26C8A701323C0262A7396FA /* Pods */, + 335A8CF08CF155B5A144E865 /* Frameworks */, + ); + 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 */, + 1AFF947229C1978C00F3E68E /* InfoPlist.strings */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + B26C8A701323C0262A7396FA /* Pods */ = { + isa = PBXGroup; + children = ( + 9AD1B2FE02040D4034AA22EC /* Pods-Runner.debug.xcconfig */, + 49A03E1F5AD2E9DACBAED9EA /* Pods-Runner.release.xcconfig */, + C942B140FBE01FE9F279C39A /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C969790543225AACCBA7A1B7 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 9CDD320A99C9F43BFE6E718D /* [CP] Embed Pods Frameworks */, + ); + 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 = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + "zh-Hans", + ); + 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 */, + 1AFF947029C1978C00F3E68E /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + 9CDD320A99C9F43BFE6E718D /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C969790543225AACCBA7A1B7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* 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 */ + 1AFF947229C1978C00F3E68E /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 1AFF947129C1978C00F3E68E /* zh-Hans */, + 1AFF947329C1984200F3E68E /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + 1AFF946C29C1971200F3E68E /* zh-Hans */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + 1AFF946D29C1971200F3E68E /* zh-Hans */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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 = 11.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)"; + DEVELOPMENT_TEAM = 9R87RMF9C9; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.xycz.zmhx; + 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; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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 = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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 = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + 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)"; + DEVELOPMENT_TEAM = 9R87RMF9C9; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.xycz.zmhx; + 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)"; + DEVELOPMENT_TEAM = 9R87RMF9C9; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.xycz.zmhx; + 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..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + 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/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..c68df94 --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,120 @@ +{ + "images": [ + { + "size": "20x20", + "idiom": "universal", + "filename": "icon-20@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "20x20", + "idiom": "universal", + "filename": "icon-20@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "29x29", + "idiom": "universal", + "filename": "icon-29@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "29x29", + "idiom": "universal", + "filename": "icon-29@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "38x38", + "idiom": "universal", + "filename": "icon-38@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "38x38", + "idiom": "universal", + "filename": "icon-38@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "40x40", + "idiom": "universal", + "filename": "icon-40@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "40x40", + "idiom": "universal", + "filename": "icon-40@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "60x60", + "idiom": "universal", + "filename": "icon-60@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "60x60", + "idiom": "universal", + "filename": "icon-60@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "64x64", + "idiom": "universal", + "filename": "icon-64@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "64x64", + "idiom": "universal", + "filename": "icon-64@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "68x68", + "idiom": "universal", + "filename": "icon-68@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "76x76", + "idiom": "universal", + "filename": "icon-76@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "83.5x83.5", + "idiom": "universal", + "filename": "icon-83.5@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "1024x1024", + "idiom": "universal", + "filename": "icon-1024.png", + "scale": "1x", + "platform": "ios" + } + ], + "info": { + "version": 1, + "author": "icon.wuruihong.com" + } +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..ae6bee0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..f1f6085 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..56b3bb4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..9d94d80 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..dad48f3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..11fa14c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..afc5998 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..56b3bb4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..2dafec4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..722a744 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..9fbc7ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..3fa2073 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..c5311a0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..5aaeda7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..722a744 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..652bdb7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..c67c537 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..61ecc46 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c881632 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..4ef7667 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..1923ad7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png new file mode 100644 index 0000000..4a9ddc4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png new file mode 100644 index 0000000..cff25c9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png new file mode 100644 index 0000000..19eb2dd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png new file mode 100644 index 0000000..d7cac68 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png new file mode 100644 index 0000000..55b58ba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png new file mode 100644 index 0000000..242f983 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png new file mode 100644 index 0000000..3c4e205 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 0000000..069ea43 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 0000000..13e403a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 0000000..13e403a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 0000000..9c783b3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png new file mode 100644 index 0000000..c0e94ab Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png new file mode 100644 index 0000000..7a4ceb6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png new file mode 100644 index 0000000..ab00cd2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 0000000..234f1fe Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000..849b3d7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ 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..fdd7618 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + ZAIX + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ZAIX + 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 + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSPhotoLibraryUsageDescription + Save pictures to your gallery + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/en.lproj/InfoPlist.strings b/ios/Runner/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..b6bb47e --- /dev/null +++ b/ios/Runner/en.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* + InfoPlist.strings + Runner + + Created by xiaoyaocz on 2023/3/15. + +*/ +"CFBundleDisplayName" = "DMZJX"; +"NSPhotoLibraryUsageDescription" = "Save pictures to your gallery"; \ No newline at end of file diff --git a/ios/Runner/zh-Hans.lproj/InfoPlist.strings b/ios/Runner/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000..0ae4160 --- /dev/null +++ b/ios/Runner/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* + InfoPlist.strings + Runner + + Created by xiaoyaocz on 2023/3/15. + +*/ +"CFBundleDisplayName" = "再漫画Flutter"; +"NSPhotoLibraryUsageDescription" = "保存图片至相册"; diff --git a/ios/Runner/zh-Hans.lproj/LaunchScreen.strings b/ios/Runner/zh-Hans.lproj/LaunchScreen.strings new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ios/Runner/zh-Hans.lproj/LaunchScreen.strings @@ -0,0 +1 @@ + diff --git a/ios/Runner/zh-Hans.lproj/Main.strings b/ios/Runner/zh-Hans.lproj/Main.strings new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ios/Runner/zh-Hans.lproj/Main.strings @@ -0,0 +1 @@ + diff --git a/lib/app/app_color.dart b/lib/app/app_color.dart new file mode 100644 index 0000000..ac17c1a --- /dev/null +++ b/lib/app/app_color.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class AppColor { + static const Color primaryColor = Color(0xff4196f9); + + static ColorScheme colorSchemeLight = ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.light, + ); + static ColorScheme colorSchemeDark = ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.dark, + ); + static const Color backgroundColor = Color(0xfffafafa); + static const Color backgroundColorDark = Color(0xff212121); + static const Color black333 = Color(0xff333333); + static const Color greyf0f0f0 = Color(0xfff0f0f0); + + static Map> novelThemes = { + 0: [ + const Color.fromRGBO(245, 239, 217, 1), + const Color(0xff301e1b), + ], + 1: [ + const Color.fromRGBO(248, 247, 252, 1), + black333, + ], + 2: [ + const Color.fromRGBO(192, 237, 198, 1), + Colors.black, + ], + 3: [ + const Color(0xff3b3a39), + const Color.fromRGBO(230, 230, 230, 1), + ], + 4: [ + Colors.black, + const Color.fromRGBO(200, 200, 200, 1), + ], + }; +} diff --git a/lib/app/app_constant.dart b/lib/app/app_constant.dart new file mode 100644 index 0000000..b83eb03 --- /dev/null +++ b/lib/app/app_constant.dart @@ -0,0 +1,27 @@ +class AppConstant { + /// 定义平板宽度,当大于此宽度时APP进入双栏模式 + static const double kTabletWidth = 1000; + + /// 类型ID-漫画 + static const int kTypeComic = 4; + + /// 类型ID-新闻 + static const int kTypeNews = 6; + + /// 类型ID-专题 + static const int kTypeSpecial = 2; + + /// 类型ID-轻小说 + static const int kTypeNovel = 1; +} + +class ReaderDirection { + /// 左右 0 + static const int kLeftToRight = 0; + + /// 上下 1 + static const int kUpToDown = 1; + + /// 右左 2 + static const int kRightToLeft = 2; +} diff --git a/lib/app/app_error.dart b/lib/app/app_error.dart new file mode 100644 index 0000000..19958ab --- /dev/null +++ b/lib/app/app_error.dart @@ -0,0 +1,13 @@ +class AppError implements Exception { + final int code; + final String message; + AppError( + this.message, { + this.code = 0, + }); + + @override + String toString() { + return message; + } +} diff --git a/lib/app/app_style.dart b/lib/app/app_style.dart new file mode 100644 index 0000000..5dfef16 --- /dev/null +++ b/lib/app/app_style.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:get/get.dart'; + +class AppStyle { + static ThemeData lightTheme = getLightTheme(); + static ThemeData getLightTheme({ColorScheme? colorScheme}) { + final scheme = colorScheme ?? AppColor.colorSchemeLight; + return ThemeData( + useMaterial3: true, + colorScheme: scheme, + brightness: Brightness.light, + ).copyWith( + scaffoldBackgroundColor: colorScheme != null ? null : Colors.white, + cardColor: colorScheme != null ? null : Colors.white, + appBarTheme: AppBarTheme( + elevation: 0, + backgroundColor: Colors.transparent, + foregroundColor: scheme.onSurface, + centerTitle: false, + shape: Border( + bottom: BorderSide( + color: Colors.grey.withOpacity(.2), + width: 1, + ), + ), + iconTheme: IconThemeData( + color: scheme.onSurface, + ), + titleTextStyle: TextStyle( + fontSize: 16, + color: scheme.onSurface, + ), + systemOverlayStyle: SystemUiOverlayStyle.dark.copyWith( + systemNavigationBarColor: Colors.transparent, + ), + ), + ); + } + + static ThemeData darkTheme = getDarkTheme(); + static ThemeData getDarkTheme({ColorScheme? colorScheme}) { + final scheme = colorScheme ?? AppColor.colorSchemeDark; + return ThemeData( + useMaterial3: true, + colorScheme: scheme, + brightness: Brightness.dark, + ).copyWith( + primaryColor: scheme.primary, + cardColor: const Color(0xff424242), + scaffoldBackgroundColor: Colors.black, + tabBarTheme: TabBarThemeData( + indicatorColor: scheme.primary, + ), + appBarTheme: AppBarTheme( + elevation: 0, + backgroundColor: Colors.transparent, + foregroundColor: Colors.white, + centerTitle: false, + shape: Border( + bottom: BorderSide( + color: Colors.grey.withOpacity(.2), + width: 1, + ), + ), + titleTextStyle: const TextStyle( + fontSize: 16, + color: Colors.white, + ), + iconTheme: const IconThemeData( + color: Colors.white, + ), + systemOverlayStyle: SystemUiOverlayStyle.light.copyWith( + systemNavigationBarColor: Colors.transparent, + ), + ), + ); + } + static const vGap4 = SizedBox( + height: 4, + ); + static const vGap8 = SizedBox( + height: 8, + ); + static const vGap12 = SizedBox( + height: 12, + ); + static const vGap24 = SizedBox( + height: 24, + ); + static const vGap32 = SizedBox( + height: 32, + ); + + static const hGap4 = SizedBox( + width: 4, + ); + static const hGap8 = SizedBox( + width: 8, + ); + static const hGap12 = SizedBox( + width: 12, + ); + static const hGap16 = SizedBox( + width: 16, + ); + + static const hGap24 = SizedBox( + width: 24, + ); + static const hGap32 = SizedBox( + width: 32, + ); + + static const edgeInsetsH4 = EdgeInsets.symmetric(horizontal: 4); + static const edgeInsetsH8 = EdgeInsets.symmetric(horizontal: 8); + static const edgeInsetsH12 = EdgeInsets.symmetric(horizontal: 12); + static const edgeInsetsH16 = EdgeInsets.symmetric(horizontal: 16); + static const edgeInsetsH20 = EdgeInsets.symmetric(horizontal: 20); + static const edgeInsetsH24 = EdgeInsets.symmetric(horizontal: 24); + + static const edgeInsetsV4 = EdgeInsets.symmetric(vertical: 4); + static const edgeInsetsV8 = EdgeInsets.symmetric(vertical: 8); + static const edgeInsetsV12 = EdgeInsets.symmetric(vertical: 12); + static const edgeInsetsV24 = EdgeInsets.symmetric(vertical: 24); + + static const edgeInsetsA4 = EdgeInsets.all(4); + static const edgeInsetsA8 = EdgeInsets.all(8); + static const edgeInsetsA12 = EdgeInsets.all(12); + static const edgeInsetsA24 = EdgeInsets.all(24); + + static const edgeInsetsR4 = EdgeInsets.only(right: 4); + static const edgeInsetsR8 = EdgeInsets.only(right: 8); + static const edgeInsetsR12 = EdgeInsets.only(right: 12); + static const edgeInsetsR20 = EdgeInsets.only(right: 20); + static const edgeInsetsR24 = EdgeInsets.only(right: 24); + + static const edgeInsetsL4 = EdgeInsets.only(left: 4); + static const edgeInsetsL8 = EdgeInsets.only(left: 8); + static const edgeInsetsL12 = EdgeInsets.only(left: 12); + static const edgeInsetsL24 = EdgeInsets.only(left: 24); + + static const edgeInsetsT4 = EdgeInsets.only(top: 4); + static const edgeInsetsT8 = EdgeInsets.only(top: 8); + static const edgeInsetsT12 = EdgeInsets.only(top: 12); + static const edgeInsetsT24 = EdgeInsets.only(top: 24); + + static const edgeInsetsB4 = EdgeInsets.only(bottom: 4); + static const edgeInsetsB8 = EdgeInsets.only(bottom: 8); + static const edgeInsetsB12 = EdgeInsets.only(bottom: 12); + static const edgeInsetsB24 = EdgeInsets.only(bottom: 24); + + static BorderRadius radius4 = BorderRadius.circular(4); + static BorderRadius radius8 = BorderRadius.circular(8); + static BorderRadius radius12 = BorderRadius.circular(12); + static BorderRadius radius24 = BorderRadius.circular(24); + static BorderRadius radius32 = BorderRadius.circular(32); + static BorderRadius radius48 = BorderRadius.circular(48); + + /// 顶部状态栏的高度 + static double get statusBarHeight => MediaQuery.of(Get.context!).padding.top; + + /// 底部导航条的高度 + static double get bottomBarHeight => + MediaQuery.of(Get.context!).padding.bottom; +} diff --git a/lib/app/controller/base_controller.dart b/lib/app/controller/base_controller.dart new file mode 100644 index 0000000..8e8a50b --- /dev/null +++ b/lib/app/controller/base_controller.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_dmzj/app/log.dart'; + +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class BaseController extends GetxController { + /// 加载中,更新页面 + var pageLoadding = false.obs; + + /// 加载中,不会更新页面 + var loadding = false; + + /// 空白页面 + var pageEmpty = false.obs; + + /// 页面错误 + var pageError = false.obs; + + /// 未登录 + var notLogin = false.obs; + + /// 错误信息 + var errorMsg = "".obs; + + Error? error; + + /// 显示错误 + /// * [msg] 错误信息 + /// * [showPageError] 显示页面错误 + /// * 只在第一页加载错误时showPageError=true,后续页加载错误时使用Toast弹出通知 + void handleError(Object exception, {bool showPageError = false}) { + Log.logPrint(exception); + var msg = exceptionToString(exception); + if (exception is Error) { + error = exception; + } + if (showPageError) { + pageError.value = true; + errorMsg.value = msg; + } else { + SmartDialog.showToast(exceptionToString(msg)); + } + } + + String exceptionToString(Object exception) { + return exception.toString().replaceAll("Exception:", ""); + } + + void onLogin() {} + void onLogout() {} +} + +class BaseDataController extends BaseController { + T? data; + Future loadData() async { + try { + if (loadding) return; + loadding = true; + pageError.value = false; + pageLoadding.value = true; + error = null; + var result = await getData(); + data = result; + } catch (e) { + handleError(exceptionToString(e), showPageError: true); + } finally { + loadding = false; + pageLoadding.value = false; + } + } + + Future getData() async { + return null; + } +} + +class BasePageController extends BaseController { + final ScrollController scrollController = ScrollController(); + final EasyRefreshController easyRefreshController = EasyRefreshController(); + int currentPage = 1; + int count = 0; + int maxPage = 0; + int pageSize = 24; + var canLoadMore = false.obs; + var list = [].obs; + + Future refreshData() async { + currentPage = 1; + list.clear(); + await loadData(); + } + + Future loadData() async { + try { + if (loadding) return; + loadding = true; + pageError.value = false; + pageEmpty.value = false; + notLogin.value = false; + error = null; + pageLoadding.value = currentPage == 1; + + var result = await getData(currentPage, pageSize); + //是否可以加载更多 + if (result.isNotEmpty) { + currentPage++; + canLoadMore.value = true; + pageEmpty.value = false; + } else { + canLoadMore.value = false; + if (currentPage == 1) { + pageEmpty.value = true; + } + } + // 赋值数据 + if (currentPage == 1) { + list.value = result; + } else { + list.addAll(result); + } + } catch (e) { + handleError(e, showPageError: currentPage == 1); + } finally { + loadding = false; + pageLoadding.value = false; + } + } + + Future> getData(int page, int pageSize) async { + return []; + } + + void scrollToTopOrRefresh() { + if (scrollController.offset > 0) { + scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 200), + curve: Curves.linear, + ); + } else { + easyRefreshController.callRefresh(); + } + } +} diff --git a/lib/app/dialog_utils.dart b/lib/app/dialog_utils.dart new file mode 100644 index 0000000..58dfa22 --- /dev/null +++ b/lib/app/dialog_utils.dart @@ -0,0 +1,269 @@ +import 'dart:io'; + +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:get/get.dart'; +import 'package:photo_view/photo_view_gallery.dart'; + +class DialogUtils { + /// 提示弹窗 + /// - `content` 内容 + /// - `title` 弹窗标题 + /// - `confirm` 确认按钮内容,留空为确定 + /// - `cancel` 取消按钮内容,留空为取消 + static Future showAlertDialog( + String content, { + String title = '', + String confirm = '', + String cancel = '', + bool selectable = false, + bool barrierDismissible = true, + List? actions, + }) async { + var result = await Get.dialog( + AlertDialog( + title: Text(title), + content: Container( + constraints: const BoxConstraints( + maxHeight: 400, + maxWidth: 500, + ), + child: SingleChildScrollView( + child: Padding( + padding: AppStyle.edgeInsetsV12, + child: selectable ? SelectableText(content) : Text(content), + ), + ), + ), + actions: [ + TextButton( + onPressed: (() => Get.back(result: false)), + child: Text(cancel.isEmpty ? "取消" : cancel), + ), + TextButton( + onPressed: (() => Get.back(result: true)), + child: Text(confirm.isEmpty ? "确定" : confirm), + ), + ...?actions, + ], + ), + barrierDismissible: barrierDismissible, + ); + return result ?? false; + } + + /// 提示弹窗 + /// - `content` 内容 + /// - `title` 弹窗标题 + /// - `confirm` 确认按钮内容,留空为确定 + static Future showMessageDialog(String content, + {String title = '', String confirm = '', bool selectable = false}) async { + var result = await Get.dialog( + AlertDialog( + title: Text(title), + content: Padding( + padding: AppStyle.edgeInsetsV12, + child: selectable ? SelectableText(content) : Text(content), + ), + actions: [ + TextButton( + onPressed: (() => Get.back(result: true)), + child: Text(confirm.isEmpty ? "确定" : confirm), + ), + ], + ), + ); + return result ?? false; + } + + /// 文本编辑的弹窗 + /// - `content` 编辑框默认的内容 + /// - `title` 弹窗标题 + /// - `confirm` 确认按钮内容 + /// - `cancel` 取消按钮内容 + static Future showEditTextDialog(String content, + {String title = '', + String? hintText, + String confirm = '', + String cancel = ''}) async { + final TextEditingController textEditingController = + TextEditingController(text: content); + var result = await Get.dialog( + AlertDialog( + title: Text(title), + content: Padding( + padding: AppStyle.edgeInsetsT12, + child: TextField( + controller: textEditingController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + //prefixText: title, + contentPadding: AppStyle.edgeInsetsA12, + hintText: hintText ?? title, + ), + // style: TextStyle( + // height: 1.0, + // color: Get.isDarkMode ? Colors.white : Colors.black), + autofocus: true, + ), + ), + actions: [ + TextButton( + onPressed: Get.back, + child: const Text("取消"), + ), + TextButton( + onPressed: () { + Get.back(result: textEditingController.text); + }, + child: const Text("确定"), + ), + ], + ), + // barrierColor: + // Get.isDarkMode ? Colors.grey.withOpacity(.3) : Colors.black38, + ); + return result; + } + + static Future showOptionDialog( + List contents, + T value, { + String title = '', + }) async { + var result = await Get.dialog( + SimpleDialog( + title: Text(title), + children: contents + .map( + (e) => RadioListTile( + title: Text(e.toString()), + value: e, + groupValue: value, + onChanged: (e) { + Get.back(result: e); + }, + ), + ) + .toList(), + ), + ); + return result; + } + + static void showStatement() async { + var text = await rootBundle.loadString("assets/statement.txt"); + + showAlertDialog( + text, + selectable: true, + title: "免责声明", + confirm: "已阅读并同意", + cancel: "退出", + barrierDismissible: false, + ).then((value) { + if (!value) { + exit(0); + } + }); + } + + static Future showMapOptionDialog( + Map contents, + T value, { + String title = '', + }) async { + var result = await Get.dialog( + SimpleDialog( + title: Text(title), + children: contents.keys + .map( + (e) => RadioListTile( + title: Text((contents[e] ?? '-').tr), + value: e, + groupValue: value, + onChanged: (e) { + Get.back(result: e); + }, + ), + ) + .toList(), + ), + ); + return result; + } + + static void showImageViewer(int initIndex, List images) { + var index = initIndex.obs; + Get.dialog( + Scaffold( + backgroundColor: Colors.black87, + body: Stack( + children: [ + PhotoViewGallery.builder( + itemCount: images.length, + builder: (_, i) { + if (images[i].startsWith("http")) { + return PhotoViewGalleryPageOptions( + filterQuality: FilterQuality.high, + imageProvider: ExtendedNetworkImageProvider( + images[i], + cache: true, + ), + onTapUp: ((context, details, controllerValue) => + Get.back()), + ); + } else { + return PhotoViewGalleryPageOptions( + filterQuality: FilterQuality.high, + imageProvider: ExtendedMemoryImageProvider( + File(images[i]).readAsBytesSync(), + ), + onTapUp: ((context, details, controllerValue) => + Get.back()), + ); + } + }, + loadingBuilder: (context, event) => const Center( + child: CircularProgressIndicator(), + ), + pageController: PageController( + initialPage: index.value, + ), + onPageChanged: ((i) { + index.value = i; + }), + ), + Container( + alignment: Alignment.bottomCenter, + margin: AppStyle.edgeInsetsA24 + .copyWith(bottom: 24 + AppStyle.bottomBarHeight), + child: Obx( + () => Text( + "${index.value + 1}/${images.length}", + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white), + ), + ), + ), + Positioned( + right: 12 + AppStyle.bottomBarHeight, + bottom: 12, + child: TextButton.icon( + onPressed: () { + Utils.saveImage(images[index.value]); + }, + icon: const Icon(Icons.save), + label: const Text("保存"), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/event_bus.dart b/lib/app/event_bus.dart new file mode 100644 index 0000000..c4a880d --- /dev/null +++ b/lib/app/event_bus.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:flutter_dmzj/app/log.dart'; + +/// 全局事件 +class EventBus { + /// 点击了底部导航 + static const String kBottomNavigationBarClicked = + "BottomNavigationBarClicked"; + + /// 更新了漫画记录 + static const String kUpdatedComicHistory = "UpdateComicHistory"; + + /// 更新了小说记录 + static const String kUpdatedNovelHistory = "UpdateNovelHistory"; + static EventBus? _instance; + + static EventBus get instance { + _instance ??= EventBus(); + return _instance!; + } + + final Map _streams = {}; + + /// 触发事件 + void emit(String name, T data) { + if (!_streams.containsKey(name)) { + _streams.addAll({name: StreamController.broadcast()}); + } + Log.d("Emit Event:$name\r\n$data"); + + _streams[name]!.add(data); + } + + /// 监听事件 + StreamSubscription listen(String name, Function(dynamic)? onData) { + if (!_streams.containsKey(name)) { + _streams.addAll({name: StreamController.broadcast()}); + } + return _streams[name]!.stream.listen(onData); + } +} diff --git a/lib/app/keys.dart b/lib/app/keys.dart new file mode 100644 index 0000000..4af8dc6 --- /dev/null +++ b/lib/app/keys.dart @@ -0,0 +1,12 @@ +// ignore_for_file: constant_identifier_names + +class Keys { + /// APP相关设置的Hive Box名称 + static const SETTINGS_BOX_NAME = "DmzjSettings"; + + /// 主题模式 + static const SETTINGS_THEME_MODE = "ThemeMode"; + + /// 主题颜色 + static const SETTINGS_THEME_COLOR = "ThemeColor"; +} diff --git a/lib/app/log.dart b/lib/app/log.dart new file mode 100644 index 0000000..5730ffe --- /dev/null +++ b/lib/app/log.dart @@ -0,0 +1,39 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class Log { + static Logger logger = Logger( + printer: PrettyPrinter( + methodCount: 0, + errorMethodCount: 8, + lineLength: 120, + colors: true, + printEmojis: true, + printTime: false, + ), + ); + + static d(String message) { + logger.d("${DateTime.now().toString()}\n$message"); + } + + static i(String message) { + logger.i("${DateTime.now().toString()}\n$message"); + } + + static e(String message, StackTrace stackTrace) { + logger.e("${DateTime.now().toString()}\n$message", stackTrace: stackTrace); + } + + static w(String message) { + logger.w("${DateTime.now().toString()}\n$message"); + } + + static void logPrint(dynamic obj) { + if (obj is Error) { + Log.e(obj.toString(), obj.stackTrace ?? StackTrace.current); + } else if (kDebugMode) { + print(obj); + } + } +} diff --git a/lib/app/platform_utils.dart b/lib/app/platform_utils.dart new file mode 100644 index 0000000..31a8f1d --- /dev/null +++ b/lib/app/platform_utils.dart @@ -0,0 +1,51 @@ +import 'dart:io' show Platform; + +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; + +/// Windows平台检测与Fluent UI主题工具 +class PlatformUtils { + /// Web平台不支持dart:io,需先排除 + static bool get isWindows => !kIsWeb && Platform.isWindows; + + /// 根据当前Material主题生成对应的FluentThemeData + static fluent.FluentThemeData getFluentTheme(BuildContext context) { + final materialTheme = Theme.of(context); + final brightness = materialTheme.brightness; + final primary = materialTheme.colorScheme.primary; + + // 根据primary color构建AccentColor渐变色阶 + final accent = fluent.AccentColor.swatch({ + 'darkest': _darken(primary, 0.4), + 'darker': _darken(primary, 0.2), + 'dark': _darken(primary, 0.1), + 'normal': primary, + 'light': _lighten(primary, 0.15), + 'lighter': _lighten(primary, 0.3), + 'lightest': _lighten(primary, 0.5), + }); + + return fluent.FluentThemeData( + brightness: brightness, + accentColor: accent, + scaffoldBackgroundColor: brightness == Brightness.dark + ? const Color(0xff202020) + : const Color(0xfff3f3f3), + ); + } + + static Color _darken(Color color, double amount) { + final hsl = HSLColor.fromColor(color); + return hsl + .withLightness((hsl.lightness - amount).clamp(0.0, 1.0)) + .toColor(); + } + + static Color _lighten(Color color, double amount) { + final hsl = HSLColor.fromColor(color); + return hsl + .withLightness((hsl.lightness + amount).clamp(0.0, 1.0)) + .toColor(); + } +} diff --git a/lib/app/utils.dart b/lib/app/utils.dart new file mode 100644 index 0000000..86715fa --- /dev/null +++ b/lib/app/utils.dart @@ -0,0 +1,276 @@ +import 'dart:io'; + +import 'package:extended_image/extended_image.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/requests/common_request.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart'; +import 'package:intl/intl.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; +import 'package:share_plus/share_plus.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class Utils { + static late PackageInfo packageInfo; + static DateFormat dateFormat = DateFormat("yyyy-MM-dd"); + static DateFormat dateTimeFormat = DateFormat("MM-dd HH:mm"); + static DateFormat dateTimeFormatWithYear = DateFormat("yyyy-MM-dd HH:mm"); + + /// 版本号解析 + static int parseVersion(String version) { + var sp = version.split('.'); + var num = ""; + for (var item in sp) { + num = num + item.padLeft(2, '0'); + } + return int.parse(num); + } + + /// 时间戳格式化-秒 + static String formatTimestamp(int ts) { + if (ts == 0) { + return "----"; + } + return formatTimestampMS(ts * 1000); + } + + static String formatTimestampToDate(int ts) { + if (ts == 0) { + return "----"; + } + var dt = DateTime.fromMillisecondsSinceEpoch(ts * 1000); + return dateFormat.format(dt); + } + + /// 时间戳格式化-毫秒 + static String formatTimestampMS(int ts) { + var dt = DateTime.fromMillisecondsSinceEpoch(ts); + + var dtNow = DateTime.now(); + if (dt.year == dtNow.year && + dt.month == dtNow.month && + dt.day == dtNow.day) { + return "今天${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}"; + } + if (dt.year == dtNow.year && + dt.month == dtNow.month && + dt.day == dtNow.day - 1) { + return "昨天${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}"; + } + + if (dt.year == dtNow.year) { + return dateTimeFormat.format(dt); + } + + return dateTimeFormatWithYear.format(dt); + } + + /// 检查相册权限 + static Future checkPhotoPermission() async { + try { + var status = await Permission.photos.status; + if (status == PermissionStatus.granted) { + return true; + } + status = await Permission.photos.request(); + if (status.isGranted) { + return true; + } else { + SmartDialog.showToast("请授予相册权限"); + return false; + } + } catch (e) { + return false; + } + } + + /// 保存图片 + static void saveImage(String url) async { + if (Platform.isIOS && !await Utils.checkPhotoPermission()) { + return; + } + try { + Uint8List? data; + if (url.startsWith("http")) { + var provider = ExtendedNetworkImageProvider(url, cache: true); + data = await provider.getNetworkImageData(); + } else { + data = await File(url).readAsBytes(); + } + + if (data == null) { + SmartDialog.showToast("图片保存失败"); + return; + } + if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) { + saveImageDetktop(p.basename(url), data); + } else { + var cacheDir = await getTemporaryDirectory(); + var file = File(p.join(cacheDir.path, p.basename(url))); + await file.writeAsBytes(data); + final result = await ImageGallerySaverPlus.saveFile( + file.path, + name: p.basename(url), + isReturnPathOfIOS: true, + ); + Log.d(result.toString()); + SmartDialog.showToast("保存成功"); + } + } catch (e) { + SmartDialog.showToast("保存失败"); + } + } + + /// 保存图片-桌面平台 + static void saveImageDetktop(String fileName, Uint8List list) async { + final FileSaveLocation? location = + await getSaveLocation(suggestedName: fileName); + if (location == null) { + return; + } + final XFile file = XFile.fromData(list, name: fileName); + await file.saveTo(location.path); + } + + /// 分享 + static void share(String url, {String content = ""}) { + showModalBottomSheet( + context: Get.context!, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + constraints: const BoxConstraints( + maxWidth: 500, + ), + useSafeArea: true, + backgroundColor: Get.theme.cardColor, + builder: (context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.copy), + title: const Text("复制链接"), + onTap: () { + Get.back(); + Utils.copyText(url); + }, + ), + Visibility( + visible: content.isNotEmpty, + child: ListTile( + leading: const Icon(Icons.copy), + title: const Text("复制标题与链接"), + onTap: () { + Get.back(); + Utils.copyText("$content\n$url"); + }, + ), + ), + ListTile( + leading: const Icon(Icons.public), + title: const Text("浏览器打开"), + onTap: () { + Get.back(); + launchUrlString(url, mode: LaunchMode.externalApplication); + }, + ), + ListTile( + leading: const Icon(Icons.share), + title: const Text("系统分享"), + onTap: () { + Get.back(); + Share.share(content.isEmpty ? url : "$content\n$url"); + }, + ), + ], + ), + ); + } + + /// 检查更新 + static void checkUpdate({bool showMsg = false}) async { + try { + int currentVer = Utils.parseVersion(packageInfo.version); + CommonRequest request = CommonRequest(); + var versionInfo = await request.checkUpdate(); + if (versionInfo.versionNum > currentVer) { + Get.dialog( + AlertDialog( + title: Text( + "发现新版本 ${versionInfo.version}", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 18), + ), + content: Text( + versionInfo.versionDesc, + style: const TextStyle(fontSize: 14, height: 1.4), + ), + actionsPadding: AppStyle.edgeInsetsH12, + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextButton( + onPressed: () { + Get.back(); + }, + child: const Text("取消"), + ), + ), + AppStyle.hGap12, + Expanded( + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + ), + onPressed: () { + launchUrlString( + versionInfo.downloadUrl, + mode: LaunchMode.externalApplication, + ); + }, + child: const Text("更新"), + ), + ), + ], + ), + ], + ), + ); + } else { + if (showMsg) { + SmartDialog.showToast("当前已经是最新版本了"); + } + } + } catch (e) { + Log.logPrint(e); + if (showMsg) { + SmartDialog.showToast("检查更新失败"); + } + } + } + + /// 复制文本 + static void copyText(String text) async { + try { + await Clipboard.setData(ClipboardData(text: text)); + SmartDialog.showToast("已复制到剪切板"); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..2c2bb99 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,140 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:fluent_ui/fluent_ui.dart' show FluentLocalizations; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/models/db/comic_download_info.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/models/db/local_favorite.dart'; +import 'package:flutter_dmzj/models/db/novel_download_info.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/db/comic_history.dart'; +import 'package:flutter_dmzj/models/db/novel_history.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:flutter_dmzj/routes/app_pages.dart'; +import 'package:flutter_dmzj/services/local_storage_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:windows_single_instance/windows_single_instance.dart'; +import 'package:dynamic_color/dynamic_color.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + if (!kIsWeb && Platform.isWindows) { + await WindowsSingleInstance.ensureSingleInstance( + [], + "com.xycz.zmhx", + onSecondWindow: (args) { + Log.logPrint(args); + }, + ); + } + await Hive.initFlutter(); + //初始化服务 + await initServices(); + //设置状态栏为透明 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemUiOverlayStyle systemUiOverlayStyle = const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + systemNavigationBarColor: Colors.transparent, + ); + SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); + + runApp(const DMZJApp()); +} + +Future initServices() async { + //包信息 + Utils.packageInfo = await PackageInfo.fromPlatform(); + //本地存储 + Log.d("Init LocalStorage Service"); + await Get.put(LocalStorageService()).init(); + + //用户信息 + Log.d("Init User Service"); + Get.put(UserService()).init(); + + //注册Hive适配器 + Hive.registerAdapter(ComicHistoryAdapter()); + Hive.registerAdapter(NovelHistoryAdapter()); + Hive.registerAdapter(DownloadStatusAdapter()); + Hive.registerAdapter(ComicDownloadInfoAdapter()); + Hive.registerAdapter(NovelDownloadInfoAdapter()); + Hive.registerAdapter(LocalFavoriteAdapter()); + await Get.put(DBService()).init(); + + //初始化设置服务 + Get.put(AppSettingsService()); + + //初始化漫画下载服务 + Get.put(ComicDownloadService()).init(); + //初始化小说下载服务 + Get.put(NovelDownloadService()).init(); +} + +class AppScrollBehavior extends MaterialScrollBehavior { + @override + Set get dragDevices => PointerDeviceKind.values.toSet(); +} + +class DMZJApp extends StatelessWidget { + const DMZJApp({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return DynamicColorBuilder( + builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { + final settings = Get.find(); + settings.storeDynamicColorSchemes(lightDynamic, darkDynamic); + final useDynamic = settings.useDynamicColor.value; + return GetMaterialApp( + title: '动漫之家 X', + scrollBehavior: AppScrollBehavior(), + theme: AppStyle.getLightTheme(colorScheme: useDynamic ? lightDynamic : null), + darkTheme: AppStyle.getDarkTheme(colorScheme: useDynamic ? darkDynamic : null), + themeMode: + ThemeMode.values[Get.find().themeMode.value], + initialRoute: AppPages.kIndex, + localizationsDelegates: [ + if (PlatformUtils.isWindows) FluentLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + locale: const Locale("zh", "CN"), + supportedLocales: const [Locale("zh", "CN")], + getPages: AppPages.routes, + debugShowCheckedModeBanner: false, + navigatorObservers: [FlutterSmartDialog.observer], + builder: FlutterSmartDialog.init( + loadingBuilder: ((msg) => const AppLoaddingWidget()), + //字体大小不跟随系统变化 + builder: (context, child) => Obx( + () => MediaQuery( + data: AppSettingsService.instance.useSystemFontSize.value + ? MediaQuery.of(context) + : MediaQuery.of(context) + .copyWith(textScaler: const TextScaler.linear(1.0)), + child: child!, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/models/comic/author_model.dart b/lib/models/comic/author_model.dart new file mode 100644 index 0000000..c8aff53 --- /dev/null +++ b/lib/models/comic/author_model.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicAuthorModel { + ComicAuthorModel({ + required this.nickname, + this.description, + required this.cover, + required this.data, + }); + + factory ComicAuthorModel.fromJson(Map json) { + final List? data = + json['data'] is List ? [] : null; + if (data != null) { + for (final dynamic item in json['data']!) { + if (item != null) { + data.add( + ComicAuthorComicModel.fromJson(asT>(item)!)); + } + } + } + return ComicAuthorModel( + nickname: asT(json['nickname'])!, + description: asT(json['description']) ?? "", + cover: asT(json['cover'])!, + data: data!, + ); + } + + String nickname; + String? description; + String cover; + List data; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'nickname': nickname, + 'description': description, + 'cover': cover, + 'data': data, + }; +} + +class ComicAuthorComicModel { + ComicAuthorComicModel({ + required this.id, + required this.name, + required this.cover, + required this.status, + }); + + factory ComicAuthorComicModel.fromJson(Map json) => + ComicAuthorComicModel( + id: asT(json['id'])!, + name: asT(json['name'])!, + cover: asT(json['cover'])!, + status: asT(json['status'])!, + ); + + int id; + String name; + String cover; + String status; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'cover': cover, + 'status': status, + }; +} diff --git a/lib/models/comic/category_comic_model.dart b/lib/models/comic/category_comic_model.dart new file mode 100644 index 0000000..2212db1 --- /dev/null +++ b/lib/models/comic/category_comic_model.dart @@ -0,0 +1,157 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicCategoryComicModel { + ComicCategoryComicModel({ + required this.id, + required this.name, + this.authors, + this.types, + this.status, + this.lastUpdateChapterName, + this.lastUpdateChapterId, + this.lastUpdatetime, + this.cover, + this.comicPy, + this.isFee, + this.hotNum, + this.authorTag, + this.authorTagList, + this.copyright, + }); + + factory ComicCategoryComicModel.fromJson(Map json) { + final List? authorTagList = + json['authorTagList'] is List ? [] : null; + if (authorTagList != null) { + for (final dynamic item in json['authorTagList']!) { + if (item != null) { + authorTagList + .add(AuthorTagList.fromJson(asT>(item)!)); + } + } + } + return ComicCategoryComicModel( + id: asT(json['id'])!, + name: asT(json['name'])!, + authors: asT(json['authors']), + types: asT(json['types']), + status: asT(json['status']), + lastUpdateChapterName: asT(json['last_update_chapter_name']), + lastUpdateChapterId: asT(json['last_update_chapter_id']), + lastUpdatetime: asT(json['last_updatetime']), + cover: asT(json['cover']), + comicPy: asT(json['comic_py']), + isFee: asT(json['isFee']), + hotNum: asT(json['hotNum']), + authorTag: json['authorTag'] == null + ? null + : AuthorTag.fromJson(asT>(json['authorTag'])!), + authorTagList: authorTagList, + copyright: asT(json['copyright']), + ); + } + + int id; + String name; + String? authors; + String? types; + String? status; + String? lastUpdateChapterName; + int? lastUpdateChapterId; + int? lastUpdatetime; + String? cover; + String? comicPy; + bool? isFee; + int? hotNum; + AuthorTag? authorTag; + List? authorTagList; + int? copyright; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'authors': authors, + 'types': types, + 'status': status, + 'last_update_chapter_name': lastUpdateChapterName, + 'last_update_chapter_id': lastUpdateChapterId, + 'last_updatetime': lastUpdatetime, + 'cover': cover, + 'comic_py': comicPy, + 'isFee': isFee, + 'hotNum': hotNum, + 'authorTag': authorTag, + 'authorTagList': authorTagList, + 'copyright': copyright, + }; +} + +class AuthorTag { + AuthorTag({ + this.id, + this.tagName, + this.tagPy, + }); + + factory AuthorTag.fromJson(Map json) => AuthorTag( + id: asT(json['id']), + tagName: asT(json['tagName']), + tagPy: asT(json['tagPy']), + ); + + int? id; + String? tagName; + String? tagPy; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'tagName': tagName, + 'tagPy': tagPy, + }; +} + +class AuthorTagList { + AuthorTagList({ + this.id, + this.tagName, + this.tagPy, + }); + + factory AuthorTagList.fromJson(Map json) => AuthorTagList( + id: asT(json['id']), + tagName: asT(json['tagName']), + tagPy: asT(json['tagPy']), + ); + + int? id; + String? tagName; + String? tagPy; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'tagName': tagName, + 'tagPy': tagPy, + }; +} diff --git a/lib/models/comic/category_filter_model.dart b/lib/models/comic/category_filter_model.dart new file mode 100644 index 0000000..327fdf2 --- /dev/null +++ b/lib/models/comic/category_filter_model.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicCategoryFilterModel { + ComicCategoryFilterModel({ + required this.title, + required this.items, + }); + + factory ComicCategoryFilterModel.fromJson(Map json) { + final List? items = + json['items'] is List ? [] : null; + if (items != null) { + for (final dynamic item in json['items']!) { + if (item != null) { + items.add(ComicCategoryFilterItemModel.fromJson( + asT>(item)!)); + } + } + } + return ComicCategoryFilterModel( + title: asT(json['title'])!, + items: items!, + ); + } + + String title; + List items; + var selectId = 0.obs; + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'title': title, + 'items': items, + }; +} + +class ComicCategoryFilterItemModel { + ComicCategoryFilterItemModel({ + required this.tagId, + required this.tagName, + }); + + factory ComicCategoryFilterItemModel.fromJson(Map json) => + ComicCategoryFilterItemModel( + tagId: asT(json['tagId'])!, + tagName: asT(json['title'])!, + ); + + int tagId; + String tagName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'tag_id': tagId, + 'tag_name': tagName, + }; +} diff --git a/lib/models/comic/category_item_model.dart b/lib/models/comic/category_item_model.dart new file mode 100644 index 0000000..f3cba8e --- /dev/null +++ b/lib/models/comic/category_item_model.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicCategoryItemModel { + ComicCategoryItemModel({ + required this.tagId, + required this.title, + required this.cover, + }); + + factory ComicCategoryItemModel.fromJson(Map json) => + ComicCategoryItemModel( + tagId: asT(json['tagId'])!, + title: asT(json['title'])!, + cover: asT(json['cover'])!, + ); + + int tagId; + String title; + String cover; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'tagId': tagId, + 'title': title, + 'cover': cover, + }; +} diff --git a/lib/models/comic/chapter_detail_model.dart b/lib/models/comic/chapter_detail_model.dart new file mode 100644 index 0000000..161f65f --- /dev/null +++ b/lib/models/comic/chapter_detail_model.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicChapterDetailModel { + ComicChapterDetailModel({ + required this.chapterId, + required this.comicId, + required this.title, + required this.chapterOrder, + required this.direction, + required this.pageUrl, + required this.picnum, + required this.pageUrlHd, + }); + + factory ComicChapterDetailModel.fromJson(Map json) { + final List? pageUrl = json['page_url'] is List ? [] : null; + if (pageUrl != null) { + for (final dynamic item in json['page_url']!) { + if (item != null) { + pageUrl.add(asT(item)!); + } + } + } + + final List? pageUrlHd = + json['page_url_hd'] is List ? [] : null; + if (pageUrlHd != null) { + for (final dynamic item in json['page_url_hd']!) { + if (item != null) { + pageUrlHd.add(asT(item)!); + } + } + } + return ComicChapterDetailModel( + chapterId: asT(json['chapter_id'])!, + comicId: asT(json['comic_id'])!, + title: asT(json['title'])!, + chapterOrder: asT(json['chapter_order'])!, + direction: asT(json['direction'])!, + pageUrl: pageUrl!, + picnum: asT(json['picnum'])!, + pageUrlHd: pageUrlHd!, + ); + } + + int chapterId; + int comicId; + String title; + int chapterOrder; + int direction; + List pageUrl; + int picnum; + List pageUrlHd; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'chapter_id': chapterId, + 'comic_id': comicId, + 'title': title, + 'chapter_order': chapterOrder, + 'direction': direction, + 'page_url': pageUrl, + 'picnum': picnum, + 'page_url_hd': pageUrlHd, + }; +} diff --git a/lib/models/comic/chapter_detail_web_model.dart b/lib/models/comic/chapter_detail_web_model.dart new file mode 100644 index 0000000..2a26ed6 --- /dev/null +++ b/lib/models/comic/chapter_detail_web_model.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicChapterDetailWebModel { + ComicChapterDetailWebModel({ + required this.id, + required this.comicId, + required this.chapterName, + required this.chapterOrder, + required this.createtime, + required this.folder, + required this.pageUrl, + required this.chapterType, + required this.chaptertype, + required this.chapterTrueType, + required this.chapterNum, + required this.updatetime, + required this.sumPages, + required this.snsTag, + required this.uid, + required this.username, + required this.translatorid, + required this.translator, + required this.link, + required this.message, + required this.download, + required this.hidden, + required this.direction, + required this.filesize, + required this.highFileSize, + required this.picnum, + required this.hit, + required this.nextChapId, + required this.prevChapId, + required this.commentCount, + }); + + factory ComicChapterDetailWebModel.fromJson(Map json) { + final List? pageUrl = json['page_url'] is List ? [] : null; + if (pageUrl != null) { + for (final dynamic item in json['page_url']!) { + if (item != null) { + pageUrl.add(asT(item)!); + } + } + } + return ComicChapterDetailWebModel( + id: asT(json['id'])!, + comicId: asT(json['comic_id'])!, + chapterName: asT(json['chapter_name'])!, + chapterOrder: asT(json['chapter_order'])!, + createtime: asT(json['createtime'])!, + folder: asT(json['folder'])!, + pageUrl: pageUrl!, + chapterType: asT(json['chapter_type'])!, + chaptertype: asT(json['chaptertype'])!, + chapterTrueType: asT(json['chapter_true_type'])!, + chapterNum: asT(json['chapter_num']) ?? 0, + updatetime: asT(json['updatetime'])!, + sumPages: asT(json['sum_pages'])!, + snsTag: asT(json['sns_tag'])!, + uid: asT(json['uid'])!, + username: asT(json['username'])!, + translatorid: asT(json['translatorid'])!, + translator: asT(json['translator'])!, + link: asT(json['link'])!, + message: asT(json['message'])!, + download: asT(json['download'])!, + hidden: asT(json['hidden'])!, + direction: asT(json['direction']) ?? 0, + filesize: asT(json['filesize']) ?? 0, + highFileSize: asT(json['high_file_size']) ?? 0, + picnum: asT(json['picnum']) ?? 0, + hit: asT(json['hit'])!, + nextChapId: asT(json['next_chap_id']) ?? 0, + prevChapId: asT(json['prev_chap_id']) ?? 0, + commentCount: asT(json['comment_count'])!, + ); + } + + int id; + int comicId; + String chapterName; + int chapterOrder; + int createtime; + String folder; + List pageUrl; + int chapterType; + int chaptertype; + int chapterTrueType; + int chapterNum; + int updatetime; + int sumPages; + int snsTag; + int uid; + String username; + String translatorid; + String translator; + String link; + String message; + String download; + int hidden; + int direction; + int filesize; + int highFileSize; + int picnum; + int hit; + int nextChapId; + int prevChapId; + int commentCount; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'comic_id': comicId, + 'chapter_name': chapterName, + 'chapter_order': chapterOrder, + 'createtime': createtime, + 'folder': folder, + 'page_url': pageUrl, + 'chapter_type': chapterType, + 'chaptertype': chaptertype, + 'chapter_true_type': chapterTrueType, + 'chapter_num': chapterNum, + 'updatetime': updatetime, + 'sum_pages': sumPages, + 'sns_tag': snsTag, + 'uid': uid, + 'username': username, + 'translatorid': translatorid, + 'translator': translator, + 'link': link, + 'message': message, + 'download': download, + 'hidden': hidden, + 'direction': direction, + 'filesize': filesize, + 'high_file_size': highFileSize, + 'picnum': picnum, + 'hit': hit, + 'next_chap_id': nextChapId, + 'prev_chap_id': prevChapId, + 'comment_count': commentCount, + }; +} diff --git a/lib/models/comic/chapter_info.dart b/lib/models/comic/chapter_info.dart new file mode 100644 index 0000000..a6fe568 --- /dev/null +++ b/lib/models/comic/chapter_info.dart @@ -0,0 +1,83 @@ +import 'package:flutter_dmzj/models/comic/chapter_detail_model.dart'; +import 'package:flutter_dmzj/models/comic/chapter_detail_web_model.dart'; +import 'package:flutter_dmzj/models/db/comic_download_info.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; + +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; + +class ComicChapterDetail { + ComicChapterDetail({ + required this.chapterId, + required this.comicId, + required this.chapterOrder, + required this.direction, + required this.chapterTitle, + required this.pageUrls, + required this.picnum, + required this.commentCount, + this.isLocal = false, + }); + factory ComicChapterDetail.empty() => ComicChapterDetail( + chapterId: 0, + comicId: 0, + chapterOrder: 0, + direction: 0, + chapterTitle: "", + pageUrls: [], + picnum: 0, + commentCount: 0, + ); + factory ComicChapterDetail.fromWebApi(ComicChapterDetailWebModel item) => + ComicChapterDetail( + chapterId: item.id, + comicId: item.comicId, + chapterOrder: item.chapterOrder, + direction: item.direction, + chapterTitle: item.chapterName, + pageUrls: item.pageUrl, + picnum: item.picnum, + commentCount: item.commentCount, + ); + + factory ComicChapterDetail.fromV4(ComicChapterDetailModel item, bool useHD) => + ComicChapterDetail( + chapterId: item.chapterId.toInt(), + comicId: item.comicId.toInt(), + chapterOrder: item.chapterOrder, + direction: item.direction, + chapterTitle: item.title, + pageUrls: useHD + ? (item.pageUrlHd.isNotEmpty ? item.pageUrlHd : item.pageUrl) + : (item.pageUrl.isNotEmpty ? item.pageUrl : item.pageUrlHd), + //pageUrls: item.pageUrlHD.isNotEmpty ? item.pageUrlHD : item.pageUrl, + picnum: item.picnum, + commentCount: 0, + ); + + factory ComicChapterDetail.fromDownload(ComicDownloadInfo item) => + ComicChapterDetail( + chapterId: item.chapterId.toInt(), + comicId: item.comicId.toInt(), + chapterOrder: item.chapterSort, + direction: 1, + chapterTitle: item.chapterName, + pageUrls: item.files + .map((e) => + p.join(ComicDownloadService.instance.savePath, item.taskId, e)) + .toList(), + picnum: item.files.length, + commentCount: 0, + isLocal: true, + ); + + int chapterId; + int comicId; + int chapterOrder; + int direction; + String chapterTitle; + List pageUrls; + int picnum; + int commentCount; + bool isLocal; +} diff --git a/lib/models/comic/comic_related_model.dart b/lib/models/comic/comic_related_model.dart new file mode 100644 index 0000000..d1f6d02 --- /dev/null +++ b/lib/models/comic/comic_related_model.dart @@ -0,0 +1,146 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicRelatedModel { + ComicRelatedModel({ + required this.authorComics, + required this.themeComics, + required this.novels, + }); + + factory ComicRelatedModel.fromJson(Map json) { + final List? authorComics = + json['author_comics'] is List ? [] : null; + if (authorComics != null) { + for (final dynamic item in json['author_comics']!) { + if (item != null) { + authorComics.add(ComicRelatedAuthorModel.fromJson( + asT>(item)!)); + } + } + } + + final List? themeComics = + json['theme_comics'] is List ? [] : null; + if (themeComics != null) { + for (final dynamic item in json['theme_comics']!) { + if (item != null) { + themeComics.add( + ComicRelatedItemModel.fromJson(asT>(item)!)); + } + } + } + + final List? novels = + json['novels'] is List ? [] : null; + if (novels != null) { + for (final dynamic item in json['novels']!) { + if (item != null) { + novels.add( + ComicRelatedItemModel.fromJson(asT>(item)!)); + } + } + } + return ComicRelatedModel( + authorComics: authorComics!, + themeComics: themeComics!, + novels: novels!, + ); + } + + List authorComics; + List themeComics; + List novels; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'author_comics': authorComics, + 'theme_comics': themeComics, + 'novels': novels, + }; +} + +class ComicRelatedAuthorModel { + ComicRelatedAuthorModel({ + required this.authorName, + required this.authorId, + required this.data, + }); + + factory ComicRelatedAuthorModel.fromJson(Map json) { + final List? data = + json['data'] is List ? [] : null; + if (data != null) { + for (final dynamic item in json['data']!) { + if (item != null) { + data.add( + ComicRelatedItemModel.fromJson(asT>(item)!)); + } + } + } + return ComicRelatedAuthorModel( + authorName: asT(json['author_name'])!, + authorId: asT(json['author_id'])!, + data: data!, + ); + } + + String authorName; + int authorId; + List data; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'author_name': authorName, + 'author_id': authorId, + 'data': data, + }; +} + +class ComicRelatedItemModel { + ComicRelatedItemModel({ + required this.id, + required this.name, + required this.cover, + required this.status, + }); + + factory ComicRelatedItemModel.fromJson(Map json) => + ComicRelatedItemModel( + id: asT(json['id'])!, + name: asT(json['name'])!, + cover: asT(json['cover'])!, + status: asT(json['status'])!, + ); + + int id; + String name; + String cover; + String status; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'name': name, + 'cover': cover, + 'status': status, + }; +} diff --git a/lib/models/comic/detail_info.dart b/lib/models/comic/detail_info.dart new file mode 100644 index 0000000..d8a4ed3 --- /dev/null +++ b/lib/models/comic/detail_info.dart @@ -0,0 +1,275 @@ +import 'dart:convert'; + +import 'package:flutter_dmzj/models/comic/detail_model.dart'; +import 'package:flutter_dmzj/models/comic/detail_v1_model.dart'; +import 'package:get/get.dart'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicDetailInfo { + ComicDetailInfo({ + this.id = 0, + this.title = "", + this.direction = 0, + this.isLong = false, + this.cover = "", + this.description = "", + this.lastUpdatetime = 0, + this.lastUpdatechapterName = "", + this.firstLetter = "", + this.comicPy = "", + this.hotNum = 0, + this.hitNum = 0, + this.lastUpdateChapterId = 0, + required this.types, + required this.status, + required this.authors, + this.subscribeNum = 0, + required this.volumes, + this.isHide = false, + this.isVip = false, + }); + + factory ComicDetailInfo.empty() => ComicDetailInfo( + types: [], + status: [], + authors: [], + volumes: [], + ); + + factory ComicDetailInfo.fromV4(ComicDetailModel data) => ComicDetailInfo( + id: data.data.id, + title: data.data.title, + direction: data.data.direction ?? 0, + isLong: data.data.islong == 1, + cover: data.data.cover ?? "", + description: data.data.description ?? "", + lastUpdateChapterId: data.data.lastUpdateChapterId ?? 0, + lastUpdatechapterName: data.data.lastUpdateChapterName ?? "", + lastUpdatetime: data.data.lastUpdatetime ?? 0, + hitNum: 0, + hotNum: 0, + subscribeNum: 0, + firstLetter: data.data.firstLetter ?? "", + comicPy: data.data.comicPy ?? "", + isVip: false, + types: (data.data.types ?? []) + .map( + (e) => ComicDetailTag( + tagId: e.tagId.toInt(), + tagName: e.tagName, + ), + ) + .toList(), + status: (data.data.status ?? []) + .map( + (e) => ComicDetailTag( + tagId: e.tagId.toInt(), + tagName: e.tagName, + ), + ) + .toList(), + authors: (data.data.authors ?? []) + .map( + (e) => ComicDetailTag( + tagId: e.tagId, + tagName: e.tagName, + ), + ) + .toList(), + volumes: (data.data.chapters ?? []) + .map( + (e) => ComicDetailVolume( + title: e.title!, + chapters: RxList( + (e.data ?? []) + .map( + (x) => ComicDetailChapterItem( + chapterId: x.chapterId.toInt(), + chapterTitle: x.chapterTitle, + updateTime: x.updatetime ?? 0, + fileSize: 0, + chapterOrder: x.chapterOrder, + isVip: false, + ), + ) + .toList(), + ), + ), + ) + .toList(), + ); + factory ComicDetailInfo.fromV1(ComicDetailV1Model model, + {bool isHide = false}) { + var lastChapterId = 0; + List volumes = []; + List serialItems = []; + List aloneItems = []; + for (var item in model.list) { + serialItems.add( + ComicDetailChapterItem( + chapterId: int.tryParse(item.id) ?? 0, + chapterTitle: item.chapterName, + updateTime: int.tryParse(item.updatetime) ?? 0, + fileSize: int.tryParse(item.filesize) ?? 0, + chapterOrder: int.tryParse(item.chapterOrder) ?? 0, + ), + ); + } + for (var item in model.alone) { + aloneItems.add( + ComicDetailChapterItem( + chapterId: int.tryParse(item.id) ?? 0, + chapterTitle: item.chapterName, + updateTime: int.tryParse(item.updatetime) ?? 0, + fileSize: int.tryParse(item.filesize) ?? 0, + chapterOrder: int.tryParse(item.chapterOrder) ?? 0, + ), + ); + } + if (serialItems.isNotEmpty) { + lastChapterId = serialItems.last.chapterId; + } + volumes.add( + ComicDetailVolume( + title: isHide ? "神隐" : "连载", + chapters: RxList(serialItems)), + ); + if (aloneItems.isNotEmpty) { + volumes.add( + ComicDetailVolume( + title: isHide ? "神隐-单行本" : "单行本", + chapters: RxList(aloneItems)), + ); + } + return ComicDetailInfo( + id: int.tryParse(model.info.id) ?? 0, + title: model.info.title, + direction: int.tryParse(model.info.direction) ?? 0, + isLong: (int.tryParse(model.info.islong) ?? 0) == 1, + cover: model.info.cover, + description: model.info.description, + lastUpdateChapterId: lastChapterId, + lastUpdatechapterName: model.info.lastUpdateChapterName, + lastUpdatetime: int.tryParse(model.info.lastUpdatetime) ?? 0, + hitNum: 0, + hotNum: 0, + subscribeNum: 0, + firstLetter: model.info.firstLetter, + comicPy: "", + isHide: isHide, + types: model.info.types + .split("/") + .map( + (e) => ComicDetailTag( + tagId: 0, + tagName: e, + ), + ) + .toList(), + status: [ + ComicDetailTag( + tagId: 0, + tagName: model.info.status, + ) + ], + authors: model.info.authors + .split("/") + .map( + (e) => ComicDetailTag( + tagId: 0, + tagName: e, + ), + ) + .toList(), + volumes: volumes, + ); + } + + int id; + String title; + int direction; + bool isLong; + String cover; + String description; + int lastUpdatetime; + String lastUpdatechapterName; + String firstLetter; + String comicPy; + int hotNum; + int hitNum; + int lastUpdateChapterId; + List types = []; + List status = []; + List authors = []; + int subscribeNum; + List volumes = []; + + bool isVip; + + /// 是否神隐 + bool isHide; + + @override + String toString() { + return jsonEncode(this); + } +} + +class ComicDetailTag { + ComicDetailTag({ + required this.tagId, + required this.tagName, + }); + + int tagId; + String tagName; +} + +class ComicDetailVolume { + ComicDetailVolume({ + required this.title, + required this.chapters, + }) { + sort(); + } + + String title; + RxList chapters; + //0倒序,1正序 + var sortType = 0.obs; + var showAll = false.obs; + bool get showMoreButton => chapters.length > 15; + + void sort() { + if (sortType.value == 0) { + chapters.sort((a, b) => b.chapterOrder.compareTo(a.chapterOrder)); + } else { + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + } + } +} + +class ComicDetailChapterItem { + ComicDetailChapterItem({ + required this.chapterId, + required this.chapterTitle, + required this.updateTime, + required this.fileSize, + required this.chapterOrder, + this.isVip = false, + }); + + int chapterId; + String chapterTitle; + int updateTime; + int fileSize; + int chapterOrder; + + bool isVip; +} diff --git a/lib/models/comic/detail_model.dart b/lib/models/comic/detail_model.dart new file mode 100644 index 0000000..03b7ee8 --- /dev/null +++ b/lib/models/comic/detail_model.dart @@ -0,0 +1,352 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicDetailModel { + ComicDetailModel({ + required this.data, + required this.readingRecord, + }); + + factory ComicDetailModel.fromJson(Map json) => + ComicDetailModel( + data: ComicDetailDataModel.fromJson( + asT>(json['data'])!), + readingRecord: ComicDetailReadingRecordModel.fromJson( + asT>(json['readingRecord'])!), + ); + + ComicDetailDataModel data; + ComicDetailReadingRecordModel readingRecord; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'data': data, + 'readingRecord': readingRecord, + }; +} + +class ComicDetailDataModel { + ComicDetailDataModel({ + required this.id, + required this.title, + this.direction, + this.islong, + this.cover, + this.description, + this.lastUpdatetime, + this.lastUpdateChapterName, + this.firstLetter, + this.comicPy, + this.lastUpdateChapterId, + this.types, + this.status, + this.authors, + this.chapters, + this.dhUrlLinks, + }); + + factory ComicDetailDataModel.fromJson(Map json) { + final List? types = + json['types'] is List ? [] : null; + if (types != null) { + for (final dynamic item in json['types']!) { + if (item != null) { + types.add(ComicDetailDataTagModel.fromJson( + asT>(item)!)); + } + } + } + + final List? status = + json['status'] is List ? [] : null; + if (status != null) { + for (final dynamic item in json['status']!) { + if (item != null) { + status.add(ComicDetailDataTagModel.fromJson( + asT>(item)!)); + } + } + } + + final List? authors = + json['authors'] is List ? [] : null; + if (authors != null) { + for (final dynamic item in json['authors']!) { + if (item != null) { + authors.add(ComicDetailDataTagModel.fromJson( + asT>(item)!)); + } + } + } + + final List? chapters = + json['chapters'] is List ? [] : null; + if (chapters != null) { + for (final dynamic item in json['chapters']!) { + if (item != null) { + chapters.add(ComicDetailChapterModel.fromJson( + asT>(item)!)); + } + } + } + + final List? dhUrlLinks = + json['dh_url_links'] is List ? [] : null; + if (dhUrlLinks != null) { + for (final dynamic item in json['dh_url_links']!) { + if (item != null) { + dhUrlLinks.add(DhUrlLinks.fromJson(asT>(item)!)); + } + } + } + return ComicDetailDataModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + direction: asT(json['direction']), + islong: asT(json['islong']), + cover: asT(json['cover']), + description: asT(json['description']), + lastUpdatetime: asT(json['last_updatetime']), + lastUpdateChapterName: asT(json['last_update_chapter_name']), + firstLetter: asT(json['first_letter']), + comicPy: asT(json['comic_py']), + lastUpdateChapterId: asT(json['last_update_chapter_id']), + types: types, + status: status, + authors: authors, + chapters: chapters, + dhUrlLinks: dhUrlLinks, + ); + } + + int id; + String title; + int? direction; + int? islong; + String? cover; + String? description; + int? lastUpdatetime; + String? lastUpdateChapterName; + String? firstLetter; + String? comicPy; + int? lastUpdateChapterId; + List? types; + List? status; + List? authors; + List? chapters; + List? dhUrlLinks; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'direction': direction, + 'islong': islong, + 'cover': cover, + 'description': description, + 'last_updatetime': lastUpdatetime, + 'last_update_chapter_name': lastUpdateChapterName, + 'first_letter': firstLetter, + 'comic_py': comicPy, + 'last_update_chapter_id': lastUpdateChapterId, + 'types': types, + 'status': status, + 'authors': authors, + 'chapters': chapters, + 'dh_url_links': dhUrlLinks, + }; +} + +class ComicDetailDataTagModel { + ComicDetailDataTagModel({ + required this.tagId, + required this.tagName, + }); + + factory ComicDetailDataTagModel.fromJson(Map json) => + ComicDetailDataTagModel( + tagId: asT(json['tag_id'])!, + tagName: asT(json['tag_name'])!, + ); + + int tagId; + String tagName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'tag_id': tagId, + 'tag_name': tagName, + }; +} + +class ComicDetailChapterModel { + ComicDetailChapterModel({ + this.title, + this.data, + }); + + factory ComicDetailChapterModel.fromJson(Map json) { + final List? data = + json['data'] is List ? [] : null; + if (data != null) { + for (final dynamic item in json['data']!) { + if (item != null) { + data.add(ComicDetailChapterDataModel.fromJson( + asT>(item)!)); + } + } + } + return ComicDetailChapterModel( + title: asT(json['title']), + data: data, + ); + } + + String? title; + List? data; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'title': title, + 'data': data, + }; +} + +class ComicDetailChapterDataModel { + ComicDetailChapterDataModel({ + required this.chapterId, + required this.chapterTitle, + this.updatetime, + required this.chapterOrder, + }); + + factory ComicDetailChapterDataModel.fromJson(Map json) => + ComicDetailChapterDataModel( + chapterId: asT(json['chapter_id'])!, + chapterTitle: asT(json['chapter_title'])!, + updatetime: asT(json['updatetime']), + chapterOrder: asT(json['chapter_order'])!, + ); + + int chapterId; + String chapterTitle; + int? updatetime; + int chapterOrder; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'chapter_id': chapterId, + 'chapter_title': chapterTitle, + 'updatetime': updatetime, + 'chapter_order': chapterOrder, + }; +} + +class DhUrlLinks { + DhUrlLinks({ + this.title, + }); + + factory DhUrlLinks.fromJson(Map json) => DhUrlLinks( + title: asT(json['title']), + ); + + String? title; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'title': title, + }; +} + +class ComicDetailReadingRecordModel { + ComicDetailReadingRecordModel({ + this.typeName, + this.uid, + this.source, + this.bizId, + this.chapterId, + this.viewingTime, + this.record, + this.volumeId, + this.totalNum, + this.chapterName, + this.volumeName, + }); + + factory ComicDetailReadingRecordModel.fromJson(Map json) => + ComicDetailReadingRecordModel( + typeName: asT(json['type_name']), + uid: asT(json['uid']), + source: asT(json['source']), + bizId: asT(json['biz_id']), + chapterId: asT(json['chapter_id']), + viewingTime: asT(json['viewing_time']), + record: asT(json['record']), + volumeId: asT(json['volume_id']), + totalNum: asT(json['total_num']), + chapterName: asT(json['chapter_name']), + volumeName: asT(json['volume_name']), + ); + + String? typeName; + int? uid; + int? source; + int? bizId; + int? chapterId; + int? viewingTime; + int? record; + int? volumeId; + int? totalNum; + String? chapterName; + String? volumeName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'type_name': typeName, + 'uid': uid, + 'source': source, + 'biz_id': bizId, + 'chapter_id': chapterId, + 'viewing_time': viewingTime, + 'record': record, + 'volume_id': volumeId, + 'total_num': totalNum, + 'chapter_name': chapterName, + 'volume_name': volumeName, + }; +} diff --git a/lib/models/comic/detail_v1_model.dart b/lib/models/comic/detail_v1_model.dart new file mode 100644 index 0000000..d7069fb --- /dev/null +++ b/lib/models/comic/detail_v1_model.dart @@ -0,0 +1,186 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicDetailV1Model { + ComicDetailV1Model({ + required this.info, + required this.list, + required this.alone, + }); + + factory ComicDetailV1Model.fromJson(Map json) { + final List? list = + json['list'] is List ? [] : null; + if (list != null) { + for (final dynamic item in json['list']!) { + if (item != null) { + list.add(ComicDetailV1ChapterModel.fromJson( + asT>(item)!)); + } + } + } + + final List? alone = + json['alone'] is List ? [] : null; + if (alone != null) { + for (final dynamic item in json['alone']!) { + if (item != null) { + alone.add(ComicDetailV1ChapterModel.fromJson( + asT>(item)!)); + } + } + } + + return ComicDetailV1Model( + info: ComicDetailV1InfoModel.fromJson( + asT>(json['info'])!), + list: list!, + alone: alone!, + ); + } + + ComicDetailV1InfoModel info; + List list; + List alone; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'info': info, + 'list': list, + 'alone': alone, + }; +} + +class ComicDetailV1InfoModel { + ComicDetailV1InfoModel({ + required this.id, + required this.title, + required this.subtitle, + required this.types, + required this.zone, + required this.status, + required this.lastUpdateChapterName, + required this.lastUpdatetime, + required this.cover, + required this.authors, + required this.description, + required this.firstLetter, + required this.direction, + required this.islong, + required this.copyright, + }); + + factory ComicDetailV1InfoModel.fromJson(Map json) => + ComicDetailV1InfoModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + subtitle: asT(json['subtitle'])!, + types: asT(json['types'])!, + zone: asT(json['zone'])!, + status: asT(json['status'])!, + lastUpdateChapterName: asT(json['last_update_chapter_name'])!, + lastUpdatetime: asT(json['last_updatetime'])!, + cover: asT(json['cover'])!, + authors: asT(json['authors'])!, + description: asT(json['description'])!, + firstLetter: asT(json['first_letter'])!, + direction: asT(json['direction'])!, + islong: asT(json['islong'])!, + copyright: asT(json['copyright'])!, + ); + + String id; + String title; + String subtitle; + String types; + String zone; + String status; + String lastUpdateChapterName; + String lastUpdatetime; + String cover; + String authors; + String description; + String firstLetter; + String direction; + String islong; + String copyright; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'subtitle': subtitle, + 'types': types, + 'zone': zone, + 'status': status, + 'last_update_chapter_name': lastUpdateChapterName, + 'last_updatetime': lastUpdatetime, + 'cover': cover, + 'authors': authors, + 'description': description, + 'first_letter': firstLetter, + 'direction': direction, + 'islong': islong, + 'copyright': copyright, + }; +} + +class ComicDetailV1ChapterModel { + ComicDetailV1ChapterModel({ + required this.id, + required this.comicId, + required this.chapterName, + required this.chapterOrder, + required this.filesize, + required this.createtime, + required this.updatetime, + }); + + factory ComicDetailV1ChapterModel.fromJson(Map json) => + ComicDetailV1ChapterModel( + id: asT(json['id'])!, + comicId: asT(json['comic_id'])!, + chapterName: asT(json['chapter_name'])!, + chapterOrder: asT(json['chapter_order'])!, + filesize: asT(json['filesize'])!, + createtime: asT(json['createtime'])!, + updatetime: asT(json['updatetime'])!, + ); + + String id; + String comicId; + String chapterName; + String chapterOrder; + String filesize; + String createtime; + String updatetime; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'comic_id': comicId, + 'chapter_name': chapterName, + 'chapter_order': chapterOrder, + 'filesize': filesize, + 'createtime': createtime, + 'updatetime': updatetime, + }; +} diff --git a/lib/models/comic/rank_item_model.dart b/lib/models/comic/rank_item_model.dart new file mode 100644 index 0000000..9b8837a --- /dev/null +++ b/lib/models/comic/rank_item_model.dart @@ -0,0 +1,70 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicRankListItemModel { + ComicRankListItemModel({ + required this.comicId, + required this.title, + this.authors, + this.status, + this.cover, + this.types, + this.lastUpdatetime, + this.lastUpdateChapterName, + this.comicPy, + this.num, + this.tagId, + }); + + factory ComicRankListItemModel.fromJson(Map json) => + ComicRankListItemModel( + comicId: asT(json['comic_id'])!, + title: asT(json['title'])!, + authors: asT(json['authors']), + status: asT(json['status']), + cover: asT(json['cover']), + types: asT(json['types']), + lastUpdatetime: asT(json['last_updatetime']), + lastUpdateChapterName: asT(json['last_update_chapter_name']), + comicPy: asT(json['comic_py']), + num: asT(json['num']), + tagId: asT(json['tag_id']), + ); + + int comicId; + String title; + String? authors; + String? status; + String? cover; + String? types; + int? lastUpdatetime; + String? lastUpdateChapterName; + String? comicPy; + int? num; + int? tagId; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'comic_id': comicId, + 'title': title, + 'authors': authors, + 'status': status, + 'cover': cover, + 'types': types, + 'last_updatetime': lastUpdatetime, + 'last_update_chapter_name': lastUpdateChapterName, + 'comic_py': comicPy, + 'num': num, + 'tag_id': tagId, + }; +} diff --git a/lib/models/comic/recommend_model.dart b/lib/models/comic/recommend_model.dart new file mode 100644 index 0000000..cd8f97e --- /dev/null +++ b/lib/models/comic/recommend_model.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicRecommendModel { + ComicRecommendModel({ + required this.categoryId, + required this.title, + required this.sort, + required this.data, + }); + + factory ComicRecommendModel.fromJson(Map json) { + final List? data = + json['data'] is List ? [] : null; + if (data != null) { + for (final dynamic item in json['data']!) { + if (item != null) { + data.add(ComicRecommendItemModel.fromJson( + asT>(item)!)); + } + } + } + return ComicRecommendModel( + categoryId: asT(json['category_id'])!, + title: asT(json['title'])!, + sort: asT(json['sort'])!, + data: data ?? [], + ); + } + + int categoryId; + String title; + int sort; + List data; + int page = 1; + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'category_id': categoryId, + 'title': title, + 'sort': sort, + 'data': data, + }; +} + +class ComicRecommendItemModel { + ComicRecommendItemModel({ + required this.cover, + required this.title, + this.subTitle, + this.type, + this.url, + required this.objId, + this.status, + this.id, + }); + + factory ComicRecommendItemModel.fromJson(Map json) => + ComicRecommendItemModel( + id: asT(json['id']), + cover: asT(json['cover'])!, + title: asT(json['title'])!, + subTitle: asT(json['sub_title']), + type: asT(json['type']), + url: asT(json['url']), + objId: asT(json['obj_id']), + status: asT(json['status']), + ); + int? id; + String cover; + String title; + String? subTitle; + int? type; + String? url; + int? objId; + String? status; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'cover': cover, + 'title': title, + 'sub_title': subTitle, + 'type': type, + 'url': url, + 'obj_id': objId, + 'status': status, + }; +} diff --git a/lib/models/comic/search_item.dart b/lib/models/comic/search_item.dart new file mode 100644 index 0000000..fa9005c --- /dev/null +++ b/lib/models/comic/search_item.dart @@ -0,0 +1,36 @@ +import 'package:flutter_dmzj/models/comic/search_model.dart'; +import 'package:flutter_dmzj/models/comic/web_search_model.dart'; + +class SearchComicItem { + final int comicId; + final String title; + final String cover; + final String author; + final String lastChapterName; + final String tags; + SearchComicItem({ + required this.author, + required this.comicId, + required this.cover, + required this.lastChapterName, + required this.tags, + required this.title, + }); + + factory SearchComicItem.fromApi(ComicSearchModel item) => SearchComicItem( + author: item.authors ?? "", + comicId: item.id, + cover: item.cover ?? "", + lastChapterName: item.lastName ?? "", + tags: item.types ?? "", + title: item.title, + ); + factory SearchComicItem.fromWeb(ComicWebSearchModel item) => SearchComicItem( + author: item.comicAuthor, + comicId: item.id, + cover: item.cover, + lastChapterName: item.lastUpdateChapterName, + tags: "/", + title: item.comicName, + ); +} diff --git a/lib/models/comic/search_model.dart b/lib/models/comic/search_model.dart new file mode 100644 index 0000000..047f24a --- /dev/null +++ b/lib/models/comic/search_model.dart @@ -0,0 +1,70 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicSearchModel { + ComicSearchModel({ + required this.id, + this.authors, + this.copyright, + this.cover, + this.hotHits, + this.lastName, + this.status, + required this.title, + this.types, + this.aliasName, + this.comicPy, + }); + + factory ComicSearchModel.fromJson(Map json) => + ComicSearchModel( + id: asT(json['id'])!, + authors: asT(json['authors']), + copyright: asT(json['copyright']), + cover: asT(json['cover']), + hotHits: asT(json['hot_hits']), + lastName: asT(json['last_name']), + status: asT(json['status']), + title: asT(json['title'])!, + types: asT(json['types']), + aliasName: asT(json['alias_name']), + comicPy: asT(json['comic_py']), + ); + + int id; + String? authors; + int? copyright; + String? cover; + int? hotHits; + String? lastName; + String? status; + String title; + String? types; + String? aliasName; + String? comicPy; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'authors': authors, + 'copyright': copyright, + 'cover': cover, + 'hot_hits': hotHits, + 'last_name': lastName, + 'status': status, + 'title': title, + 'types': types, + 'alias_name': aliasName, + 'comic_py': comicPy, + }; +} diff --git a/lib/models/comic/special_detail_model.dart b/lib/models/comic/special_detail_model.dart new file mode 100644 index 0000000..f9502e0 --- /dev/null +++ b/lib/models/comic/special_detail_model.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicSpecialDetailModel { + ComicSpecialDetailModel({ + required this.mobileHeaderPic, + required this.title, + required this.pageUrl, + required this.description, + required this.comics, + required this.commentAmount, + }); + + factory ComicSpecialDetailModel.fromJson(Map json) { + final List? comics = + json['comics'] is List ? [] : null; + if (comics != null) { + for (final dynamic item in json['comics']!) { + if (item != null) { + comics.add(ComicSpecialComicModel.fromJson( + asT>(item)!)); + } + } + } + return ComicSpecialDetailModel( + mobileHeaderPic: asT(json['mobile_header_pic'])!, + title: asT(json['title'])!, + pageUrl: asT(json['page_url'])!, + description: asT(json['description'])!, + comics: comics!, + commentAmount: asT(json['comment_amount'])!, + ); + } + + String mobileHeaderPic; + String title; + String pageUrl; + String description; + List comics; + int commentAmount; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'mobile_header_pic': mobileHeaderPic, + 'title': title, + 'page_url': pageUrl, + 'description': description, + 'comics': comics, + 'comment_amount': commentAmount, + }; +} + +class ComicSpecialComicModel { + ComicSpecialComicModel({ + required this.cover, + required this.recommendBrief, + required this.recommendReason, + required this.id, + required this.name, + required this.aliasName, + }); + + factory ComicSpecialComicModel.fromJson(Map json) => + ComicSpecialComicModel( + cover: asT(json['cover']) ?? "", + recommendBrief: asT(json['recommend_brief']) ?? "", + recommendReason: asT(json['recommend_reason']) ?? "", + id: asT(json['id'])!, + name: asT(json['name']) ?? "", + aliasName: asT(json['alias_name']) ?? "", + ); + + String cover; + String recommendBrief; + String recommendReason; + int id; + String name; + String aliasName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'cover': cover, + 'recommend_brief': recommendBrief, + 'recommend_reason': recommendReason, + 'id': id, + 'name': name, + 'alias_name': aliasName, + }; +} diff --git a/lib/models/comic/special_model.dart b/lib/models/comic/special_model.dart new file mode 100644 index 0000000..85aff46 --- /dev/null +++ b/lib/models/comic/special_model.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicSpecialModel { + ComicSpecialModel({ + required this.id, + required this.title, + required this.shortTitle, + required this.createTime, + required this.smallCover, + required this.pageType, + required this.sort, + required this.pageUrl, + }); + + factory ComicSpecialModel.fromJson(Map json) => + ComicSpecialModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + shortTitle: asT(json['short_title'])!, + createTime: asT(json['create_time'])!, + smallCover: asT(json['small_cover'])!, + pageType: asT(json['page_type'])!, + sort: asT(json['sort'])!, + pageUrl: asT(json['page_url'])!, + ); + + int id; + String title; + String shortTitle; + int createTime; + String smallCover; + int pageType; + int sort; + String pageUrl; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'short_title': shortTitle, + 'create_time': createTime, + 'small_cover': smallCover, + 'page_type': pageType, + 'sort': sort, + 'page_url': pageUrl, + }; +} diff --git a/lib/models/comic/update_item_model.dart b/lib/models/comic/update_item_model.dart new file mode 100644 index 0000000..f1dbdbe --- /dev/null +++ b/lib/models/comic/update_item_model.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicUpdateItemModel { + ComicUpdateItemModel({ + required this.comicId, + required this.title, + this.islong, + this.authors, + this.types, + this.cover, + this.status, + this.lastUpdateChapterName, + this.lastUpdateChapterId, + this.lastUpdatetime, + }); + + factory ComicUpdateItemModel.fromJson(Map json) => + ComicUpdateItemModel( + comicId: asT(json['comic_id'])!, + title: asT(json['title'])!, + islong: asT(json['islong']), + authors: asT(json['authors']), + types: asT(json['types']), + cover: asT(json['cover']), + status: asT(json['status']), + lastUpdateChapterName: asT(json['last_update_chapter_name']), + lastUpdateChapterId: asT(json['last_update_chapter_id']), + lastUpdatetime: asT(json['last_updatetime']), + ); + + int comicId; + String title; + int? islong; + String? authors; + String? types; + String? cover; + String? status; + String? lastUpdateChapterName; + int? lastUpdateChapterId; + int? lastUpdatetime; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'comic_id': comicId, + 'title': title, + 'islong': islong, + 'authors': authors, + 'types': types, + 'cover': cover, + 'status': status, + 'last_update_chapter_name': lastUpdateChapterName, + 'last_update_chapter_id': lastUpdateChapterId, + 'last_updatetime': lastUpdatetime, + }; +} diff --git a/lib/models/comic/view_point_model.dart b/lib/models/comic/view_point_model.dart new file mode 100644 index 0000000..715196c --- /dev/null +++ b/lib/models/comic/view_point_model.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicViewPointModel { + ComicViewPointModel({ + required this.id, + required this.uid, + required this.content, + required this.num, + required this.page, + }); + + factory ComicViewPointModel.fromJson(Map json) => + ComicViewPointModel( + id: asT(json['id'])!, + uid: asT(json['uid'])!, + content: asT(json['content'])!, + num: (asT(json['num']) ?? 0).obs, + page: asT(json['page'])!, + ); + + int id; + int uid; + String content; + RxInt num; + int page; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'uid': uid, + 'content': content, + 'num': num, + 'page': page, + }; +} diff --git a/lib/models/comic/web_search_model.dart b/lib/models/comic/web_search_model.dart new file mode 100644 index 0000000..39c82bc --- /dev/null +++ b/lib/models/comic/web_search_model.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class ComicWebSearchModel { + ComicWebSearchModel({ + required this.id, + required this.comicName, + required this.comicAuthor, + required this.comicCover, + required this.cover, + required this.lastUpdateChapterName, + required this.comicUrlRaw, + required this.comicUrl, + required this.status, + required this.chapterUrlRaw, + required this.chapterUrl, + }); + + factory ComicWebSearchModel.fromJson(Map json) => + ComicWebSearchModel( + id: asT(json['id'])!, + comicName: asT(json['comic_name'])!, + comicAuthor: asT(json['comic_author']) ?? "", + comicCover: asT(json['comic_cover']) ?? "", + cover: asT(json['cover']) ?? "", + lastUpdateChapterName: + asT(json['last_update_chapter_name']) ?? "", + comicUrlRaw: asT(json['comic_url_raw']) ?? "", + comicUrl: asT(json['comic_url']) ?? "", + status: asT(json['status']) ?? "", + chapterUrlRaw: asT(json['chapter_url_raw']) ?? "", + chapterUrl: asT(json['chapter_url']) ?? "", + ); + + int id; + String comicName; + String comicAuthor; + String comicCover; + String cover; + String lastUpdateChapterName; + String comicUrlRaw; + String comicUrl; + String status; + String chapterUrlRaw; + String chapterUrl; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'comic_name': comicName, + 'comic_author': comicAuthor, + 'comic_cover': comicCover, + 'cover': cover, + 'last_update_chapter_name': lastUpdateChapterName, + 'comic_url_raw': comicUrlRaw, + 'comic_url': comicUrl, + 'status': status, + 'chapter_url_raw': chapterUrlRaw, + 'chapter_url': chapterUrl, + }; +} diff --git a/lib/models/comment/comment_item.dart b/lib/models/comment/comment_item.dart new file mode 100644 index 0000000..64a08e3 --- /dev/null +++ b/lib/models/comment/comment_item.dart @@ -0,0 +1,58 @@ +import 'package:get/get.dart'; + +/// 动漫之家评论接口太TM混乱了 +/// 使用此类统一Model + +class CommentItem { + CommentItem({ + required this.id, + required this.objId, + required this.content, + required this.photo, + required this.createTime, + required this.images, + required this.likeAmount, + required this.nickname, + required this.replyAmount, + required this.userId, + required this.gender, + required this.type, + required this.originId, + this.isEmpty = false, + }); + + factory CommentItem.createEmpty() { + return CommentItem( + id: 0, + objId: 0, + content: "该评论不存在,可能已被删除", + photo: "", + createTime: 0, + images: [], + likeAmount: 0.obs, + nickname: "-", + replyAmount: 0, + userId: 0, + gender: 0, + type: 0, + originId: 0, + isEmpty: true, + ); + } + + int id; + int objId; + String content; + int createTime; + Rx likeAmount; + int replyAmount; + String nickname; + String photo; + List images; + int userId; + List parents = []; + bool isEmpty; + int gender; + int type; + int originId; +} diff --git a/lib/models/comment/user_comment_item.dart b/lib/models/comment/user_comment_item.dart new file mode 100644 index 0000000..c029ece --- /dev/null +++ b/lib/models/comment/user_comment_item.dart @@ -0,0 +1,123 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserCommentItem { + UserCommentItem({ + required this.commentId, + required this.content, + required this.replyAmount, + required this.likeAmount, + this.originCommentId, + required this.objId, + required this.createTime, + this.toCommentId, + required this.objCover, + required this.objName, + this.pageUrl, + this.mastercomment, + }); + + factory UserCommentItem.fromJson(Map json) => + UserCommentItem( + commentId: asT(json['comment_id']) ?? 0, + content: asT(json['content']) ?? '', + replyAmount: asT(json['reply_amount']) ?? 0, + likeAmount: asT(json['like_amount']) ?? 0, + originCommentId: asT(json['origin_comment_id']), + objId: asT(json['obj_id']) ?? 0, + createTime: asT(json['create_time']) ?? 0, + toCommentId: asT(json['to_comment_id']), + objCover: asT(json['obj_cover']) ?? '', + objName: asT(json['obj_name']) ?? '', + pageUrl: asT(json['page_url']), + mastercomment: json['masterComment'] == null + ? null + : UserMasterComment.fromJson( + asT>(json['masterComment'])!), + ); + + int commentId; + String content; + int replyAmount; + int likeAmount; + int? originCommentId; + int objId; + int createTime; + int? toCommentId; + String objCover; + String objName; + String? pageUrl; + UserMasterComment? mastercomment; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'comment_id': commentId, + 'content': content, + 'reply_amount': replyAmount, + 'like_amount': likeAmount, + 'origin_comment_id': originCommentId, + 'obj_id': objId, + 'create_time': createTime, + 'to_comment_id': toCommentId, + 'obj_cover': objCover, + 'obj_name': objName, + 'page_url': pageUrl, + 'masterComment': mastercomment, + }; +} + +class UserMasterComment { + UserMasterComment({ + required this.id, + required this.content, + this.senderUid, + this.likeAmount, + required this.createTime, + this.replyAmount, + required this.nickname, + }); + + factory UserMasterComment.fromJson(Map json) => + UserMasterComment( + id: asT(json['id'])!, + content: asT(json['content'])!, + senderUid: asT(json['sender_uid']), + likeAmount: asT(json['like_amount']), + createTime: asT(json['create_time'])!, + replyAmount: asT(json['reply_amount']), + nickname: asT(json['nickname'])!, + ); + + int id; + String content; + int? senderUid; + int? likeAmount; + int createTime; + int? replyAmount; + String nickname; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'content': content, + 'sender_uid': senderUid, + 'like_amount': likeAmount, + 'create_time': createTime, + 'reply_amount': replyAmount, + 'nickname': nickname, + }; +} diff --git a/lib/models/db/comic_download_info.dart b/lib/models/db/comic_download_info.dart new file mode 100644 index 0000000..8123dcb --- /dev/null +++ b/lib/models/db/comic_download_info.dart @@ -0,0 +1,94 @@ +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:hive/hive.dart'; +part 'comic_download_info.g.dart'; + +@HiveType(typeId: 3) +class ComicDownloadInfo { + ComicDownloadInfo({ + required this.addTime, + required this.chapterId, + required this.chapterSort, + required this.comicCover, + required this.comicId, + required this.comicName, + required this.files, + required this.index, + required this.savePath, + required this.status, + required this.taskId, + required this.total, + required this.volumeName, + required this.urls, + required this.chapterName, + required this.isVip, + required this.isLongComic, + }); + + ///TaskID 任务,由漫画ID_章节ID组成 + @HiveField(0) + String taskId; + + ///ComicID 漫画ID + @HiveField(1) + int comicId; + + ///ComicName 漫画名称 + @HiveField(2) + String comicName; + + ///ComicCover 漫画封面 + @HiveField(3) + String comicCover; + + ///ChapterID 章节ID + @HiveField(4) + int chapterId; + + @HiveField(5) + String chapterName; + + ///VoulmeName 分卷名称 + @HiveField(6) + String volumeName; + + ///ChapterSort 排序 + @HiveField(7) + int chapterSort; + + ///SavePath 存储路径 + @HiveField(8) + String savePath; + + ///Files 文件列表 + @HiveField(9) + List files; + + ///Index 当前下载页数 + @HiveField(10) + int index; + + ///Total 总计页数 + @HiveField(11) + int total; + + ///Status 当前状态 + @HiveField(12) + DownloadStatus status; + + ///AddTime 任务时间 + @HiveField(13) + DateTime addTime; + + /// 下载图片链接 + @HiveField(14) + List urls; + + /// 是否VIP章节 + /// * 暂时没啥用,总之先加上 + @HiveField(15) + bool isVip; + + /// 是否为条漫 + @HiveField(16) + bool isLongComic; +} diff --git a/lib/models/db/comic_download_info.g.dart b/lib/models/db/comic_download_info.g.dart new file mode 100644 index 0000000..3693f64 --- /dev/null +++ b/lib/models/db/comic_download_info.g.dart @@ -0,0 +1,89 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'comic_download_info.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ComicDownloadInfoAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + ComicDownloadInfo read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ComicDownloadInfo( + addTime: fields[13] as DateTime, + chapterId: fields[4] as int, + chapterSort: fields[7] as int, + comicCover: fields[3] as String, + comicId: fields[1] as int, + comicName: fields[2] as String, + files: (fields[9] as List).cast(), + index: fields[10] as int, + savePath: fields[8] as String, + status: fields[12] as DownloadStatus, + taskId: fields[0] as String, + total: fields[11] as int, + volumeName: fields[6] as String, + urls: (fields[14] as List).cast(), + chapterName: fields[5] as String, + isVip: (fields[15] ?? false) as bool, + isLongComic: (fields[16] ?? false) as bool, + ); + } + + @override + void write(BinaryWriter writer, ComicDownloadInfo obj) { + writer + ..writeByte(17) + ..writeByte(0) + ..write(obj.taskId) + ..writeByte(1) + ..write(obj.comicId) + ..writeByte(2) + ..write(obj.comicName) + ..writeByte(3) + ..write(obj.comicCover) + ..writeByte(4) + ..write(obj.chapterId) + ..writeByte(5) + ..write(obj.chapterName) + ..writeByte(6) + ..write(obj.volumeName) + ..writeByte(7) + ..write(obj.chapterSort) + ..writeByte(8) + ..write(obj.savePath) + ..writeByte(9) + ..write(obj.files) + ..writeByte(10) + ..write(obj.index) + ..writeByte(11) + ..write(obj.total) + ..writeByte(12) + ..write(obj.status) + ..writeByte(13) + ..write(obj.addTime) + ..writeByte(14) + ..write(obj.urls) + ..writeByte(15) + ..write(obj.isVip) + ..writeByte(16) + ..write(obj.isLongComic); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ComicDownloadInfoAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/db/comic_history.dart b/lib/models/db/comic_history.dart new file mode 100644 index 0000000..af2b9ce --- /dev/null +++ b/lib/models/db/comic_history.dart @@ -0,0 +1,36 @@ +import 'package:hive/hive.dart'; +part 'comic_history.g.dart'; + +@HiveType(typeId: 1) +class ComicHistory { + ComicHistory({ + required this.comicId, + required this.chapterId, + required this.comicName, + required this.comicCover, + required this.chapterName, + required this.updateTime, + required this.page, + }); + + @HiveField(0) + int comicId; + + @HiveField(1) + int chapterId; + + @HiveField(2) + String comicName; + + @HiveField(3) + String comicCover; + + @HiveField(4) + String chapterName; + + @HiveField(5) + int page; + + @HiveField(6) + DateTime updateTime; +} diff --git a/lib/models/db/comic_history.g.dart b/lib/models/db/comic_history.g.dart new file mode 100644 index 0000000..2eec9ef --- /dev/null +++ b/lib/models/db/comic_history.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'comic_history.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ComicHistoryAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + ComicHistory read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ComicHistory( + comicId: fields[0] as int, + chapterId: fields[1] as int, + comicName: fields[2] as String, + comicCover: fields[3] as String, + chapterName: fields[4] as String, + updateTime: fields[6] as DateTime, + page: fields[5] as int, + ); + } + + @override + void write(BinaryWriter writer, ComicHistory obj) { + writer + ..writeByte(7) + ..writeByte(0) + ..write(obj.comicId) + ..writeByte(1) + ..write(obj.chapterId) + ..writeByte(2) + ..write(obj.comicName) + ..writeByte(3) + ..write(obj.comicCover) + ..writeByte(4) + ..write(obj.chapterName) + ..writeByte(5) + ..write(obj.page) + ..writeByte(6) + ..write(obj.updateTime); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ComicHistoryAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/db/download_status.dart b/lib/models/db/download_status.dart new file mode 100644 index 0000000..9521fe0 --- /dev/null +++ b/lib/models/db/download_status.dart @@ -0,0 +1,46 @@ +import 'package:hive/hive.dart'; +part 'download_status.g.dart'; + +/// 下载状态 +@HiveType(typeId: 4) +enum DownloadStatus { + /// 等待下载中 + @HiveField(0) + wait, + + /// 正在读取章节信息 + @HiveField(1) + loadding, + + /// 下载中 + @HiveField(2) + downloading, + + /// 使用数据,自动暂停,当网络切换时恢复下载 + @HiveField(3) + pauseCellular, + + /// 暂停 + @HiveField(4) + pause, + + /// 已完成 + @HiveField(5) + complete, + + /// 读取信息时出现错误 + @HiveField(6) + errorLoad, + + /// 下载出错 + @HiveField(7) + error, + + /// 已取消 + @HiveField(8) + cancel, + + /// 等待网络连接 + @HiveField(9) + waitNetwork, +} diff --git a/lib/models/db/download_status.g.dart b/lib/models/db/download_status.g.dart new file mode 100644 index 0000000..2923d87 --- /dev/null +++ b/lib/models/db/download_status.g.dart @@ -0,0 +1,86 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'download_status.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class DownloadStatusAdapter extends TypeAdapter { + @override + final int typeId = 4; + + @override + DownloadStatus read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return DownloadStatus.wait; + case 1: + return DownloadStatus.loadding; + case 2: + return DownloadStatus.downloading; + case 3: + return DownloadStatus.pauseCellular; + case 4: + return DownloadStatus.pause; + case 5: + return DownloadStatus.complete; + case 6: + return DownloadStatus.errorLoad; + case 7: + return DownloadStatus.error; + case 8: + return DownloadStatus.cancel; + case 9: + return DownloadStatus.waitNetwork; + default: + return DownloadStatus.wait; + } + } + + @override + void write(BinaryWriter writer, DownloadStatus obj) { + switch (obj) { + case DownloadStatus.wait: + writer.writeByte(0); + break; + case DownloadStatus.loadding: + writer.writeByte(1); + break; + case DownloadStatus.downloading: + writer.writeByte(2); + break; + case DownloadStatus.pauseCellular: + writer.writeByte(3); + break; + case DownloadStatus.pause: + writer.writeByte(4); + break; + case DownloadStatus.complete: + writer.writeByte(5); + break; + case DownloadStatus.errorLoad: + writer.writeByte(6); + break; + case DownloadStatus.error: + writer.writeByte(7); + break; + case DownloadStatus.cancel: + writer.writeByte(8); + break; + case DownloadStatus.waitNetwork: + writer.writeByte(9); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DownloadStatusAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/db/local_favorite.dart b/lib/models/db/local_favorite.dart new file mode 100644 index 0000000..e445eef --- /dev/null +++ b/lib/models/db/local_favorite.dart @@ -0,0 +1,39 @@ +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +part 'local_favorite.g.dart'; + +@HiveType(typeId: 6) +class LocalFavorite { + LocalFavorite({ + required this.id, + required this.objId, + required this.title, + required this.cover, + required this.type, + required this.updateTime, + }); + + @HiveField(0) + String id; + + String get hiveId => "${type}_$objId"; + + @HiveField(1) + int objId; + + @HiveField(2) + String title; + + @HiveField(3) + String cover; + + /// 类型,对应app_constant,漫画或小说 + @HiveField(4) + int type; + + @HiveField(5) + DateTime updateTime; + + //是否被选中 + Rx isChecked = false.obs; +} diff --git a/lib/models/db/local_favorite.g.dart b/lib/models/db/local_favorite.g.dart new file mode 100644 index 0000000..c05b069 --- /dev/null +++ b/lib/models/db/local_favorite.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'local_favorite.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class LocalFavoriteAdapter extends TypeAdapter { + @override + final int typeId = 6; + + @override + LocalFavorite read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return LocalFavorite( + id: fields[0] as String, + objId: fields[1] as int, + title: fields[2] as String, + cover: fields[3] as String, + type: fields[4] as int, + updateTime: fields[5] as DateTime, + ); + } + + @override + void write(BinaryWriter writer, LocalFavorite obj) { + writer + ..writeByte(6) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.objId) + ..writeByte(2) + ..write(obj.title) + ..writeByte(3) + ..write(obj.cover) + ..writeByte(4) + ..write(obj.type) + ..writeByte(5) + ..write(obj.updateTime); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LocalFavoriteAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/db/novel_download_info.dart b/lib/models/db/novel_download_info.dart new file mode 100644 index 0000000..6fafa59 --- /dev/null +++ b/lib/models/db/novel_download_info.dart @@ -0,0 +1,105 @@ +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:hive/hive.dart'; +part 'novel_download_info.g.dart'; + +@HiveType(typeId: 5) +class NovelDownloadInfo { + NovelDownloadInfo({ + required this.addTime, + required this.chapterId, + required this.chapterSort, + required this.novelCover, + required this.novelId, + required this.novelName, + required this.fileName, + required this.imageFiles, + required this.savePath, + required this.status, + required this.taskId, + required this.isImage, + required this.volumeName, + required this.progress, + required this.chapterName, + required this.volumeID, + required this.isVip, + required this.volumeOrder, + required this.imageUrls, + }); + + ///TaskID 任务,由小说ID_章节ID组成 + @HiveField(0) + String taskId; + + ///NovelID 小说ID + @HiveField(1) + int novelId; + + ///NovelName 小说名称 + @HiveField(2) + String novelName; + + ///NovelCover 小说封面 + @HiveField(3) + String novelCover; + + ///ChapterID 章节ID + @HiveField(4) + int chapterId; + + ///chapterName 章节名称 + @HiveField(5) + String chapterName; + + ///VoulmeID 分卷ID + @HiveField(6) + int volumeID; + + ///VoulmeName 分卷名称 + @HiveField(7) + String volumeName; + + ///volumeOrder 分卷排序 + @HiveField(8) + int volumeOrder; + + ///ChapterSort 排序 + @HiveField(9) + int chapterSort; + + ///SavePath 存储路径 + @HiveField(10) + String savePath; + + ///Files 文件列表 + @HiveField(11) + String fileName; + + ///isImage 是否为插图 + @HiveField(12) + bool isImage; + + /// 图片保存路径 + @HiveField(13) + List imageFiles; + + ///下载进度 0-100 + @HiveField(14) + int progress; + + ///Status 当前状态 + @HiveField(15) + DownloadStatus status; + + ///AddTime 任务时间 + @HiveField(16) + DateTime addTime; + + /// 是否VIP章节 + /// * 暂时没啥用,总之先加上 + @HiveField(17) + bool isVip; + + /// 下载图片链接 + @HiveField(18) + List imageUrls; +} diff --git a/lib/models/db/novel_download_info.g.dart b/lib/models/db/novel_download_info.g.dart new file mode 100644 index 0000000..b374643 --- /dev/null +++ b/lib/models/db/novel_download_info.g.dart @@ -0,0 +1,95 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'novel_download_info.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class NovelDownloadInfoAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + NovelDownloadInfo read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return NovelDownloadInfo( + addTime: fields[16] as DateTime, + chapterId: fields[4] as int, + chapterSort: fields[9] as int, + novelCover: fields[3] as String, + novelId: fields[1] as int, + novelName: fields[2] as String, + fileName: fields[11] as String, + imageFiles: (fields[13] as List).cast(), + savePath: fields[10] as String, + status: fields[15] as DownloadStatus, + taskId: fields[0] as String, + isImage: fields[12] as bool, + volumeName: fields[7] as String, + progress: fields[14] as int, + chapterName: fields[5] as String, + volumeID: fields[6] as int, + isVip: fields[17] as bool, + volumeOrder: fields[8] as int, + imageUrls: (fields[18] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, NovelDownloadInfo obj) { + writer + ..writeByte(19) + ..writeByte(0) + ..write(obj.taskId) + ..writeByte(1) + ..write(obj.novelId) + ..writeByte(2) + ..write(obj.novelName) + ..writeByte(3) + ..write(obj.novelCover) + ..writeByte(4) + ..write(obj.chapterId) + ..writeByte(5) + ..write(obj.chapterName) + ..writeByte(6) + ..write(obj.volumeID) + ..writeByte(7) + ..write(obj.volumeName) + ..writeByte(8) + ..write(obj.volumeOrder) + ..writeByte(9) + ..write(obj.chapterSort) + ..writeByte(10) + ..write(obj.savePath) + ..writeByte(11) + ..write(obj.fileName) + ..writeByte(12) + ..write(obj.isImage) + ..writeByte(13) + ..write(obj.imageFiles) + ..writeByte(14) + ..write(obj.progress) + ..writeByte(15) + ..write(obj.status) + ..writeByte(16) + ..write(obj.addTime) + ..writeByte(17) + ..write(obj.isVip) + ..writeByte(18) + ..write(obj.imageUrls); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NovelDownloadInfoAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/db/novel_history.dart b/lib/models/db/novel_history.dart new file mode 100644 index 0000000..71b4a8b --- /dev/null +++ b/lib/models/db/novel_history.dart @@ -0,0 +1,48 @@ +import 'package:hive/hive.dart'; +part 'novel_history.g.dart'; + +@HiveType(typeId: 2) +class NovelHistory { + NovelHistory({ + required this.novelId, + required this.chapterId, + required this.novelName, + required this.novelCover, + required this.chapterName, + required this.updateTime, + required this.index, + required this.total, + required this.volumeId, + required this.volumeName, + }); + + @HiveField(0) + int novelId; + + @HiveField(1) + int chapterId; + + @HiveField(2) + String novelName; + + @HiveField(3) + String novelCover; + + @HiveField(4) + String chapterName; + + @HiveField(5) + int index; + + @HiveField(6) + int total; + + @HiveField(7) + int volumeId; + + @HiveField(8) + String volumeName; + + @HiveField(9) + DateTime updateTime; +} diff --git a/lib/models/db/novel_history.g.dart b/lib/models/db/novel_history.g.dart new file mode 100644 index 0000000..b100bda --- /dev/null +++ b/lib/models/db/novel_history.g.dart @@ -0,0 +1,68 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'novel_history.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class NovelHistoryAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + NovelHistory read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return NovelHistory( + novelId: fields[0] as int, + chapterId: fields[1] as int, + novelName: fields[2] as String, + novelCover: fields[3] as String, + chapterName: fields[4] as String, + updateTime: fields[9] as DateTime, + index: fields[5] as int, + total: fields[6] as int, + volumeId: fields[7] as int, + volumeName: fields[8] as String, + ); + } + + @override + void write(BinaryWriter writer, NovelHistory obj) { + writer + ..writeByte(10) + ..writeByte(0) + ..write(obj.novelId) + ..writeByte(1) + ..write(obj.chapterId) + ..writeByte(2) + ..write(obj.novelName) + ..writeByte(3) + ..write(obj.novelCover) + ..writeByte(4) + ..write(obj.chapterName) + ..writeByte(5) + ..write(obj.index) + ..writeByte(6) + ..write(obj.total) + ..writeByte(7) + ..write(obj.volumeId) + ..writeByte(8) + ..write(obj.volumeName) + ..writeByte(9) + ..write(obj.updateTime); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NovelHistoryAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/models/news/news_banner_model.dart b/lib/models/news/news_banner_model.dart new file mode 100644 index 0000000..ce00e5f --- /dev/null +++ b/lib/models/news/news_banner_model.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NewsBannerModel { + NewsBannerModel({ + required this.id, + required this.title, + required this.picUrl, + this.objectId, + this.objectUrl, + }); + + factory NewsBannerModel.fromJson(Map json) => + NewsBannerModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + picUrl: asT(json['pic_url'])!, + objectId: asT(json['object_id']), + objectUrl: asT(json['object_url']), + ); + + int id; + String title; + String picUrl; + int? objectId; + String? objectUrl; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'pic_url': picUrl, + 'object_id': objectId, + 'object_url': objectUrl, + }; +} diff --git a/lib/models/news/news_list_item_model.dart b/lib/models/news/news_list_item_model.dart new file mode 100644 index 0000000..cff1d94 --- /dev/null +++ b/lib/models/news/news_list_item_model.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NewsListItemModel { + NewsListItemModel({ + required this.articleId, + required this.title, + this.createTime, + this.intro, + this.authorId, + this.status, + this.rowPicUrl, + this.colPicUrl, + this.pageUrl, + this.authorUid, + this.cover, + this.nickname, + }); + + factory NewsListItemModel.fromJson(Map json) => + NewsListItemModel( + articleId: asT(json['article_id'])!, + title: asT(json['title'])!, + createTime: asT(json['create_time']), + intro: asT(json['intro']), + authorId: asT(json['author_id']), + status: asT(json['status']), + rowPicUrl: asT(json['row_pic_url']), + colPicUrl: asT(json['col_pic_url']), + pageUrl: asT(json['page_url']), + authorUid: asT(json['author_uid']), + cover: asT(json['cover']), + nickname: asT(json['nickname']), + ); + + int articleId; + String title; + int? createTime; + String? intro; + int? authorId; + int? status; + String? rowPicUrl; + String? colPicUrl; + String? pageUrl; + int? authorUid; + String? cover; + String? nickname; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'article_id': articleId, + 'title': title, + 'create_time': createTime, + 'intro': intro, + 'author_id': authorId, + 'status': status, + 'row_pic_url': rowPicUrl, + 'col_pic_url': colPicUrl, + 'page_url': pageUrl, + 'author_uid': authorUid, + 'cover': cover, + 'nickname': nickname, + }; +} diff --git a/lib/models/news/news_stat_model.dart b/lib/models/news/news_stat_model.dart new file mode 100644 index 0000000..ee31ec7 --- /dev/null +++ b/lib/models/news/news_stat_model.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NewsStatModel { + NewsStatModel({ + required this.commentAmount, + required this.moodAmount, + required this.rowPicUrl, + required this.title, + }); + + factory NewsStatModel.fromJson(Map json) => NewsStatModel( + /// DMZJ后端是真混乱... commentAmount是string,mood_amount是int + commentAmount: int.tryParse(json['comment_amount'].toString()) ?? 0, + moodAmount: int.tryParse(json['mood_amount'].toString()) ?? 0, + rowPicUrl: asT(json['row_pic_url'])!, + title: asT(json['title'])!, + ); + + int commentAmount; + int moodAmount; + String rowPicUrl; + String title; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'comment_amount': commentAmount, + 'mood_amount': moodAmount, + 'row_pic_url': rowPicUrl, + 'title': title, + }; +} diff --git a/lib/models/news/news_tag_model.dart b/lib/models/news/news_tag_model.dart new file mode 100644 index 0000000..15c4032 --- /dev/null +++ b/lib/models/news/news_tag_model.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NewsTagModel { + NewsTagModel({ + required this.id, + required this.name, + }); + + factory NewsTagModel.fromJson(Map json) => NewsTagModel( + id: asT(json['id'])!, + name: asT(json['name'])!, + ); + + int id; + String name; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'name': name, + }; +} diff --git a/lib/models/novel/category_filter_model.dart b/lib/models/novel/category_filter_model.dart new file mode 100644 index 0000000..72d42e5 --- /dev/null +++ b/lib/models/novel/category_filter_model.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelCategoryFilterModel { + NovelCategoryFilterModel({ + required this.title, + required this.items, + }); + + factory NovelCategoryFilterModel.fromJson(Map json) { + final List? items = + json['items'] is List ? [] : null; + if (items != null) { + for (final dynamic item in json['items']!) { + if (item != null) { + items.add(NovelCategoryFilterItemModel.fromJson( + asT>(item)!)); + } + } + } + return NovelCategoryFilterModel( + title: asT(json['title'])!, + items: items!, + ); + } + + String title; + List items; + var selectId = 0.obs; + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'title': title, + 'items': items, + }; +} + +class NovelCategoryFilterItemModel { + NovelCategoryFilterItemModel({ + required this.tagId, + required this.tagName, + }); + + factory NovelCategoryFilterItemModel.fromJson(Map json) => + NovelCategoryFilterItemModel( + tagId: asT(json['tagId'])!, + tagName: asT(json['title'])!, + ); + + int tagId; + String tagName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'tag_id': tagId, + 'tag_name': tagName, + }; +} diff --git a/lib/models/novel/category_model.dart b/lib/models/novel/category_model.dart new file mode 100644 index 0000000..af45f26 --- /dev/null +++ b/lib/models/novel/category_model.dart @@ -0,0 +1,38 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelCategoryModel { + NovelCategoryModel({ + required this.tagId, + required this.title, + required this.cover, + }); + + factory NovelCategoryModel.fromJson(Map json) => + NovelCategoryModel( + tagId: asT(json['tagId'])!, + title: asT(json['title'])!, + cover: asT(json['cover'])!, + ); + + int tagId; + String title; + String cover; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'tag_id': tagId, + 'title': title, + 'cover': cover, + }; +} diff --git a/lib/models/novel/category_novel_model.dart b/lib/models/novel/category_novel_model.dart new file mode 100644 index 0000000..067a69f --- /dev/null +++ b/lib/models/novel/category_novel_model.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelCategoryNovelModel { + NovelCategoryNovelModel({ + required this.id, + required this.title, + this.authors, + this.cover, + this.hotHits, + this.lastName, + this.status, + this.types, + this.subNums, + }); + + factory NovelCategoryNovelModel.fromJson(Map json) => + NovelCategoryNovelModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + authors: asT(json['authors']), + cover: asT(json['cover']), + hotHits: asT(json['hot_hits']), + lastName: asT(json['last_name']), + status: asT(json['status']), + types: asT(json['types']), + subNums: asT(json['sub_nums']), + ); + + int id; + String title; + String? authors; + String? cover; + int? hotHits; + String? lastName; + String? status; + String? types; + int? subNums; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'authors': authors, + 'cover': cover, + 'hot_hits': hotHits, + 'last_name': lastName, + 'status': status, + 'types': types, + 'sub_nums': subNums, + }; +} diff --git a/lib/models/novel/detail_model.dart b/lib/models/novel/detail_model.dart new file mode 100644 index 0000000..62969e0 --- /dev/null +++ b/lib/models/novel/detail_model.dart @@ -0,0 +1,239 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelDetailModel { + NovelDetailModel({ + required this.data, + required this.readingRecord, + }); + + factory NovelDetailModel.fromJson(Map json) => + NovelDetailModel( + data: NovelDetailDataModel.fromJson( + asT>(json['data'])!), + readingRecord: NovelDetailReadingRecordModel.fromJson( + asT>(json['readingRecord'])!), + ); + + NovelDetailDataModel data; + NovelDetailReadingRecordModel readingRecord; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'data': data, + 'readingRecord': readingRecord, + }; +} + +class NovelDetailDataModel { + NovelDetailDataModel({ + required this.novelId, + required this.name, + required this.zone, + required this.status, + required this.lastUpdateVolumeName, + required this.lastUpdateChapterName, + required this.lastUpdateVolumeId, + required this.lastUpdateChapterId, + required this.lastUpdateTime, + required this.cover, + required this.hotHits, + required this.introduction, + required this.types, + required this.authors, + required this.firstLetter, + required this.volume, + }); + + factory NovelDetailDataModel.fromJson(Map json) { + final List? types = json['types'] is List ? [] : null; + if (types != null) { + for (final dynamic item in json['types']!) { + if (item != null) { + types.add(asT(item)!); + } + } + } + + final List? volume = json['volume'] is List ? [] : null; + if (volume != null) { + for (final dynamic item in json['volume']!) { + if (item != null) { + volume.add(Volume.fromJson(asT>(item)!)); + } + } + } + return NovelDetailDataModel( + novelId: asT(json['novel_id'])!, + name: asT(json['name'])!, + zone: asT(json['zone'])!, + status: asT(json['status'])!, + lastUpdateVolumeName: asT(json['last_update_volume_name'])!, + lastUpdateChapterName: asT(json['last_update_chapter_name'])!, + lastUpdateVolumeId: asT(json['last_update_volume_id'])!, + lastUpdateChapterId: asT(json['last_update_chapter_id'])!, + lastUpdateTime: asT(json['last_update_time'])!, + cover: asT(json['cover'])!, + hotHits: asT(json['hot_hits']) ?? 0, + introduction: asT(json['introduction'])!, + types: types!, + authors: asT(json['authors'])!, + firstLetter: asT(json['first_letter'])!, + volume: volume!, + ); + } + + int novelId; + String name; + String zone; + String status; + String lastUpdateVolumeName; + String lastUpdateChapterName; + int lastUpdateVolumeId; + int lastUpdateChapterId; + int lastUpdateTime; + String cover; + int hotHits; + String introduction; + List types; + String authors; + String firstLetter; + List volume; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'novel_id': novelId, + 'name': name, + 'zone': zone, + 'status': status, + 'last_update_volume_name': lastUpdateVolumeName, + 'last_update_chapter_name': lastUpdateChapterName, + 'last_update_volume_id': lastUpdateVolumeId, + 'last_update_chapter_id': lastUpdateChapterId, + 'last_update_time': lastUpdateTime, + 'cover': cover, + 'hot_hits': hotHits, + 'introduction': introduction, + 'types': types, + 'authors': authors, + 'first_letter': firstLetter, + 'volume': volume, + }; +} + +class Volume { + Volume({ + required this.volumeId, + required this.lnovelId, + required this.volumeName, + required this.volumeOrder, + required this.addtime, + required this.sumChapters, + }); + + factory Volume.fromJson(Map json) => Volume( + volumeId: asT(json['volume_id'])!, + lnovelId: asT(json['lnovel_id'])!, + volumeName: asT(json['volume_name'])!, + volumeOrder: asT(json['volume_order'])!, + addtime: asT(json['addtime'])!, + sumChapters: asT(json['sum_chapters'])!, + ); + + int volumeId; + int lnovelId; + String volumeName; + int volumeOrder; + int addtime; + int sumChapters; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'volume_id': volumeId, + 'lnovel_id': lnovelId, + 'volume_name': volumeName, + 'volume_order': volumeOrder, + 'addtime': addtime, + 'sum_chapters': sumChapters, + }; +} + +class NovelDetailReadingRecordModel { + NovelDetailReadingRecordModel({ + required this.typeName, + required this.uid, + required this.source, + required this.bizId, + required this.chapterId, + required this.viewingTime, + required this.record, + required this.volumeId, + required this.totalNum, + required this.chapterName, + required this.volumeName, + }); + + factory NovelDetailReadingRecordModel.fromJson(Map json) => + NovelDetailReadingRecordModel( + typeName: asT(json['type_name'])!, + uid: asT(json['uid'])!, + source: asT(json['source'])!, + bizId: asT(json['biz_id'])!, + chapterId: asT(json['chapter_id'])!, + viewingTime: asT(json['viewing_time'])!, + record: asT(json['record'])!, + volumeId: asT(json['volume_id'])!, + totalNum: asT(json['total_num'])!, + chapterName: asT(json['chapter_name'])!, + volumeName: asT(json['volume_name'])!, + ); + + String typeName; + int uid; + int source; + int bizId; + int chapterId; + int viewingTime; + int record; + int volumeId; + int totalNum; + String chapterName; + String volumeName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'type_name': typeName, + 'uid': uid, + 'source': source, + 'biz_id': bizId, + 'chapter_id': chapterId, + 'viewing_time': viewingTime, + 'record': record, + 'volume_id': volumeId, + 'total_num': totalNum, + 'chapter_name': chapterName, + 'volume_name': volumeName, + }; +} diff --git a/lib/models/novel/latest_model.dart b/lib/models/novel/latest_model.dart new file mode 100644 index 0000000..a6a597a --- /dev/null +++ b/lib/models/novel/latest_model.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelLatestModel { + NovelLatestModel({ + required this.id, + required this.title, + this.authors, + this.cover, + this.hotHits, + this.lastName, + this.status, + this.types, + this.subNums, + }); + + factory NovelLatestModel.fromJson(Map json) => + NovelLatestModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + authors: asT(json['authors']), + cover: asT(json['cover']), + hotHits: asT(json['hot_hits']), + lastName: asT(json['last_name']), + status: asT(json['status']), + types: asT(json['types']), + subNums: asT(json['sub_nums']), + ); + + int id; + String title; + String? authors; + String? cover; + int? hotHits; + String? lastName; + String? status; + String? types; + int? subNums; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'authors': authors, + 'cover': cover, + 'hot_hits': hotHits, + 'last_name': lastName, + 'status': status, + 'types': types, + 'sub_nums': subNums, + }; +} diff --git a/lib/models/novel/novel_detail_model.dart b/lib/models/novel/novel_detail_model.dart new file mode 100644 index 0000000..46b736f --- /dev/null +++ b/lib/models/novel/novel_detail_model.dart @@ -0,0 +1,193 @@ +import 'package:flutter_dmzj/models/novel/detail_model.dart'; +import 'package:flutter_dmzj/models/novel/volume_detail_model.dart'; +import 'package:flutter_dmzj/models/proto/novel.pb.dart'; +import 'package:get/get.dart'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelDetailInfo { + NovelDetailInfo({ + required this.novelId, + required this.name, + required this.zone, + required this.status, + required this.lastUpdateVolumeName, + required this.lastUpdateChapterName, + required this.lastUpdateVolumeId, + required this.lastUpdateChapterId, + required this.lastUpdateTime, + required this.cover, + required this.hotHits, + required this.introduction, + required this.types, + required this.authors, + required this.firstLetter, + required this.subscribeNum, + }); + + factory NovelDetailInfo.empty() => NovelDetailInfo( + novelId: 0, + name: "", + zone: "", + status: "", + lastUpdateVolumeName: "", + lastUpdateChapterName: "", + lastUpdateVolumeId: 0, + lastUpdateChapterId: 0, + lastUpdateTime: 0, + cover: "", + hotHits: 0, + introduction: "", + types: [], + authors: "", + firstLetter: "", + subscribeNum: 0, + ); + factory NovelDetailInfo.fromJson(NovelDetailDataModel item) => + NovelDetailInfo( + novelId: item.novelId.toInt(), + name: item.name, + zone: item.zone, + status: item.status, + lastUpdateVolumeName: item.lastUpdateVolumeName, + lastUpdateChapterName: item.lastUpdateChapterName, + lastUpdateVolumeId: item.lastUpdateVolumeId.toInt(), + lastUpdateChapterId: item.lastUpdateChapterId.toInt(), + lastUpdateTime: item.lastUpdateTime.toInt(), + cover: item.cover, + hotHits: item.hotHits.toInt(), + introduction: item.introduction, + types: item.types, + authors: item.authors, + firstLetter: item.firstLetter, + subscribeNum: 0, + ); + + factory NovelDetailInfo.fromV4(NovelDetailProto item) => NovelDetailInfo( + novelId: item.novelId.toInt(), + name: item.name, + zone: item.zone, + status: item.status, + lastUpdateVolumeName: item.lastUpdateVolumeName, + lastUpdateChapterName: item.lastUpdateChapterName, + lastUpdateVolumeId: item.lastUpdateVolumeId.toInt(), + lastUpdateChapterId: item.lastUpdateChapterId.toInt(), + lastUpdateTime: item.lastUpdateTime.toInt(), + cover: item.cover, + hotHits: item.hotHits.toInt(), + introduction: item.introduction, + types: item.types, + authors: item.authors, + firstLetter: item.firstLetter, + subscribeNum: item.subscribeNum.toInt(), + ); + + int novelId; + String name; + String zone; + String status; + String lastUpdateVolumeName; + String lastUpdateChapterName; + int lastUpdateVolumeId; + int lastUpdateChapterId; + int lastUpdateTime; + String cover; + int hotHits; + String introduction; + List types; + String authors; + String firstLetter; + int subscribeNum; + RxList volume = RxList(); +} + +class NovelDetailVolume { + NovelDetailVolume({ + required this.volumeId, + required this.volumeName, + required this.volumeOrder, + required this.chapters, + }); + factory NovelDetailVolume.fromJson(NovelVolumeDetailModel item) => + NovelDetailVolume( + volumeId: item.volumeId.toInt(), + volumeName: item.volumeName, + volumeOrder: item.volumeOrder, + chapters: item.chapters + .map( + (e) => NovelDetailChapter.fromJson( + e, + item.volumeId.toInt(), + item.volumeName, + item.volumeOrder, + ), + ) + .toList(), + ); + + factory NovelDetailVolume.fromV4(NovelVolumeDetailProto item) => + NovelDetailVolume( + volumeId: item.volumeId.toInt(), + volumeName: item.volumeName, + volumeOrder: item.volumeOrder, + chapters: item.chapters + .map( + (e) => NovelDetailChapter.fromV4( + e, + item.volumeId.toInt(), + item.volumeName, + item.volumeOrder, + ), + ) + .toList(), + ); + + int volumeId; + String volumeName; + int volumeOrder; + List chapters; +} + +class NovelDetailChapter { + NovelDetailChapter({ + required this.chapterId, + required this.chapterName, + required this.chapterOrder, + required this.volumeId, + required this.volumeName, + required this.volumeOrder, + }); + factory NovelDetailChapter.fromJson(NovelVolumeDetailChapterModel item, + int volumeId, String volumeName, int volumeOrder) => + NovelDetailChapter( + chapterId: item.chapterId.toInt(), + chapterName: item.chapterName, + chapterOrder: item.chapterOrder, + volumeId: volumeId, + volumeName: volumeName, + volumeOrder: volumeOrder, + ); + + factory NovelDetailChapter.fromV4(NovelChapterDetailProto item, int volumeId, + String volumeName, int volumeOrder) => + NovelDetailChapter( + chapterId: item.chapterId.toInt(), + chapterName: item.chapterName, + chapterOrder: item.chapterOrder, + volumeId: volumeId, + volumeName: volumeName, + volumeOrder: volumeOrder, + ); + + int chapterId; + String chapterName; + int chapterOrder; + int volumeId; + int volumeOrder; + String volumeName; +} diff --git a/lib/models/novel/rank_model.dart b/lib/models/novel/rank_model.dart new file mode 100644 index 0000000..816df8b --- /dev/null +++ b/lib/models/novel/rank_model.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelRankModel { + NovelRankModel({ + required this.id, + required this.lastUpdateTime, + required this.name, + required this.types, + required this.cover, + required this.authors, + required this.lastUpdateChapterName, + required this.top, + required this.subscribeAmount, + }); + + factory NovelRankModel.fromJson(Map json) { + final List? types = json['types'] is List ? [] : null; + if (types != null) { + for (final dynamic item in json['types']!) { + if (item != null) { + types.add(asT(item)!); + } + } + } + return NovelRankModel( + id: asT(json['id'])!, + lastUpdateTime: asT(json['last_update_time'])!, + name: asT(json['name'])!, + types: types!, + cover: asT(json['cover'])!, + authors: asT(json['authors'])!, + lastUpdateChapterName: asT(json['last_update_chapter_name'])!, + top: asT(json['top'])!, + subscribeAmount: asT(json['subscribe_amount'])!, + ); + } + + int id; + int lastUpdateTime; + String name; + List types; + String cover; + String authors; + String lastUpdateChapterName; + int top; + int subscribeAmount; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'last_update_time': lastUpdateTime, + 'name': name, + 'types': types, + 'cover': cover, + 'authors': authors, + 'last_update_chapter_name': lastUpdateChapterName, + 'top': top, + 'subscribe_amount': subscribeAmount, + }; +} diff --git a/lib/models/novel/recommend_model.dart b/lib/models/novel/recommend_model.dart new file mode 100644 index 0000000..3bb5b51 --- /dev/null +++ b/lib/models/novel/recommend_model.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelRecommendModel { + NovelRecommendModel({ + required this.categoryId, + required this.title, + required this.sort, + required this.data, + }); + + factory NovelRecommendModel.fromJson(Map json) { + final List? data = + json['data'] is List ? [] : null; + if (data != null) { + for (final dynamic item in json['data']!) { + if (item != null) { + data.add(NovelRecommendItemModel.fromJson( + asT>(item)!)); + } + } + } + return NovelRecommendModel( + categoryId: asT(json['category_id'])!, + title: asT(json['title'])!, + sort: asT(json['sort'])!, + data: data!, + ); + } + + int categoryId; + String title; + int sort; + List data; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'category_id': categoryId, + 'title': title, + 'sort': sort, + 'data': data, + }; +} + +class NovelRecommendItemModel { + NovelRecommendItemModel({ + required this.cover, + required this.title, + this.subTitle, + this.type, + this.url, + required this.objId, + this.status, + this.id, + }); + + factory NovelRecommendItemModel.fromJson(Map json) => + NovelRecommendItemModel( + id: asT(json['id']), + cover: asT(json['cover'])!, + title: asT(json['title'])!, + subTitle: asT(json['sub_title']), + type: asT(json['type']), + url: asT(json['url']), + objId: asT(json['obj_id']), + status: asT(json['status']), + ); + int? id; + String cover; + String title; + String? subTitle; + int? type; + String? url; + int? objId; + String? status; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'cover': cover, + 'title': title, + 'sub_title': subTitle, + 'type': type, + 'url': url, + 'obj_id': objId, + 'status': status, + }; +} diff --git a/lib/models/novel/search_model.dart b/lib/models/novel/search_model.dart new file mode 100644 index 0000000..dd1d873 --- /dev/null +++ b/lib/models/novel/search_model.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelSearchModel { + NovelSearchModel({ + required this.id, + required this.title, + this.authors, + this.cover, + this.hotHits, + this.lastName, + this.status, + this.types, + this.subNums, + }); + + factory NovelSearchModel.fromJson(Map json) => + NovelSearchModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + authors: asT(json['authors']), + cover: asT(json['cover']), + hotHits: asT(json['hot_hits']), + lastName: asT(json['last_name']), + status: asT(json['status']), + types: asT(json['types']), + subNums: asT(json['sub_nums']), + ); + + int id; + String title; + String? authors; + String? cover; + int? hotHits; + String? lastName; + String? status; + String? types; + int? subNums; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'authors': authors, + 'cover': cover, + 'hot_hits': hotHits, + 'last_name': lastName, + 'status': status, + 'types': types, + 'sub_nums': subNums, + }; +} diff --git a/lib/models/novel/volume_detail_model.dart b/lib/models/novel/volume_detail_model.dart new file mode 100644 index 0000000..61a7aae --- /dev/null +++ b/lib/models/novel/volume_detail_model.dart @@ -0,0 +1,83 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class NovelVolumeDetailModel { + NovelVolumeDetailModel({ + required this.volumeId, + required this.volumeName, + required this.volumeOrder, + required this.chapters, + }); + + factory NovelVolumeDetailModel.fromJson(Map json) { + final List? chapters = + json['chapters'] is List ? [] : null; + if (chapters != null) { + for (final dynamic item in json['chapters']!) { + if (item != null) { + chapters.add(NovelVolumeDetailChapterModel.fromJson( + asT>(item)!)); + } + } + } + return NovelVolumeDetailModel( + volumeId: asT(json['volume_id'])!, + volumeName: asT(json['volume_name'])!, + volumeOrder: asT(json['volume_order'])!, + chapters: chapters!, + ); + } + + int volumeId; + String volumeName; + int volumeOrder; + List chapters; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'volume_id': volumeId, + 'volume_name': volumeName, + 'volume_order': volumeOrder, + 'chapters': chapters, + }; +} + +class NovelVolumeDetailChapterModel { + NovelVolumeDetailChapterModel({ + required this.chapterId, + required this.chapterName, + required this.chapterOrder, + }); + + factory NovelVolumeDetailChapterModel.fromJson(Map json) => + NovelVolumeDetailChapterModel( + chapterId: asT(json['chapter_id'])!, + chapterName: asT(json['chapter_name'])!, + chapterOrder: asT(json['chapter_order'])!, + ); + + int chapterId; + String chapterName; + int chapterOrder; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'chapter_id': chapterId, + 'chapter_name': chapterName, + 'chapter_order': chapterOrder, + }; +} diff --git a/lib/models/proto/comic.pb.dart b/lib/models/proto/comic.pb.dart new file mode 100644 index 0000000..b859a01 --- /dev/null +++ b/lib/models/proto/comic.pb.dart @@ -0,0 +1,2593 @@ +/// +// Generated code. Do not modify. +// source: comic.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name, no_leading_underscores_for_local_identifiers, depend_on_referenced_packages + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class ComicChapterDetailProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicChapterDetailProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterId', + protoName: 'chapterId') + ..aInt64( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'comicId', + protoName: 'comicId') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterOrder', + $pb.PbFieldType.O3, + protoName: 'chapterOrder') + ..a<$core.int>( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'direction', + $pb.PbFieldType.O3) + ..pPS( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageUrl', + protoName: 'pageUrl') + ..a<$core.int>( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'picnum', + $pb.PbFieldType.O3) + ..pPS( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageUrlHD', + protoName: 'pageUrlHD') + ..a<$core.int>( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'commentCount', + $pb.PbFieldType.O3, + protoName: 'commentCount') + ..hasRequiredFields = false; + + ComicChapterDetailProto._() : super(); + factory ComicChapterDetailProto({ + $fixnum.Int64? chapterId, + $fixnum.Int64? comicId, + $core.String? title, + $core.int? chapterOrder, + $core.int? direction, + $core.Iterable<$core.String>? pageUrl, + $core.int? picnum, + $core.Iterable<$core.String>? pageUrlHD, + $core.int? commentCount, + }) { + final _result = create(); + if (chapterId != null) { + _result.chapterId = chapterId; + } + if (comicId != null) { + _result.comicId = comicId; + } + if (title != null) { + _result.title = title; + } + if (chapterOrder != null) { + _result.chapterOrder = chapterOrder; + } + if (direction != null) { + _result.direction = direction; + } + if (pageUrl != null) { + _result.pageUrl.addAll(pageUrl); + } + if (picnum != null) { + _result.picnum = picnum; + } + if (pageUrlHD != null) { + _result.pageUrlHD.addAll(pageUrlHD); + } + if (commentCount != null) { + _result.commentCount = commentCount; + } + return _result; + } + factory ComicChapterDetailProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicChapterDetailProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicChapterDetailProto clone() => + ComicChapterDetailProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicChapterDetailProto copyWith( + void Function(ComicChapterDetailProto) updates) => + super.copyWith((message) => updates(message as ComicChapterDetailProto)) + as ComicChapterDetailProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicChapterDetailProto create() => ComicChapterDetailProto._(); + ComicChapterDetailProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicChapterDetailProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicChapterDetailProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get chapterId => $_getI64(0); + @$pb.TagNumber(1) + set chapterId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasChapterId() => $_has(0); + @$pb.TagNumber(1) + void clearChapterId() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get comicId => $_getI64(1); + @$pb.TagNumber(2) + set comicId($fixnum.Int64 v) { + $_setInt64(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasComicId() => $_has(1); + @$pb.TagNumber(2) + void clearComicId() => clearField(2); + + @$pb.TagNumber(3) + $core.String get title => $_getSZ(2); + @$pb.TagNumber(3) + set title($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasTitle() => $_has(2); + @$pb.TagNumber(3) + void clearTitle() => clearField(3); + + @$pb.TagNumber(4) + $core.int get chapterOrder => $_getIZ(3); + @$pb.TagNumber(4) + set chapterOrder($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasChapterOrder() => $_has(3); + @$pb.TagNumber(4) + void clearChapterOrder() => clearField(4); + + @$pb.TagNumber(5) + $core.int get direction => $_getIZ(4); + @$pb.TagNumber(5) + set direction($core.int v) { + $_setSignedInt32(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasDirection() => $_has(4); + @$pb.TagNumber(5) + void clearDirection() => clearField(5); + + @$pb.TagNumber(6) + $core.List<$core.String> get pageUrl => $_getList(5); + + @$pb.TagNumber(7) + $core.int get picnum => $_getIZ(6); + @$pb.TagNumber(7) + set picnum($core.int v) { + $_setSignedInt32(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasPicnum() => $_has(6); + @$pb.TagNumber(7) + void clearPicnum() => clearField(7); + + @$pb.TagNumber(8) + $core.List<$core.String> get pageUrlHD => $_getList(7); + + @$pb.TagNumber(9) + $core.int get commentCount => $_getIZ(8); + @$pb.TagNumber(9) + set commentCount($core.int v) { + $_setSignedInt32(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasCommentCount() => $_has(8); + @$pb.TagNumber(9) + void clearCommentCount() => clearField(9); +} + +class ComicChapterInfoProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicChapterInfoProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterId', + protoName: 'chapterId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterTitle', + protoName: 'chapterTitle') + ..aInt64( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'updateTime', + protoName: 'updateTime') + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fileSize', + $pb.PbFieldType.O3, + protoName: 'fileSize') + ..a<$core.int>( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterOrder', + $pb.PbFieldType.O3, + protoName: 'chapterOrder') + ..a<$core.int>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'isFee', + $pb.PbFieldType.O3, + protoName: 'isFee') + ..hasRequiredFields = false; + + ComicChapterInfoProto._() : super(); + factory ComicChapterInfoProto({ + $fixnum.Int64? chapterId, + $core.String? chapterTitle, + $fixnum.Int64? updateTime, + $core.int? fileSize, + $core.int? chapterOrder, + $core.int? isFee, + }) { + final _result = create(); + if (chapterId != null) { + _result.chapterId = chapterId; + } + if (chapterTitle != null) { + _result.chapterTitle = chapterTitle; + } + if (updateTime != null) { + _result.updateTime = updateTime; + } + if (fileSize != null) { + _result.fileSize = fileSize; + } + if (chapterOrder != null) { + _result.chapterOrder = chapterOrder; + } + if (isFee != null) { + _result.isFee = isFee; + } + return _result; + } + factory ComicChapterInfoProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicChapterInfoProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicChapterInfoProto clone() => + ComicChapterInfoProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicChapterInfoProto copyWith( + void Function(ComicChapterInfoProto) updates) => + super.copyWith((message) => updates(message as ComicChapterInfoProto)) + as ComicChapterInfoProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicChapterInfoProto create() => ComicChapterInfoProto._(); + ComicChapterInfoProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicChapterInfoProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicChapterInfoProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get chapterId => $_getI64(0); + @$pb.TagNumber(1) + set chapterId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasChapterId() => $_has(0); + @$pb.TagNumber(1) + void clearChapterId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get chapterTitle => $_getSZ(1); + @$pb.TagNumber(2) + set chapterTitle($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasChapterTitle() => $_has(1); + @$pb.TagNumber(2) + void clearChapterTitle() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get updateTime => $_getI64(2); + @$pb.TagNumber(3) + set updateTime($fixnum.Int64 v) { + $_setInt64(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasUpdateTime() => $_has(2); + @$pb.TagNumber(3) + void clearUpdateTime() => clearField(3); + + @$pb.TagNumber(4) + $core.int get fileSize => $_getIZ(3); + @$pb.TagNumber(4) + set fileSize($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasFileSize() => $_has(3); + @$pb.TagNumber(4) + void clearFileSize() => clearField(4); + + @$pb.TagNumber(5) + $core.int get chapterOrder => $_getIZ(4); + @$pb.TagNumber(5) + set chapterOrder($core.int v) { + $_setSignedInt32(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasChapterOrder() => $_has(4); + @$pb.TagNumber(5) + void clearChapterOrder() => clearField(5); + + @$pb.TagNumber(6) + $core.int get isFee => $_getIZ(5); + @$pb.TagNumber(6) + set isFee($core.int v) { + $_setSignedInt32(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasIsFee() => $_has(5); + @$pb.TagNumber(6) + void clearIsFee() => clearField(6); +} + +class ComicChapterResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicChapterResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..aOM( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + subBuilder: ComicChapterDetailProto.create) + ..hasRequiredFields = false; + + ComicChapterResponseProto._() : super(); + factory ComicChapterResponseProto({ + $core.int? errno, + $core.String? errmsg, + ComicChapterDetailProto? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data = data; + } + return _result; + } + factory ComicChapterResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicChapterResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicChapterResponseProto clone() => + ComicChapterResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicChapterResponseProto copyWith( + void Function(ComicChapterResponseProto) updates) => + super.copyWith((message) => updates(message as ComicChapterResponseProto)) + as ComicChapterResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicChapterResponseProto create() => ComicChapterResponseProto._(); + ComicChapterResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicChapterResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicChapterResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + ComicChapterDetailProto get data => $_getN(2); + @$pb.TagNumber(3) + set data(ComicChapterDetailProto v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasData() => $_has(2); + @$pb.TagNumber(3) + void clearData() => clearField(3); + @$pb.TagNumber(3) + ComicChapterDetailProto ensureData() => $_ensure(2); +} + +class ComicChapterListProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicChapterListProto', + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..pc( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + $pb.PbFieldType.PM, + subBuilder: ComicChapterInfoProto.create) + ..hasRequiredFields = false; + + ComicChapterListProto._() : super(); + factory ComicChapterListProto({ + $core.String? title, + $core.Iterable? data, + }) { + final _result = create(); + if (title != null) { + _result.title = title; + } + if (data != null) { + _result.data.addAll(data); + } + return _result; + } + factory ComicChapterListProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicChapterListProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicChapterListProto clone() => + ComicChapterListProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicChapterListProto copyWith( + void Function(ComicChapterListProto) updates) => + super.copyWith((message) => updates(message as ComicChapterListProto)) + as ComicChapterListProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicChapterListProto create() => ComicChapterListProto._(); + ComicChapterListProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicChapterListProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicChapterListProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get title => $_getSZ(0); + @$pb.TagNumber(1) + set title($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasTitle() => $_has(0); + @$pb.TagNumber(1) + void clearTitle() => clearField(1); + + @$pb.TagNumber(2) + $core.List get data => $_getList(1); +} + +class ComicDetailResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicDetailResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..aOM( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + subBuilder: ComicDetailProto.create) + ..hasRequiredFields = false; + + ComicDetailResponseProto._() : super(); + factory ComicDetailResponseProto({ + $core.int? errno, + $core.String? errmsg, + ComicDetailProto? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data = data; + } + return _result; + } + factory ComicDetailResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicDetailResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicDetailResponseProto clone() => + ComicDetailResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicDetailResponseProto copyWith( + void Function(ComicDetailResponseProto) updates) => + super.copyWith((message) => updates(message as ComicDetailResponseProto)) + as ComicDetailResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicDetailResponseProto create() => ComicDetailResponseProto._(); + ComicDetailResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicDetailResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicDetailResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + ComicDetailProto get data => $_getN(2); + @$pb.TagNumber(3) + set data(ComicDetailProto v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasData() => $_has(2); + @$pb.TagNumber(3) + void clearData() => clearField(3); + @$pb.TagNumber(3) + ComicDetailProto ensureData() => $_ensure(2); +} + +class ComicDetailProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicDetailProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'id') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'direction', + $pb.PbFieldType.O3) + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'islong', + $pb.PbFieldType.O3) + ..a<$core.int>( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'isDmzj', + $pb.PbFieldType.O3, + protoName: 'isDmzj') + ..aOS( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cover') + ..aOS( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'description') + ..aInt64( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdatetime', + protoName: 'lastUpdatetime') + ..aOS( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateChapterName', + protoName: 'lastUpdateChapterName') + ..a<$core.int>( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'copyright', + $pb.PbFieldType.O3) + ..aOS( + 11, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'firstLetter', + protoName: 'firstLetter') + ..aOS( + 12, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'comicPy', + protoName: 'comicPy') + ..a<$core.int>( + 13, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'hidden', + $pb.PbFieldType.O3) + ..aInt64( + 14, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'hotNum', + protoName: 'hotNum') + ..aInt64( + 15, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'hitNum', + protoName: 'hitNum') + ..aInt64( + 16, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'uid') + ..a<$core.int>( + 17, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'isLock', + $pb.PbFieldType.O3, + protoName: 'isLock') + ..a<$core.int>( + 18, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateChapterId', + $pb.PbFieldType.O3, + protoName: 'lastUpdateChapterId') + ..pc( + 19, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'types', + $pb.PbFieldType.PM, + subBuilder: ComicTagProto.create) + ..pc( + 20, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'status', + $pb.PbFieldType.PM, + subBuilder: ComicTagProto.create) + ..pc( + 21, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'authors', + $pb.PbFieldType.PM, + subBuilder: ComicTagProto.create) + ..aInt64( + 22, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'subscribeNum', + protoName: 'subscribeNum') + ..pc( + 23, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapters', + $pb.PbFieldType.PM, + subBuilder: ComicChapterListProto.create) + ..a<$core.int>( + 24, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'isNeedLogin', + $pb.PbFieldType.O3, + protoName: 'isNeedLogin') + ..pc( + 25, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'urlLinks', + $pb.PbFieldType.PM, + protoName: 'urlLinks', + subBuilder: ComicDetailUrlLinkProto.create) + ..a<$core.int>( + 26, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'isHideChapter', + $pb.PbFieldType.O3, + protoName: 'isHideChapter') + ..pc( + 27, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'dhUrlLinks', + $pb.PbFieldType.PM, + protoName: 'dhUrlLinks', + subBuilder: ComicDetailUrlLinkProto.create) + ..aOS( + 28, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cornerMark', + protoName: 'cornerMark') + ..a<$core.int>( + 29, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'isFee', + $pb.PbFieldType.O3, + protoName: 'isFee') + ..hasRequiredFields = false; + + ComicDetailProto._() : super(); + factory ComicDetailProto({ + $fixnum.Int64? id, + $core.String? title, + $core.int? direction, + $core.int? islong, + $core.int? isDmzj, + $core.String? cover, + $core.String? description, + $fixnum.Int64? lastUpdatetime, + $core.String? lastUpdateChapterName, + $core.int? copyright, + $core.String? firstLetter, + $core.String? comicPy, + $core.int? hidden, + $fixnum.Int64? hotNum, + $fixnum.Int64? hitNum, + $fixnum.Int64? uid, + $core.int? isLock, + $core.int? lastUpdateChapterId, + $core.Iterable? types, + $core.Iterable? status, + $core.Iterable? authors, + $fixnum.Int64? subscribeNum, + $core.Iterable? chapters, + $core.int? isNeedLogin, + $core.Iterable? urlLinks, + $core.int? isHideChapter, + $core.Iterable? dhUrlLinks, + $core.String? cornerMark, + $core.int? isFee, + }) { + final _result = create(); + if (id != null) { + _result.id = id; + } + if (title != null) { + _result.title = title; + } + if (direction != null) { + _result.direction = direction; + } + if (islong != null) { + _result.islong = islong; + } + if (isDmzj != null) { + _result.isDmzj = isDmzj; + } + if (cover != null) { + _result.cover = cover; + } + if (description != null) { + _result.description = description; + } + if (lastUpdatetime != null) { + _result.lastUpdatetime = lastUpdatetime; + } + if (lastUpdateChapterName != null) { + _result.lastUpdateChapterName = lastUpdateChapterName; + } + if (copyright != null) { + _result.copyright = copyright; + } + if (firstLetter != null) { + _result.firstLetter = firstLetter; + } + if (comicPy != null) { + _result.comicPy = comicPy; + } + if (hidden != null) { + _result.hidden = hidden; + } + if (hotNum != null) { + _result.hotNum = hotNum; + } + if (hitNum != null) { + _result.hitNum = hitNum; + } + if (uid != null) { + _result.uid = uid; + } + if (isLock != null) { + _result.isLock = isLock; + } + if (lastUpdateChapterId != null) { + _result.lastUpdateChapterId = lastUpdateChapterId; + } + if (types != null) { + _result.types.addAll(types); + } + if (status != null) { + _result.status.addAll(status); + } + if (authors != null) { + _result.authors.addAll(authors); + } + if (subscribeNum != null) { + _result.subscribeNum = subscribeNum; + } + if (chapters != null) { + _result.chapters.addAll(chapters); + } + if (isNeedLogin != null) { + _result.isNeedLogin = isNeedLogin; + } + if (urlLinks != null) { + _result.urlLinks.addAll(urlLinks); + } + if (isHideChapter != null) { + _result.isHideChapter = isHideChapter; + } + if (dhUrlLinks != null) { + _result.dhUrlLinks.addAll(dhUrlLinks); + } + if (cornerMark != null) { + _result.cornerMark = cornerMark; + } + if (isFee != null) { + _result.isFee = isFee; + } + return _result; + } + factory ComicDetailProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicDetailProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicDetailProto clone() => ComicDetailProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicDetailProto copyWith(void Function(ComicDetailProto) updates) => + super.copyWith((message) => updates(message as ComicDetailProto)) + as ComicDetailProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicDetailProto create() => ComicDetailProto._(); + ComicDetailProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicDetailProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicDetailProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get id => $_getI64(0); + @$pb.TagNumber(1) + set id($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get title => $_getSZ(1); + @$pb.TagNumber(2) + set title($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasTitle() => $_has(1); + @$pb.TagNumber(2) + void clearTitle() => clearField(2); + + @$pb.TagNumber(3) + $core.int get direction => $_getIZ(2); + @$pb.TagNumber(3) + set direction($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasDirection() => $_has(2); + @$pb.TagNumber(3) + void clearDirection() => clearField(3); + + @$pb.TagNumber(4) + $core.int get islong => $_getIZ(3); + @$pb.TagNumber(4) + set islong($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasIslong() => $_has(3); + @$pb.TagNumber(4) + void clearIslong() => clearField(4); + + @$pb.TagNumber(5) + $core.int get isDmzj => $_getIZ(4); + @$pb.TagNumber(5) + set isDmzj($core.int v) { + $_setSignedInt32(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasIsDmzj() => $_has(4); + @$pb.TagNumber(5) + void clearIsDmzj() => clearField(5); + + @$pb.TagNumber(6) + $core.String get cover => $_getSZ(5); + @$pb.TagNumber(6) + set cover($core.String v) { + $_setString(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasCover() => $_has(5); + @$pb.TagNumber(6) + void clearCover() => clearField(6); + + @$pb.TagNumber(7) + $core.String get description => $_getSZ(6); + @$pb.TagNumber(7) + set description($core.String v) { + $_setString(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasDescription() => $_has(6); + @$pb.TagNumber(7) + void clearDescription() => clearField(7); + + @$pb.TagNumber(8) + $fixnum.Int64 get lastUpdatetime => $_getI64(7); + @$pb.TagNumber(8) + set lastUpdatetime($fixnum.Int64 v) { + $_setInt64(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasLastUpdatetime() => $_has(7); + @$pb.TagNumber(8) + void clearLastUpdatetime() => clearField(8); + + @$pb.TagNumber(9) + $core.String get lastUpdateChapterName => $_getSZ(8); + @$pb.TagNumber(9) + set lastUpdateChapterName($core.String v) { + $_setString(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasLastUpdateChapterName() => $_has(8); + @$pb.TagNumber(9) + void clearLastUpdateChapterName() => clearField(9); + + @$pb.TagNumber(10) + $core.int get copyright => $_getIZ(9); + @$pb.TagNumber(10) + set copyright($core.int v) { + $_setSignedInt32(9, v); + } + + @$pb.TagNumber(10) + $core.bool hasCopyright() => $_has(9); + @$pb.TagNumber(10) + void clearCopyright() => clearField(10); + + @$pb.TagNumber(11) + $core.String get firstLetter => $_getSZ(10); + @$pb.TagNumber(11) + set firstLetter($core.String v) { + $_setString(10, v); + } + + @$pb.TagNumber(11) + $core.bool hasFirstLetter() => $_has(10); + @$pb.TagNumber(11) + void clearFirstLetter() => clearField(11); + + @$pb.TagNumber(12) + $core.String get comicPy => $_getSZ(11); + @$pb.TagNumber(12) + set comicPy($core.String v) { + $_setString(11, v); + } + + @$pb.TagNumber(12) + $core.bool hasComicPy() => $_has(11); + @$pb.TagNumber(12) + void clearComicPy() => clearField(12); + + @$pb.TagNumber(13) + $core.int get hidden => $_getIZ(12); + @$pb.TagNumber(13) + set hidden($core.int v) { + $_setSignedInt32(12, v); + } + + @$pb.TagNumber(13) + $core.bool hasHidden() => $_has(12); + @$pb.TagNumber(13) + void clearHidden() => clearField(13); + + @$pb.TagNumber(14) + $fixnum.Int64 get hotNum => $_getI64(13); + @$pb.TagNumber(14) + set hotNum($fixnum.Int64 v) { + $_setInt64(13, v); + } + + @$pb.TagNumber(14) + $core.bool hasHotNum() => $_has(13); + @$pb.TagNumber(14) + void clearHotNum() => clearField(14); + + @$pb.TagNumber(15) + $fixnum.Int64 get hitNum => $_getI64(14); + @$pb.TagNumber(15) + set hitNum($fixnum.Int64 v) { + $_setInt64(14, v); + } + + @$pb.TagNumber(15) + $core.bool hasHitNum() => $_has(14); + @$pb.TagNumber(15) + void clearHitNum() => clearField(15); + + @$pb.TagNumber(16) + $fixnum.Int64 get uid => $_getI64(15); + @$pb.TagNumber(16) + set uid($fixnum.Int64 v) { + $_setInt64(15, v); + } + + @$pb.TagNumber(16) + $core.bool hasUid() => $_has(15); + @$pb.TagNumber(16) + void clearUid() => clearField(16); + + @$pb.TagNumber(17) + $core.int get isLock => $_getIZ(16); + @$pb.TagNumber(17) + set isLock($core.int v) { + $_setSignedInt32(16, v); + } + + @$pb.TagNumber(17) + $core.bool hasIsLock() => $_has(16); + @$pb.TagNumber(17) + void clearIsLock() => clearField(17); + + @$pb.TagNumber(18) + $core.int get lastUpdateChapterId => $_getIZ(17); + @$pb.TagNumber(18) + set lastUpdateChapterId($core.int v) { + $_setSignedInt32(17, v); + } + + @$pb.TagNumber(18) + $core.bool hasLastUpdateChapterId() => $_has(17); + @$pb.TagNumber(18) + void clearLastUpdateChapterId() => clearField(18); + + @$pb.TagNumber(19) + $core.List get types => $_getList(18); + + @$pb.TagNumber(20) + $core.List get status => $_getList(19); + + @$pb.TagNumber(21) + $core.List get authors => $_getList(20); + + @$pb.TagNumber(22) + $fixnum.Int64 get subscribeNum => $_getI64(21); + @$pb.TagNumber(22) + set subscribeNum($fixnum.Int64 v) { + $_setInt64(21, v); + } + + @$pb.TagNumber(22) + $core.bool hasSubscribeNum() => $_has(21); + @$pb.TagNumber(22) + void clearSubscribeNum() => clearField(22); + + @$pb.TagNumber(23) + $core.List get chapters => $_getList(22); + + @$pb.TagNumber(24) + $core.int get isNeedLogin => $_getIZ(23); + @$pb.TagNumber(24) + set isNeedLogin($core.int v) { + $_setSignedInt32(23, v); + } + + @$pb.TagNumber(24) + $core.bool hasIsNeedLogin() => $_has(23); + @$pb.TagNumber(24) + void clearIsNeedLogin() => clearField(24); + + @$pb.TagNumber(25) + $core.List get urlLinks => $_getList(24); + + @$pb.TagNumber(26) + $core.int get isHideChapter => $_getIZ(25); + @$pb.TagNumber(26) + set isHideChapter($core.int v) { + $_setSignedInt32(25, v); + } + + @$pb.TagNumber(26) + $core.bool hasIsHideChapter() => $_has(25); + @$pb.TagNumber(26) + void clearIsHideChapter() => clearField(26); + + @$pb.TagNumber(27) + $core.List get dhUrlLinks => $_getList(26); + + @$pb.TagNumber(28) + $core.String get cornerMark => $_getSZ(27); + @$pb.TagNumber(28) + set cornerMark($core.String v) { + $_setString(27, v); + } + + @$pb.TagNumber(28) + $core.bool hasCornerMark() => $_has(27); + @$pb.TagNumber(28) + void clearCornerMark() => clearField(28); + + @$pb.TagNumber(29) + $core.int get isFee => $_getIZ(28); + @$pb.TagNumber(29) + set isFee($core.int v) { + $_setSignedInt32(28, v); + } + + @$pb.TagNumber(29) + $core.bool hasIsFee() => $_has(28); + @$pb.TagNumber(29) + void clearIsFee() => clearField(29); +} + +class ComicTagProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicTagProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'tagId', + protoName: 'tagId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'tagName', + protoName: 'tagName') + ..hasRequiredFields = false; + + ComicTagProto._() : super(); + factory ComicTagProto({ + $fixnum.Int64? tagId, + $core.String? tagName, + }) { + final _result = create(); + if (tagId != null) { + _result.tagId = tagId; + } + if (tagName != null) { + _result.tagName = tagName; + } + return _result; + } + factory ComicTagProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicTagProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicTagProto clone() => ComicTagProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicTagProto copyWith(void Function(ComicTagProto) updates) => + super.copyWith((message) => updates(message as ComicTagProto)) + as ComicTagProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicTagProto create() => ComicTagProto._(); + ComicTagProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicTagProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicTagProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get tagId => $_getI64(0); + @$pb.TagNumber(1) + set tagId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasTagId() => $_has(0); + @$pb.TagNumber(1) + void clearTagId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get tagName => $_getSZ(1); + @$pb.TagNumber(2) + set tagName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasTagName() => $_has(1); + @$pb.TagNumber(2) + void clearTagName() => clearField(2); +} + +class ComicDetailUrlLinkProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicDetailUrlLinkProto', + createEmptyInstance: create) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..pc( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'list', + $pb.PbFieldType.PM, + subBuilder: ComicDetailUrlProto.create) + ..hasRequiredFields = false; + + ComicDetailUrlLinkProto._() : super(); + factory ComicDetailUrlLinkProto({ + $core.String? title, + $core.Iterable? list, + }) { + final _result = create(); + if (title != null) { + _result.title = title; + } + if (list != null) { + _result.list.addAll(list); + } + return _result; + } + factory ComicDetailUrlLinkProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicDetailUrlLinkProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicDetailUrlLinkProto clone() => + ComicDetailUrlLinkProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicDetailUrlLinkProto copyWith( + void Function(ComicDetailUrlLinkProto) updates) => + super.copyWith((message) => updates(message as ComicDetailUrlLinkProto)) + as ComicDetailUrlLinkProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicDetailUrlLinkProto create() => ComicDetailUrlLinkProto._(); + ComicDetailUrlLinkProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicDetailUrlLinkProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicDetailUrlLinkProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get title => $_getSZ(0); + @$pb.TagNumber(1) + set title($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasTitle() => $_has(0); + @$pb.TagNumber(1) + void clearTitle() => clearField(1); + + @$pb.TagNumber(2) + $core.List get list => $_getList(1); +} + +class ComicDetailUrlProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicDetailUrlProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'id') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'url') + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'icon') + ..aOS( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'packageName', + protoName: 'packageName') + ..aOS( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'dUrl', + protoName: 'dUrl') + ..a<$core.int>( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'btype', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + ComicDetailUrlProto._() : super(); + factory ComicDetailUrlProto({ + $fixnum.Int64? id, + $core.String? title, + $core.String? url, + $core.String? icon, + $core.String? packageName, + $core.String? dUrl, + $core.int? btype, + }) { + final _result = create(); + if (id != null) { + _result.id = id; + } + if (title != null) { + _result.title = title; + } + if (url != null) { + _result.url = url; + } + if (icon != null) { + _result.icon = icon; + } + if (packageName != null) { + _result.packageName = packageName; + } + if (dUrl != null) { + _result.dUrl = dUrl; + } + if (btype != null) { + _result.btype = btype; + } + return _result; + } + factory ComicDetailUrlProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicDetailUrlProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicDetailUrlProto clone() => ComicDetailUrlProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicDetailUrlProto copyWith(void Function(ComicDetailUrlProto) updates) => + super.copyWith((message) => updates(message as ComicDetailUrlProto)) + as ComicDetailUrlProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicDetailUrlProto create() => ComicDetailUrlProto._(); + ComicDetailUrlProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicDetailUrlProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicDetailUrlProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get id => $_getI64(0); + @$pb.TagNumber(1) + set id($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasId() => $_has(0); + @$pb.TagNumber(1) + void clearId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get title => $_getSZ(1); + @$pb.TagNumber(2) + set title($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasTitle() => $_has(1); + @$pb.TagNumber(2) + void clearTitle() => clearField(2); + + @$pb.TagNumber(3) + $core.String get url => $_getSZ(2); + @$pb.TagNumber(3) + set url($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasUrl() => $_has(2); + @$pb.TagNumber(3) + void clearUrl() => clearField(3); + + @$pb.TagNumber(4) + $core.String get icon => $_getSZ(3); + @$pb.TagNumber(4) + set icon($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasIcon() => $_has(3); + @$pb.TagNumber(4) + void clearIcon() => clearField(4); + + @$pb.TagNumber(5) + $core.String get packageName => $_getSZ(4); + @$pb.TagNumber(5) + set packageName($core.String v) { + $_setString(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasPackageName() => $_has(4); + @$pb.TagNumber(5) + void clearPackageName() => clearField(5); + + @$pb.TagNumber(6) + $core.String get dUrl => $_getSZ(5); + @$pb.TagNumber(6) + set dUrl($core.String v) { + $_setString(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasDUrl() => $_has(5); + @$pb.TagNumber(6) + void clearDUrl() => clearField(6); + + @$pb.TagNumber(7) + $core.int get btype => $_getIZ(6); + @$pb.TagNumber(7) + set btype($core.int v) { + $_setSignedInt32(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasBtype() => $_has(6); + @$pb.TagNumber(7) + void clearBtype() => clearField(7); +} + +class ComicRankListResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicRankListResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..pc( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + $pb.PbFieldType.PM, + subBuilder: ComicRankListInfoProto.create) + ..hasRequiredFields = false; + + ComicRankListResponseProto._() : super(); + factory ComicRankListResponseProto({ + $core.int? errno, + $core.String? errmsg, + $core.Iterable? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data.addAll(data); + } + return _result; + } + factory ComicRankListResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicRankListResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicRankListResponseProto clone() => + ComicRankListResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicRankListResponseProto copyWith( + void Function(ComicRankListResponseProto) updates) => + super.copyWith( + (message) => updates(message as ComicRankListResponseProto)) + as ComicRankListResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicRankListResponseProto create() => ComicRankListResponseProto._(); + ComicRankListResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicRankListResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicRankListResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + $core.List get data => $_getList(2); +} + +class ComicRankListInfoProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicRankListInfoProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'comicId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'authors') + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'status') + ..aOS( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cover') + ..aOS( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'types') + ..aInt64( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdatetime') + ..aOS( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateChapterName') + ..aOS( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'comicPy') + ..aInt64( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'num') + ..a<$core.int>( + 11, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'tagId', + $pb.PbFieldType.O3) + ..aOS( + 12, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterName') + ..aInt64( + 13, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterId') + ..hasRequiredFields = false; + + ComicRankListInfoProto._() : super(); + factory ComicRankListInfoProto({ + $fixnum.Int64? comicId, + $core.String? title, + $core.String? authors, + $core.String? status, + $core.String? cover, + $core.String? types, + $fixnum.Int64? lastUpdatetime, + $core.String? lastUpdateChapterName, + $core.String? comicPy, + $fixnum.Int64? num, + $core.int? tagId, + $core.String? chapterName, + $fixnum.Int64? chapterId, + }) { + final _result = create(); + if (comicId != null) { + _result.comicId = comicId; + } + if (title != null) { + _result.title = title; + } + if (authors != null) { + _result.authors = authors; + } + if (status != null) { + _result.status = status; + } + if (cover != null) { + _result.cover = cover; + } + if (types != null) { + _result.types = types; + } + if (lastUpdatetime != null) { + _result.lastUpdatetime = lastUpdatetime; + } + if (lastUpdateChapterName != null) { + _result.lastUpdateChapterName = lastUpdateChapterName; + } + if (comicPy != null) { + _result.comicPy = comicPy; + } + if (num != null) { + _result.num = num; + } + if (tagId != null) { + _result.tagId = tagId; + } + if (chapterName != null) { + _result.chapterName = chapterName; + } + if (chapterId != null) { + _result.chapterId = chapterId; + } + return _result; + } + factory ComicRankListInfoProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicRankListInfoProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicRankListInfoProto clone() => + ComicRankListInfoProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicRankListInfoProto copyWith( + void Function(ComicRankListInfoProto) updates) => + super.copyWith((message) => updates(message as ComicRankListInfoProto)) + as ComicRankListInfoProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicRankListInfoProto create() => ComicRankListInfoProto._(); + ComicRankListInfoProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicRankListInfoProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicRankListInfoProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get comicId => $_getI64(0); + @$pb.TagNumber(1) + set comicId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasComicId() => $_has(0); + @$pb.TagNumber(1) + void clearComicId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get title => $_getSZ(1); + @$pb.TagNumber(2) + set title($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasTitle() => $_has(1); + @$pb.TagNumber(2) + void clearTitle() => clearField(2); + + @$pb.TagNumber(3) + $core.String get authors => $_getSZ(2); + @$pb.TagNumber(3) + set authors($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasAuthors() => $_has(2); + @$pb.TagNumber(3) + void clearAuthors() => clearField(3); + + @$pb.TagNumber(4) + $core.String get status => $_getSZ(3); + @$pb.TagNumber(4) + set status($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasStatus() => $_has(3); + @$pb.TagNumber(4) + void clearStatus() => clearField(4); + + @$pb.TagNumber(5) + $core.String get cover => $_getSZ(4); + @$pb.TagNumber(5) + set cover($core.String v) { + $_setString(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasCover() => $_has(4); + @$pb.TagNumber(5) + void clearCover() => clearField(5); + + @$pb.TagNumber(6) + $core.String get types => $_getSZ(5); + @$pb.TagNumber(6) + set types($core.String v) { + $_setString(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasTypes() => $_has(5); + @$pb.TagNumber(6) + void clearTypes() => clearField(6); + + @$pb.TagNumber(7) + $fixnum.Int64 get lastUpdatetime => $_getI64(6); + @$pb.TagNumber(7) + set lastUpdatetime($fixnum.Int64 v) { + $_setInt64(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasLastUpdatetime() => $_has(6); + @$pb.TagNumber(7) + void clearLastUpdatetime() => clearField(7); + + @$pb.TagNumber(8) + $core.String get lastUpdateChapterName => $_getSZ(7); + @$pb.TagNumber(8) + set lastUpdateChapterName($core.String v) { + $_setString(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasLastUpdateChapterName() => $_has(7); + @$pb.TagNumber(8) + void clearLastUpdateChapterName() => clearField(8); + + @$pb.TagNumber(9) + $core.String get comicPy => $_getSZ(8); + @$pb.TagNumber(9) + set comicPy($core.String v) { + $_setString(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasComicPy() => $_has(8); + @$pb.TagNumber(9) + void clearComicPy() => clearField(9); + + @$pb.TagNumber(10) + $fixnum.Int64 get num => $_getI64(9); + @$pb.TagNumber(10) + set num($fixnum.Int64 v) { + $_setInt64(9, v); + } + + @$pb.TagNumber(10) + $core.bool hasNum() => $_has(9); + @$pb.TagNumber(10) + void clearNum() => clearField(10); + + @$pb.TagNumber(11) + $core.int get tagId => $_getIZ(10); + @$pb.TagNumber(11) + set tagId($core.int v) { + $_setSignedInt32(10, v); + } + + @$pb.TagNumber(11) + $core.bool hasTagId() => $_has(10); + @$pb.TagNumber(11) + void clearTagId() => clearField(11); + + @$pb.TagNumber(12) + $core.String get chapterName => $_getSZ(11); + @$pb.TagNumber(12) + set chapterName($core.String v) { + $_setString(11, v); + } + + @$pb.TagNumber(12) + $core.bool hasChapterName() => $_has(11); + @$pb.TagNumber(12) + void clearChapterName() => clearField(12); + + @$pb.TagNumber(13) + $fixnum.Int64 get chapterId => $_getI64(12); + @$pb.TagNumber(13) + set chapterId($fixnum.Int64 v) { + $_setInt64(12, v); + } + + @$pb.TagNumber(13) + $core.bool hasChapterId() => $_has(12); + @$pb.TagNumber(13) + void clearChapterId() => clearField(13); +} + +class RankTypeFilterResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'RankTypeFilterResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..pc( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + $pb.PbFieldType.PM, + subBuilder: ComicTagProto.create) + ..hasRequiredFields = false; + + RankTypeFilterResponseProto._() : super(); + factory RankTypeFilterResponseProto({ + $core.int? errno, + $core.String? errmsg, + $core.Iterable? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data.addAll(data); + } + return _result; + } + factory RankTypeFilterResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory RankTypeFilterResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RankTypeFilterResponseProto clone() => + RankTypeFilterResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RankTypeFilterResponseProto copyWith( + void Function(RankTypeFilterResponseProto) updates) => + super.copyWith( + (message) => updates(message as RankTypeFilterResponseProto)) + as RankTypeFilterResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RankTypeFilterResponseProto create() => + RankTypeFilterResponseProto._(); + RankTypeFilterResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RankTypeFilterResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static RankTypeFilterResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + $core.List get data => $_getList(2); +} + +class ComicUpdateListResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicUpdateListResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..pc( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + $pb.PbFieldType.PM, + subBuilder: ComicUpdateListInfoProto.create) + ..hasRequiredFields = false; + + ComicUpdateListResponseProto._() : super(); + factory ComicUpdateListResponseProto({ + $core.int? errno, + $core.String? errmsg, + $core.Iterable? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data.addAll(data); + } + return _result; + } + factory ComicUpdateListResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicUpdateListResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicUpdateListResponseProto clone() => + ComicUpdateListResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicUpdateListResponseProto copyWith( + void Function(ComicUpdateListResponseProto) updates) => + super.copyWith( + (message) => updates(message as ComicUpdateListResponseProto)) + as ComicUpdateListResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicUpdateListResponseProto create() => + ComicUpdateListResponseProto._(); + ComicUpdateListResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicUpdateListResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicUpdateListResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + $core.List get data => $_getList(2); +} + +class ComicUpdateListInfoProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ComicUpdateListInfoProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'comicId', + protoName: 'comicId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'islong', + $pb.PbFieldType.O3) + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'authors') + ..aOS( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'types') + ..aOS( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cover') + ..aOS( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'status') + ..aOS( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateChapterName', + protoName: 'lastUpdateChapterName') + ..aInt64( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateChapterId', + protoName: 'lastUpdateChapterId') + ..aInt64( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdatetime', + protoName: 'lastUpdatetime') + ..hasRequiredFields = false; + + ComicUpdateListInfoProto._() : super(); + factory ComicUpdateListInfoProto({ + $fixnum.Int64? comicId, + $core.String? title, + $core.int? islong, + $core.String? authors, + $core.String? types, + $core.String? cover, + $core.String? status, + $core.String? lastUpdateChapterName, + $fixnum.Int64? lastUpdateChapterId, + $fixnum.Int64? lastUpdatetime, + }) { + final _result = create(); + if (comicId != null) { + _result.comicId = comicId; + } + if (title != null) { + _result.title = title; + } + if (islong != null) { + _result.islong = islong; + } + if (authors != null) { + _result.authors = authors; + } + if (types != null) { + _result.types = types; + } + if (cover != null) { + _result.cover = cover; + } + if (status != null) { + _result.status = status; + } + if (lastUpdateChapterName != null) { + _result.lastUpdateChapterName = lastUpdateChapterName; + } + if (lastUpdateChapterId != null) { + _result.lastUpdateChapterId = lastUpdateChapterId; + } + if (lastUpdatetime != null) { + _result.lastUpdatetime = lastUpdatetime; + } + return _result; + } + factory ComicUpdateListInfoProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ComicUpdateListInfoProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ComicUpdateListInfoProto clone() => + ComicUpdateListInfoProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ComicUpdateListInfoProto copyWith( + void Function(ComicUpdateListInfoProto) updates) => + super.copyWith((message) => updates(message as ComicUpdateListInfoProto)) + as ComicUpdateListInfoProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ComicUpdateListInfoProto create() => ComicUpdateListInfoProto._(); + ComicUpdateListInfoProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ComicUpdateListInfoProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ComicUpdateListInfoProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get comicId => $_getI64(0); + @$pb.TagNumber(1) + set comicId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasComicId() => $_has(0); + @$pb.TagNumber(1) + void clearComicId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get title => $_getSZ(1); + @$pb.TagNumber(2) + set title($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasTitle() => $_has(1); + @$pb.TagNumber(2) + void clearTitle() => clearField(2); + + @$pb.TagNumber(3) + $core.int get islong => $_getIZ(2); + @$pb.TagNumber(3) + set islong($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasIslong() => $_has(2); + @$pb.TagNumber(3) + void clearIslong() => clearField(3); + + @$pb.TagNumber(4) + $core.String get authors => $_getSZ(3); + @$pb.TagNumber(4) + set authors($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasAuthors() => $_has(3); + @$pb.TagNumber(4) + void clearAuthors() => clearField(4); + + @$pb.TagNumber(5) + $core.String get types => $_getSZ(4); + @$pb.TagNumber(5) + set types($core.String v) { + $_setString(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasTypes() => $_has(4); + @$pb.TagNumber(5) + void clearTypes() => clearField(5); + + @$pb.TagNumber(6) + $core.String get cover => $_getSZ(5); + @$pb.TagNumber(6) + set cover($core.String v) { + $_setString(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasCover() => $_has(5); + @$pb.TagNumber(6) + void clearCover() => clearField(6); + + @$pb.TagNumber(7) + $core.String get status => $_getSZ(6); + @$pb.TagNumber(7) + set status($core.String v) { + $_setString(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasStatus() => $_has(6); + @$pb.TagNumber(7) + void clearStatus() => clearField(7); + + @$pb.TagNumber(8) + $core.String get lastUpdateChapterName => $_getSZ(7); + @$pb.TagNumber(8) + set lastUpdateChapterName($core.String v) { + $_setString(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasLastUpdateChapterName() => $_has(7); + @$pb.TagNumber(8) + void clearLastUpdateChapterName() => clearField(8); + + @$pb.TagNumber(9) + $fixnum.Int64 get lastUpdateChapterId => $_getI64(8); + @$pb.TagNumber(9) + set lastUpdateChapterId($fixnum.Int64 v) { + $_setInt64(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasLastUpdateChapterId() => $_has(8); + @$pb.TagNumber(9) + void clearLastUpdateChapterId() => clearField(9); + + @$pb.TagNumber(10) + $fixnum.Int64 get lastUpdatetime => $_getI64(9); + @$pb.TagNumber(10) + set lastUpdatetime($fixnum.Int64 v) { + $_setInt64(9, v); + } + + @$pb.TagNumber(10) + $core.bool hasLastUpdatetime() => $_has(9); + @$pb.TagNumber(10) + void clearLastUpdatetime() => clearField(10); +} diff --git a/lib/models/proto/comic.pbjson.dart b/lib/models/proto/comic.pbjson.dart new file mode 100644 index 0000000..275882d --- /dev/null +++ b/lib/models/proto/comic.pbjson.dart @@ -0,0 +1,231 @@ +/// +// Generated code. Do not modify. +// source: comic.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name + +import 'dart:core' as $core; +import 'dart:convert' as $convert; +import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use comicChapterDetailProtoDescriptor instead') +const ComicChapterDetailProto$json = const { + '1': 'ComicChapterDetailProto', + '2': const [ + const {'1': 'chapterId', '3': 1, '4': 1, '5': 3, '10': 'chapterId'}, + const {'1': 'comicId', '3': 2, '4': 1, '5': 3, '10': 'comicId'}, + const {'1': 'title', '3': 3, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'chapterOrder', '3': 4, '4': 1, '5': 5, '10': 'chapterOrder'}, + const {'1': 'direction', '3': 5, '4': 1, '5': 5, '10': 'direction'}, + const {'1': 'pageUrl', '3': 6, '4': 3, '5': 9, '10': 'pageUrl'}, + const {'1': 'picnum', '3': 7, '4': 1, '5': 5, '10': 'picnum'}, + const {'1': 'pageUrlHD', '3': 8, '4': 3, '5': 9, '10': 'pageUrlHD'}, + const {'1': 'commentCount', '3': 9, '4': 1, '5': 5, '10': 'commentCount'}, + ], +}; + +/// Descriptor for `ComicChapterDetailProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicChapterDetailProtoDescriptor = $convert.base64Decode('ChdDb21pY0NoYXB0ZXJEZXRhaWxQcm90bxIcCgljaGFwdGVySWQYASABKANSCWNoYXB0ZXJJZBIYCgdjb21pY0lkGAIgASgDUgdjb21pY0lkEhQKBXRpdGxlGAMgASgJUgV0aXRsZRIiCgxjaGFwdGVyT3JkZXIYBCABKAVSDGNoYXB0ZXJPcmRlchIcCglkaXJlY3Rpb24YBSABKAVSCWRpcmVjdGlvbhIYCgdwYWdlVXJsGAYgAygJUgdwYWdlVXJsEhYKBnBpY251bRgHIAEoBVIGcGljbnVtEhwKCXBhZ2VVcmxIRBgIIAMoCVIJcGFnZVVybEhEEiIKDGNvbW1lbnRDb3VudBgJIAEoBVIMY29tbWVudENvdW50'); +@$core.Deprecated('Use comicChapterInfoProtoDescriptor instead') +const ComicChapterInfoProto$json = const { + '1': 'ComicChapterInfoProto', + '2': const [ + const {'1': 'chapterId', '3': 1, '4': 1, '5': 3, '10': 'chapterId'}, + const {'1': 'chapterTitle', '3': 2, '4': 1, '5': 9, '10': 'chapterTitle'}, + const {'1': 'updateTime', '3': 3, '4': 1, '5': 3, '10': 'updateTime'}, + const {'1': 'fileSize', '3': 4, '4': 1, '5': 5, '10': 'fileSize'}, + const {'1': 'chapterOrder', '3': 5, '4': 1, '5': 5, '10': 'chapterOrder'}, + const {'1': 'isFee', '3': 6, '4': 1, '5': 5, '10': 'isFee'}, + ], +}; + +/// Descriptor for `ComicChapterInfoProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicChapterInfoProtoDescriptor = $convert.base64Decode('ChVDb21pY0NoYXB0ZXJJbmZvUHJvdG8SHAoJY2hhcHRlcklkGAEgASgDUgljaGFwdGVySWQSIgoMY2hhcHRlclRpdGxlGAIgASgJUgxjaGFwdGVyVGl0bGUSHgoKdXBkYXRlVGltZRgDIAEoA1IKdXBkYXRlVGltZRIaCghmaWxlU2l6ZRgEIAEoBVIIZmlsZVNpemUSIgoMY2hhcHRlck9yZGVyGAUgASgFUgxjaGFwdGVyT3JkZXISFAoFaXNGZWUYBiABKAVSBWlzRmVl'); +@$core.Deprecated('Use comicChapterResponseProtoDescriptor instead') +const ComicChapterResponseProto$json = const { + '1': 'ComicChapterResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 1, '5': 11, '6': '.ComicChapterDetailProto', '10': 'data'}, + ], +}; + +/// Descriptor for `ComicChapterResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicChapterResponseProtoDescriptor = $convert.base64Decode('ChlDb21pY0NoYXB0ZXJSZXNwb25zZVByb3RvEhQKBWVycm5vGAEgASgFUgVlcnJubxIWCgZlcnJtc2cYAiABKAlSBmVycm1zZxIsCgRkYXRhGAMgASgLMhguQ29taWNDaGFwdGVyRGV0YWlsUHJvdG9SBGRhdGE='); +@$core.Deprecated('Use comicChapterListProtoDescriptor instead') +const ComicChapterListProto$json = const { + '1': 'ComicChapterListProto', + '2': const [ + const {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'data', '3': 2, '4': 3, '5': 11, '6': '.ComicChapterInfoProto', '10': 'data'}, + ], +}; + +/// Descriptor for `ComicChapterListProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicChapterListProtoDescriptor = $convert.base64Decode('ChVDb21pY0NoYXB0ZXJMaXN0UHJvdG8SFAoFdGl0bGUYASABKAlSBXRpdGxlEioKBGRhdGEYAiADKAsyFi5Db21pY0NoYXB0ZXJJbmZvUHJvdG9SBGRhdGE='); +@$core.Deprecated('Use comicDetailResponseProtoDescriptor instead') +const ComicDetailResponseProto$json = const { + '1': 'ComicDetailResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 1, '5': 11, '6': '.ComicDetailProto', '10': 'data'}, + ], +}; + +/// Descriptor for `ComicDetailResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicDetailResponseProtoDescriptor = $convert.base64Decode('ChhDb21pY0RldGFpbFJlc3BvbnNlUHJvdG8SFAoFZXJybm8YASABKAVSBWVycm5vEhYKBmVycm1zZxgCIAEoCVIGZXJybXNnEiUKBGRhdGEYAyABKAsyES5Db21pY0RldGFpbFByb3RvUgRkYXRh'); +@$core.Deprecated('Use comicDetailProtoDescriptor instead') +const ComicDetailProto$json = const { + '1': 'ComicDetailProto', + '2': const [ + const {'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'}, + const {'1': 'title', '3': 2, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'direction', '3': 3, '4': 1, '5': 5, '10': 'direction'}, + const {'1': 'islong', '3': 4, '4': 1, '5': 5, '10': 'islong'}, + const {'1': 'isDmzj', '3': 5, '4': 1, '5': 5, '10': 'isDmzj'}, + const {'1': 'cover', '3': 6, '4': 1, '5': 9, '10': 'cover'}, + const {'1': 'description', '3': 7, '4': 1, '5': 9, '10': 'description'}, + const {'1': 'lastUpdatetime', '3': 8, '4': 1, '5': 3, '10': 'lastUpdatetime'}, + const {'1': 'lastUpdateChapterName', '3': 9, '4': 1, '5': 9, '10': 'lastUpdateChapterName'}, + const {'1': 'copyright', '3': 10, '4': 1, '5': 5, '10': 'copyright'}, + const {'1': 'firstLetter', '3': 11, '4': 1, '5': 9, '10': 'firstLetter'}, + const {'1': 'comicPy', '3': 12, '4': 1, '5': 9, '10': 'comicPy'}, + const {'1': 'hidden', '3': 13, '4': 1, '5': 5, '10': 'hidden'}, + const {'1': 'hotNum', '3': 14, '4': 1, '5': 3, '10': 'hotNum'}, + const {'1': 'hitNum', '3': 15, '4': 1, '5': 3, '10': 'hitNum'}, + const {'1': 'uid', '3': 16, '4': 1, '5': 3, '10': 'uid'}, + const {'1': 'isLock', '3': 17, '4': 1, '5': 5, '10': 'isLock'}, + const {'1': 'lastUpdateChapterId', '3': 18, '4': 1, '5': 5, '10': 'lastUpdateChapterId'}, + const {'1': 'types', '3': 19, '4': 3, '5': 11, '6': '.ComicTagProto', '10': 'types'}, + const {'1': 'status', '3': 20, '4': 3, '5': 11, '6': '.ComicTagProto', '10': 'status'}, + const {'1': 'authors', '3': 21, '4': 3, '5': 11, '6': '.ComicTagProto', '10': 'authors'}, + const {'1': 'subscribeNum', '3': 22, '4': 1, '5': 3, '10': 'subscribeNum'}, + const {'1': 'chapters', '3': 23, '4': 3, '5': 11, '6': '.ComicChapterListProto', '10': 'chapters'}, + const {'1': 'isNeedLogin', '3': 24, '4': 1, '5': 5, '10': 'isNeedLogin'}, + const {'1': 'urlLinks', '3': 25, '4': 3, '5': 11, '6': '.ComicDetailUrlLinkProto', '10': 'urlLinks'}, + const {'1': 'isHideChapter', '3': 26, '4': 1, '5': 5, '10': 'isHideChapter'}, + const {'1': 'dhUrlLinks', '3': 27, '4': 3, '5': 11, '6': '.ComicDetailUrlLinkProto', '10': 'dhUrlLinks'}, + const {'1': 'cornerMark', '3': 28, '4': 1, '5': 9, '10': 'cornerMark'}, + const {'1': 'isFee', '3': 29, '4': 1, '5': 5, '10': 'isFee'}, + ], +}; + +/// Descriptor for `ComicDetailProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicDetailProtoDescriptor = $convert.base64Decode('ChBDb21pY0RldGFpbFByb3RvEg4KAmlkGAEgASgDUgJpZBIUCgV0aXRsZRgCIAEoCVIFdGl0bGUSHAoJZGlyZWN0aW9uGAMgASgFUglkaXJlY3Rpb24SFgoGaXNsb25nGAQgASgFUgZpc2xvbmcSFgoGaXNEbXpqGAUgASgFUgZpc0RtemoSFAoFY292ZXIYBiABKAlSBWNvdmVyEiAKC2Rlc2NyaXB0aW9uGAcgASgJUgtkZXNjcmlwdGlvbhImCg5sYXN0VXBkYXRldGltZRgIIAEoA1IObGFzdFVwZGF0ZXRpbWUSNAoVbGFzdFVwZGF0ZUNoYXB0ZXJOYW1lGAkgASgJUhVsYXN0VXBkYXRlQ2hhcHRlck5hbWUSHAoJY29weXJpZ2h0GAogASgFUgljb3B5cmlnaHQSIAoLZmlyc3RMZXR0ZXIYCyABKAlSC2ZpcnN0TGV0dGVyEhgKB2NvbWljUHkYDCABKAlSB2NvbWljUHkSFgoGaGlkZGVuGA0gASgFUgZoaWRkZW4SFgoGaG90TnVtGA4gASgDUgZob3ROdW0SFgoGaGl0TnVtGA8gASgDUgZoaXROdW0SEAoDdWlkGBAgASgDUgN1aWQSFgoGaXNMb2NrGBEgASgFUgZpc0xvY2sSMAoTbGFzdFVwZGF0ZUNoYXB0ZXJJZBgSIAEoBVITbGFzdFVwZGF0ZUNoYXB0ZXJJZBIkCgV0eXBlcxgTIAMoCzIOLkNvbWljVGFnUHJvdG9SBXR5cGVzEiYKBnN0YXR1cxgUIAMoCzIOLkNvbWljVGFnUHJvdG9SBnN0YXR1cxIoCgdhdXRob3JzGBUgAygLMg4uQ29taWNUYWdQcm90b1IHYXV0aG9ycxIiCgxzdWJzY3JpYmVOdW0YFiABKANSDHN1YnNjcmliZU51bRIyCghjaGFwdGVycxgXIAMoCzIWLkNvbWljQ2hhcHRlckxpc3RQcm90b1IIY2hhcHRlcnMSIAoLaXNOZWVkTG9naW4YGCABKAVSC2lzTmVlZExvZ2luEjQKCHVybExpbmtzGBkgAygLMhguQ29taWNEZXRhaWxVcmxMaW5rUHJvdG9SCHVybExpbmtzEiQKDWlzSGlkZUNoYXB0ZXIYGiABKAVSDWlzSGlkZUNoYXB0ZXISOAoKZGhVcmxMaW5rcxgbIAMoCzIYLkNvbWljRGV0YWlsVXJsTGlua1Byb3RvUgpkaFVybExpbmtzEh4KCmNvcm5lck1hcmsYHCABKAlSCmNvcm5lck1hcmsSFAoFaXNGZWUYHSABKAVSBWlzRmVl'); +@$core.Deprecated('Use comicTagProtoDescriptor instead') +const ComicTagProto$json = const { + '1': 'ComicTagProto', + '2': const [ + const {'1': 'tagId', '3': 1, '4': 1, '5': 3, '10': 'tagId'}, + const {'1': 'tagName', '3': 2, '4': 1, '5': 9, '10': 'tagName'}, + ], +}; + +/// Descriptor for `ComicTagProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicTagProtoDescriptor = $convert.base64Decode('Cg1Db21pY1RhZ1Byb3RvEhQKBXRhZ0lkGAEgASgDUgV0YWdJZBIYCgd0YWdOYW1lGAIgASgJUgd0YWdOYW1l'); +@$core.Deprecated('Use comicDetailUrlLinkProtoDescriptor instead') +const ComicDetailUrlLinkProto$json = const { + '1': 'ComicDetailUrlLinkProto', + '2': const [ + const {'1': 'title', '3': 1, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'list', '3': 2, '4': 3, '5': 11, '6': '.ComicDetailUrlProto', '10': 'list'}, + ], +}; + +/// Descriptor for `ComicDetailUrlLinkProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicDetailUrlLinkProtoDescriptor = $convert.base64Decode('ChdDb21pY0RldGFpbFVybExpbmtQcm90bxIUCgV0aXRsZRgBIAEoCVIFdGl0bGUSKAoEbGlzdBgCIAMoCzIULkNvbWljRGV0YWlsVXJsUHJvdG9SBGxpc3Q='); +@$core.Deprecated('Use comicDetailUrlProtoDescriptor instead') +const ComicDetailUrlProto$json = const { + '1': 'ComicDetailUrlProto', + '2': const [ + const {'1': 'id', '3': 1, '4': 1, '5': 3, '10': 'id'}, + const {'1': 'title', '3': 2, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'url', '3': 3, '4': 1, '5': 9, '10': 'url'}, + const {'1': 'icon', '3': 4, '4': 1, '5': 9, '10': 'icon'}, + const {'1': 'packageName', '3': 5, '4': 1, '5': 9, '10': 'packageName'}, + const {'1': 'dUrl', '3': 6, '4': 1, '5': 9, '10': 'dUrl'}, + const {'1': 'btype', '3': 7, '4': 1, '5': 5, '10': 'btype'}, + ], +}; + +/// Descriptor for `ComicDetailUrlProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicDetailUrlProtoDescriptor = $convert.base64Decode('ChNDb21pY0RldGFpbFVybFByb3RvEg4KAmlkGAEgASgDUgJpZBIUCgV0aXRsZRgCIAEoCVIFdGl0bGUSEAoDdXJsGAMgASgJUgN1cmwSEgoEaWNvbhgEIAEoCVIEaWNvbhIgCgtwYWNrYWdlTmFtZRgFIAEoCVILcGFja2FnZU5hbWUSEgoEZFVybBgGIAEoCVIEZFVybBIUCgVidHlwZRgHIAEoBVIFYnR5cGU='); +@$core.Deprecated('Use comicRankListResponseProtoDescriptor instead') +const ComicRankListResponseProto$json = const { + '1': 'ComicRankListResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 3, '5': 11, '6': '.ComicRankListInfoProto', '10': 'data'}, + ], +}; + +/// Descriptor for `ComicRankListResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicRankListResponseProtoDescriptor = $convert.base64Decode('ChpDb21pY1JhbmtMaXN0UmVzcG9uc2VQcm90bxIUCgVlcnJubxgBIAEoBVIFZXJybm8SFgoGZXJybXNnGAIgASgJUgZlcnJtc2cSKwoEZGF0YRgDIAMoCzIXLkNvbWljUmFua0xpc3RJbmZvUHJvdG9SBGRhdGE='); +@$core.Deprecated('Use comicRankListInfoProtoDescriptor instead') +const ComicRankListInfoProto$json = const { + '1': 'ComicRankListInfoProto', + '2': const [ + const {'1': 'comic_id', '3': 1, '4': 1, '5': 3, '10': 'comicId'}, + const {'1': 'title', '3': 2, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'authors', '3': 3, '4': 1, '5': 9, '10': 'authors'}, + const {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, + const {'1': 'cover', '3': 5, '4': 1, '5': 9, '10': 'cover'}, + const {'1': 'types', '3': 6, '4': 1, '5': 9, '10': 'types'}, + const {'1': 'last_updatetime', '3': 7, '4': 1, '5': 3, '10': 'lastUpdatetime'}, + const {'1': 'last_update_chapter_name', '3': 8, '4': 1, '5': 9, '10': 'lastUpdateChapterName'}, + const {'1': 'comic_py', '3': 9, '4': 1, '5': 9, '10': 'comicPy'}, + const {'1': 'num', '3': 10, '4': 1, '5': 3, '10': 'num'}, + const {'1': 'tag_id', '3': 11, '4': 1, '5': 5, '10': 'tagId'}, + const {'1': 'chapter_name', '3': 12, '4': 1, '5': 9, '10': 'chapterName'}, + const {'1': 'chapter_id', '3': 13, '4': 1, '5': 3, '10': 'chapterId'}, + ], +}; + +/// Descriptor for `ComicRankListInfoProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicRankListInfoProtoDescriptor = $convert.base64Decode('ChZDb21pY1JhbmtMaXN0SW5mb1Byb3RvEhkKCGNvbWljX2lkGAEgASgDUgdjb21pY0lkEhQKBXRpdGxlGAIgASgJUgV0aXRsZRIYCgdhdXRob3JzGAMgASgJUgdhdXRob3JzEhYKBnN0YXR1cxgEIAEoCVIGc3RhdHVzEhQKBWNvdmVyGAUgASgJUgVjb3ZlchIUCgV0eXBlcxgGIAEoCVIFdHlwZXMSJwoPbGFzdF91cGRhdGV0aW1lGAcgASgDUg5sYXN0VXBkYXRldGltZRI3ChhsYXN0X3VwZGF0ZV9jaGFwdGVyX25hbWUYCCABKAlSFWxhc3RVcGRhdGVDaGFwdGVyTmFtZRIZCghjb21pY19weRgJIAEoCVIHY29taWNQeRIQCgNudW0YCiABKANSA251bRIVCgZ0YWdfaWQYCyABKAVSBXRhZ0lkEiEKDGNoYXB0ZXJfbmFtZRgMIAEoCVILY2hhcHRlck5hbWUSHQoKY2hhcHRlcl9pZBgNIAEoA1IJY2hhcHRlcklk'); +@$core.Deprecated('Use rankTypeFilterResponseProtoDescriptor instead') +const RankTypeFilterResponseProto$json = const { + '1': 'RankTypeFilterResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 3, '5': 11, '6': '.ComicTagProto', '10': 'data'}, + ], +}; + +/// Descriptor for `RankTypeFilterResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List rankTypeFilterResponseProtoDescriptor = $convert.base64Decode('ChtSYW5rVHlwZUZpbHRlclJlc3BvbnNlUHJvdG8SFAoFZXJybm8YASABKAVSBWVycm5vEhYKBmVycm1zZxgCIAEoCVIGZXJybXNnEiIKBGRhdGEYAyADKAsyDi5Db21pY1RhZ1Byb3RvUgRkYXRh'); +@$core.Deprecated('Use comicUpdateListResponseProtoDescriptor instead') +const ComicUpdateListResponseProto$json = const { + '1': 'ComicUpdateListResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 3, '5': 11, '6': '.ComicUpdateListInfoProto', '10': 'data'}, + ], +}; + +/// Descriptor for `ComicUpdateListResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicUpdateListResponseProtoDescriptor = $convert.base64Decode('ChxDb21pY1VwZGF0ZUxpc3RSZXNwb25zZVByb3RvEhQKBWVycm5vGAEgASgFUgVlcnJubxIWCgZlcnJtc2cYAiABKAlSBmVycm1zZxItCgRkYXRhGAMgAygLMhkuQ29taWNVcGRhdGVMaXN0SW5mb1Byb3RvUgRkYXRh'); +@$core.Deprecated('Use comicUpdateListInfoProtoDescriptor instead') +const ComicUpdateListInfoProto$json = const { + '1': 'ComicUpdateListInfoProto', + '2': const [ + const {'1': 'comicId', '3': 1, '4': 1, '5': 3, '10': 'comicId'}, + const {'1': 'title', '3': 2, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'islong', '3': 3, '4': 1, '5': 5, '10': 'islong'}, + const {'1': 'authors', '3': 4, '4': 1, '5': 9, '10': 'authors'}, + const {'1': 'types', '3': 5, '4': 1, '5': 9, '10': 'types'}, + const {'1': 'cover', '3': 6, '4': 1, '5': 9, '10': 'cover'}, + const {'1': 'status', '3': 7, '4': 1, '5': 9, '10': 'status'}, + const {'1': 'lastUpdateChapterName', '3': 8, '4': 1, '5': 9, '10': 'lastUpdateChapterName'}, + const {'1': 'lastUpdateChapterId', '3': 9, '4': 1, '5': 3, '10': 'lastUpdateChapterId'}, + const {'1': 'lastUpdatetime', '3': 10, '4': 1, '5': 3, '10': 'lastUpdatetime'}, + ], +}; + +/// Descriptor for `ComicUpdateListInfoProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List comicUpdateListInfoProtoDescriptor = $convert.base64Decode('ChhDb21pY1VwZGF0ZUxpc3RJbmZvUHJvdG8SGAoHY29taWNJZBgBIAEoA1IHY29taWNJZBIUCgV0aXRsZRgCIAEoCVIFdGl0bGUSFgoGaXNsb25nGAMgASgFUgZpc2xvbmcSGAoHYXV0aG9ycxgEIAEoCVIHYXV0aG9ycxIUCgV0eXBlcxgFIAEoCVIFdHlwZXMSFAoFY292ZXIYBiABKAlSBWNvdmVyEhYKBnN0YXR1cxgHIAEoCVIGc3RhdHVzEjQKFWxhc3RVcGRhdGVDaGFwdGVyTmFtZRgIIAEoCVIVbGFzdFVwZGF0ZUNoYXB0ZXJOYW1lEjAKE2xhc3RVcGRhdGVDaGFwdGVySWQYCSABKANSE2xhc3RVcGRhdGVDaGFwdGVySWQSJgoObGFzdFVwZGF0ZXRpbWUYCiABKANSDmxhc3RVcGRhdGV0aW1l'); diff --git a/lib/models/proto/news.pb.dart b/lib/models/proto/news.pb.dart new file mode 100644 index 0000000..4593528 --- /dev/null +++ b/lib/models/proto/news.pb.dart @@ -0,0 +1,570 @@ +/// +// Generated code. Do not modify. +// source: news.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name, depend_on_referenced_packages, no_leading_underscores_for_local_identifiers + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class NewsListResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NewsListResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..pc( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + $pb.PbFieldType.PM, + subBuilder: NewsListInfoProto.create) + ..hasRequiredFields = false; + + NewsListResponseProto._() : super(); + factory NewsListResponseProto({ + $core.int? errno, + $core.String? errmsg, + $core.Iterable? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data.addAll(data); + } + return _result; + } + factory NewsListResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NewsListResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NewsListResponseProto clone() => + NewsListResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NewsListResponseProto copyWith( + void Function(NewsListResponseProto) updates) => + super.copyWith((message) => updates(message as NewsListResponseProto)) + as NewsListResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NewsListResponseProto create() => NewsListResponseProto._(); + NewsListResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NewsListResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NewsListResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + $core.List get data => $_getList(2); +} + +class NewsListInfoProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NewsListInfoProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'articleId', + protoName: 'articleId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'title') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fromName', + protoName: 'fromName') + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fromUrl', + protoName: 'fromUrl') + ..aInt64( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'createTime', + protoName: 'createTime') + ..a<$core.int>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'isForeign', + $pb.PbFieldType.O3, + protoName: 'isForeign') + ..aOS( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'foreignUrl', + protoName: 'foreignUrl') + ..aOS( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'intro') + ..aInt64( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'authorId', + protoName: 'authorId') + ..a<$core.int>( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'status', + $pb.PbFieldType.O3) + ..aOS( + 11, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'rowPicUrl', + protoName: 'rowPicUrl') + ..aOS( + 12, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'colPicUrl', + protoName: 'colPicUrl') + ..a<$core.int>( + 13, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'qchatShow', + $pb.PbFieldType.O3, + protoName: 'qchatShow') + ..aOS( + 14, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'pageUrl', + protoName: 'pageUrl') + ..aInt64( + 15, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'commentAmount', + protoName: 'commentAmount') + ..aInt64( + 16, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'authorUid', + protoName: 'authorUid') + ..aOS( + 17, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cover') + ..aOS( + 18, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'nickname') + ..aInt64( + 19, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'moodAmount', + protoName: 'moodAmount') + ..hasRequiredFields = false; + + NewsListInfoProto._() : super(); + factory NewsListInfoProto({ + $fixnum.Int64? articleId, + $core.String? title, + $core.String? fromName, + $core.String? fromUrl, + $fixnum.Int64? createTime, + $core.int? isForeign, + $core.String? foreignUrl, + $core.String? intro, + $fixnum.Int64? authorId, + $core.int? status, + $core.String? rowPicUrl, + $core.String? colPicUrl, + $core.int? qchatShow, + $core.String? pageUrl, + $fixnum.Int64? commentAmount, + $fixnum.Int64? authorUid, + $core.String? cover, + $core.String? nickname, + $fixnum.Int64? moodAmount, + }) { + final _result = create(); + if (articleId != null) { + _result.articleId = articleId; + } + if (title != null) { + _result.title = title; + } + if (fromName != null) { + _result.fromName = fromName; + } + if (fromUrl != null) { + _result.fromUrl = fromUrl; + } + if (createTime != null) { + _result.createTime = createTime; + } + if (isForeign != null) { + _result.isForeign = isForeign; + } + if (foreignUrl != null) { + _result.foreignUrl = foreignUrl; + } + if (intro != null) { + _result.intro = intro; + } + if (authorId != null) { + _result.authorId = authorId; + } + if (status != null) { + _result.status = status; + } + if (rowPicUrl != null) { + _result.rowPicUrl = rowPicUrl; + } + if (colPicUrl != null) { + _result.colPicUrl = colPicUrl; + } + if (qchatShow != null) { + _result.qchatShow = qchatShow; + } + if (pageUrl != null) { + _result.pageUrl = pageUrl; + } + if (commentAmount != null) { + _result.commentAmount = commentAmount; + } + if (authorUid != null) { + _result.authorUid = authorUid; + } + if (cover != null) { + _result.cover = cover; + } + if (nickname != null) { + _result.nickname = nickname; + } + if (moodAmount != null) { + _result.moodAmount = moodAmount; + } + return _result; + } + factory NewsListInfoProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NewsListInfoProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NewsListInfoProto clone() => NewsListInfoProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NewsListInfoProto copyWith(void Function(NewsListInfoProto) updates) => + super.copyWith((message) => updates(message as NewsListInfoProto)) + as NewsListInfoProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NewsListInfoProto create() => NewsListInfoProto._(); + NewsListInfoProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NewsListInfoProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NewsListInfoProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get articleId => $_getI64(0); + @$pb.TagNumber(1) + set articleId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasArticleId() => $_has(0); + @$pb.TagNumber(1) + void clearArticleId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get title => $_getSZ(1); + @$pb.TagNumber(2) + set title($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasTitle() => $_has(1); + @$pb.TagNumber(2) + void clearTitle() => clearField(2); + + @$pb.TagNumber(3) + $core.String get fromName => $_getSZ(2); + @$pb.TagNumber(3) + set fromName($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasFromName() => $_has(2); + @$pb.TagNumber(3) + void clearFromName() => clearField(3); + + @$pb.TagNumber(4) + $core.String get fromUrl => $_getSZ(3); + @$pb.TagNumber(4) + set fromUrl($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasFromUrl() => $_has(3); + @$pb.TagNumber(4) + void clearFromUrl() => clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get createTime => $_getI64(4); + @$pb.TagNumber(5) + set createTime($fixnum.Int64 v) { + $_setInt64(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasCreateTime() => $_has(4); + @$pb.TagNumber(5) + void clearCreateTime() => clearField(5); + + @$pb.TagNumber(6) + $core.int get isForeign => $_getIZ(5); + @$pb.TagNumber(6) + set isForeign($core.int v) { + $_setSignedInt32(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasIsForeign() => $_has(5); + @$pb.TagNumber(6) + void clearIsForeign() => clearField(6); + + @$pb.TagNumber(7) + $core.String get foreignUrl => $_getSZ(6); + @$pb.TagNumber(7) + set foreignUrl($core.String v) { + $_setString(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasForeignUrl() => $_has(6); + @$pb.TagNumber(7) + void clearForeignUrl() => clearField(7); + + @$pb.TagNumber(8) + $core.String get intro => $_getSZ(7); + @$pb.TagNumber(8) + set intro($core.String v) { + $_setString(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasIntro() => $_has(7); + @$pb.TagNumber(8) + void clearIntro() => clearField(8); + + @$pb.TagNumber(9) + $fixnum.Int64 get authorId => $_getI64(8); + @$pb.TagNumber(9) + set authorId($fixnum.Int64 v) { + $_setInt64(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasAuthorId() => $_has(8); + @$pb.TagNumber(9) + void clearAuthorId() => clearField(9); + + @$pb.TagNumber(10) + $core.int get status => $_getIZ(9); + @$pb.TagNumber(10) + set status($core.int v) { + $_setSignedInt32(9, v); + } + + @$pb.TagNumber(10) + $core.bool hasStatus() => $_has(9); + @$pb.TagNumber(10) + void clearStatus() => clearField(10); + + @$pb.TagNumber(11) + $core.String get rowPicUrl => $_getSZ(10); + @$pb.TagNumber(11) + set rowPicUrl($core.String v) { + $_setString(10, v); + } + + @$pb.TagNumber(11) + $core.bool hasRowPicUrl() => $_has(10); + @$pb.TagNumber(11) + void clearRowPicUrl() => clearField(11); + + @$pb.TagNumber(12) + $core.String get colPicUrl => $_getSZ(11); + @$pb.TagNumber(12) + set colPicUrl($core.String v) { + $_setString(11, v); + } + + @$pb.TagNumber(12) + $core.bool hasColPicUrl() => $_has(11); + @$pb.TagNumber(12) + void clearColPicUrl() => clearField(12); + + @$pb.TagNumber(13) + $core.int get qchatShow => $_getIZ(12); + @$pb.TagNumber(13) + set qchatShow($core.int v) { + $_setSignedInt32(12, v); + } + + @$pb.TagNumber(13) + $core.bool hasQchatShow() => $_has(12); + @$pb.TagNumber(13) + void clearQchatShow() => clearField(13); + + @$pb.TagNumber(14) + $core.String get pageUrl => $_getSZ(13); + @$pb.TagNumber(14) + set pageUrl($core.String v) { + $_setString(13, v); + } + + @$pb.TagNumber(14) + $core.bool hasPageUrl() => $_has(13); + @$pb.TagNumber(14) + void clearPageUrl() => clearField(14); + + @$pb.TagNumber(15) + $fixnum.Int64 get commentAmount => $_getI64(14); + @$pb.TagNumber(15) + set commentAmount($fixnum.Int64 v) { + $_setInt64(14, v); + } + + @$pb.TagNumber(15) + $core.bool hasCommentAmount() => $_has(14); + @$pb.TagNumber(15) + void clearCommentAmount() => clearField(15); + + @$pb.TagNumber(16) + $fixnum.Int64 get authorUid => $_getI64(15); + @$pb.TagNumber(16) + set authorUid($fixnum.Int64 v) { + $_setInt64(15, v); + } + + @$pb.TagNumber(16) + $core.bool hasAuthorUid() => $_has(15); + @$pb.TagNumber(16) + void clearAuthorUid() => clearField(16); + + @$pb.TagNumber(17) + $core.String get cover => $_getSZ(16); + @$pb.TagNumber(17) + set cover($core.String v) { + $_setString(16, v); + } + + @$pb.TagNumber(17) + $core.bool hasCover() => $_has(16); + @$pb.TagNumber(17) + void clearCover() => clearField(17); + + @$pb.TagNumber(18) + $core.String get nickname => $_getSZ(17); + @$pb.TagNumber(18) + set nickname($core.String v) { + $_setString(17, v); + } + + @$pb.TagNumber(18) + $core.bool hasNickname() => $_has(17); + @$pb.TagNumber(18) + void clearNickname() => clearField(18); + + @$pb.TagNumber(19) + $fixnum.Int64 get moodAmount => $_getI64(18); + @$pb.TagNumber(19) + set moodAmount($fixnum.Int64 v) { + $_setInt64(18, v); + } + + @$pb.TagNumber(19) + $core.bool hasMoodAmount() => $_has(18); + @$pb.TagNumber(19) + void clearMoodAmount() => clearField(19); +} diff --git a/lib/models/proto/news.pbjson.dart b/lib/models/proto/news.pbjson.dart new file mode 100644 index 0000000..863e6a1 --- /dev/null +++ b/lib/models/proto/news.pbjson.dart @@ -0,0 +1,50 @@ +/// +// Generated code. Do not modify. +// source: news.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name + +import 'dart:core' as $core; +import 'dart:convert' as $convert; +import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use newsListResponseProtoDescriptor instead') +const NewsListResponseProto$json = const { + '1': 'NewsListResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 3, '5': 11, '6': '.NewsListInfoProto', '10': 'data'}, + ], +}; + +/// Descriptor for `NewsListResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List newsListResponseProtoDescriptor = $convert.base64Decode('ChVOZXdzTGlzdFJlc3BvbnNlUHJvdG8SFAoFZXJybm8YASABKAVSBWVycm5vEhYKBmVycm1zZxgCIAEoCVIGZXJybXNnEiYKBGRhdGEYAyADKAsyEi5OZXdzTGlzdEluZm9Qcm90b1IEZGF0YQ=='); +@$core.Deprecated('Use newsListInfoProtoDescriptor instead') +const NewsListInfoProto$json = const { + '1': 'NewsListInfoProto', + '2': const [ + const {'1': 'articleId', '3': 1, '4': 1, '5': 3, '10': 'articleId'}, + const {'1': 'title', '3': 2, '4': 1, '5': 9, '10': 'title'}, + const {'1': 'fromName', '3': 3, '4': 1, '5': 9, '10': 'fromName'}, + const {'1': 'fromUrl', '3': 4, '4': 1, '5': 9, '10': 'fromUrl'}, + const {'1': 'createTime', '3': 5, '4': 1, '5': 3, '10': 'createTime'}, + const {'1': 'isForeign', '3': 6, '4': 1, '5': 5, '10': 'isForeign'}, + const {'1': 'foreignUrl', '3': 7, '4': 1, '5': 9, '10': 'foreignUrl'}, + const {'1': 'intro', '3': 8, '4': 1, '5': 9, '10': 'intro'}, + const {'1': 'authorId', '3': 9, '4': 1, '5': 3, '10': 'authorId'}, + const {'1': 'status', '3': 10, '4': 1, '5': 5, '10': 'status'}, + const {'1': 'rowPicUrl', '3': 11, '4': 1, '5': 9, '10': 'rowPicUrl'}, + const {'1': 'colPicUrl', '3': 12, '4': 1, '5': 9, '10': 'colPicUrl'}, + const {'1': 'qchatShow', '3': 13, '4': 1, '5': 5, '10': 'qchatShow'}, + const {'1': 'pageUrl', '3': 14, '4': 1, '5': 9, '10': 'pageUrl'}, + const {'1': 'commentAmount', '3': 15, '4': 1, '5': 3, '10': 'commentAmount'}, + const {'1': 'authorUid', '3': 16, '4': 1, '5': 3, '10': 'authorUid'}, + const {'1': 'cover', '3': 17, '4': 1, '5': 9, '10': 'cover'}, + const {'1': 'nickname', '3': 18, '4': 1, '5': 9, '10': 'nickname'}, + const {'1': 'moodAmount', '3': 19, '4': 1, '5': 3, '10': 'moodAmount'}, + ], +}; + +/// Descriptor for `NewsListInfoProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List newsListInfoProtoDescriptor = $convert.base64Decode('ChFOZXdzTGlzdEluZm9Qcm90bxIcCglhcnRpY2xlSWQYASABKANSCWFydGljbGVJZBIUCgV0aXRsZRgCIAEoCVIFdGl0bGUSGgoIZnJvbU5hbWUYAyABKAlSCGZyb21OYW1lEhgKB2Zyb21VcmwYBCABKAlSB2Zyb21VcmwSHgoKY3JlYXRlVGltZRgFIAEoA1IKY3JlYXRlVGltZRIcCglpc0ZvcmVpZ24YBiABKAVSCWlzRm9yZWlnbhIeCgpmb3JlaWduVXJsGAcgASgJUgpmb3JlaWduVXJsEhQKBWludHJvGAggASgJUgVpbnRybxIaCghhdXRob3JJZBgJIAEoA1IIYXV0aG9ySWQSFgoGc3RhdHVzGAogASgFUgZzdGF0dXMSHAoJcm93UGljVXJsGAsgASgJUglyb3dQaWNVcmwSHAoJY29sUGljVXJsGAwgASgJUgljb2xQaWNVcmwSHAoJcWNoYXRTaG93GA0gASgFUglxY2hhdFNob3cSGAoHcGFnZVVybBgOIAEoCVIHcGFnZVVybBIkCg1jb21tZW50QW1vdW50GA8gASgDUg1jb21tZW50QW1vdW50EhwKCWF1dGhvclVpZBgQIAEoA1IJYXV0aG9yVWlkEhQKBWNvdmVyGBEgASgJUgVjb3ZlchIaCghuaWNrbmFtZRgSIAEoCVIIbmlja25hbWUSHgoKbW9vZEFtb3VudBgTIAEoA1IKbW9vZEFtb3VudA=='); diff --git a/lib/models/proto/novel.pb.dart b/lib/models/proto/novel.pb.dart new file mode 100644 index 0000000..f0b30e4 --- /dev/null +++ b/lib/models/proto/novel.pb.dart @@ -0,0 +1,1030 @@ +/// +// Generated code. Do not modify. +// source: novel.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name, no_leading_underscores_for_local_identifiers, depend_on_referenced_packages + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class NovelChapterDetailProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NovelChapterDetailProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterId', + protoName: 'chapterId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterName', + protoName: 'chapterName') + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapterOrder', + $pb.PbFieldType.O3, + protoName: 'chapterOrder') + ..hasRequiredFields = false; + + NovelChapterDetailProto._() : super(); + factory NovelChapterDetailProto({ + $fixnum.Int64? chapterId, + $core.String? chapterName, + $core.int? chapterOrder, + }) { + final _result = create(); + if (chapterId != null) { + _result.chapterId = chapterId; + } + if (chapterName != null) { + _result.chapterName = chapterName; + } + if (chapterOrder != null) { + _result.chapterOrder = chapterOrder; + } + return _result; + } + factory NovelChapterDetailProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NovelChapterDetailProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NovelChapterDetailProto clone() => + NovelChapterDetailProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NovelChapterDetailProto copyWith( + void Function(NovelChapterDetailProto) updates) => + super.copyWith((message) => updates(message as NovelChapterDetailProto)) + as NovelChapterDetailProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NovelChapterDetailProto create() => NovelChapterDetailProto._(); + NovelChapterDetailProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NovelChapterDetailProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NovelChapterDetailProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get chapterId => $_getI64(0); + @$pb.TagNumber(1) + set chapterId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasChapterId() => $_has(0); + @$pb.TagNumber(1) + void clearChapterId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get chapterName => $_getSZ(1); + @$pb.TagNumber(2) + set chapterName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasChapterName() => $_has(1); + @$pb.TagNumber(2) + void clearChapterName() => clearField(2); + + @$pb.TagNumber(3) + $core.int get chapterOrder => $_getIZ(2); + @$pb.TagNumber(3) + set chapterOrder($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasChapterOrder() => $_has(2); + @$pb.TagNumber(3) + void clearChapterOrder() => clearField(3); +} + +class NovelVolumeProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NovelVolumeProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'volumeId') + ..aInt64( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lnovelId') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'volumeName') + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'volumeOrder', + $pb.PbFieldType.O3) + ..aInt64( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'addtime') + ..a<$core.int>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'sumChapters', + $pb.PbFieldType.O3) + ..hasRequiredFields = false; + + NovelVolumeProto._() : super(); + factory NovelVolumeProto({ + $fixnum.Int64? volumeId, + $fixnum.Int64? lnovelId, + $core.String? volumeName, + $core.int? volumeOrder, + $fixnum.Int64? addtime, + $core.int? sumChapters, + }) { + final _result = create(); + if (volumeId != null) { + _result.volumeId = volumeId; + } + if (lnovelId != null) { + _result.lnovelId = lnovelId; + } + if (volumeName != null) { + _result.volumeName = volumeName; + } + if (volumeOrder != null) { + _result.volumeOrder = volumeOrder; + } + if (addtime != null) { + _result.addtime = addtime; + } + if (sumChapters != null) { + _result.sumChapters = sumChapters; + } + return _result; + } + factory NovelVolumeProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NovelVolumeProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NovelVolumeProto clone() => NovelVolumeProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NovelVolumeProto copyWith(void Function(NovelVolumeProto) updates) => + super.copyWith((message) => updates(message as NovelVolumeProto)) + as NovelVolumeProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NovelVolumeProto create() => NovelVolumeProto._(); + NovelVolumeProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NovelVolumeProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NovelVolumeProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get volumeId => $_getI64(0); + @$pb.TagNumber(1) + set volumeId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasVolumeId() => $_has(0); + @$pb.TagNumber(1) + void clearVolumeId() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get lnovelId => $_getI64(1); + @$pb.TagNumber(2) + set lnovelId($fixnum.Int64 v) { + $_setInt64(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasLnovelId() => $_has(1); + @$pb.TagNumber(2) + void clearLnovelId() => clearField(2); + + @$pb.TagNumber(3) + $core.String get volumeName => $_getSZ(2); + @$pb.TagNumber(3) + set volumeName($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasVolumeName() => $_has(2); + @$pb.TagNumber(3) + void clearVolumeName() => clearField(3); + + @$pb.TagNumber(4) + $core.int get volumeOrder => $_getIZ(3); + @$pb.TagNumber(4) + set volumeOrder($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasVolumeOrder() => $_has(3); + @$pb.TagNumber(4) + void clearVolumeOrder() => clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get addtime => $_getI64(4); + @$pb.TagNumber(5) + set addtime($fixnum.Int64 v) { + $_setInt64(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasAddtime() => $_has(4); + @$pb.TagNumber(5) + void clearAddtime() => clearField(5); + + @$pb.TagNumber(6) + $core.int get sumChapters => $_getIZ(5); + @$pb.TagNumber(6) + set sumChapters($core.int v) { + $_setSignedInt32(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasSumChapters() => $_has(5); + @$pb.TagNumber(6) + void clearSumChapters() => clearField(6); +} + +class NovelChapterResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NovelChapterResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..pc( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + $pb.PbFieldType.PM, + subBuilder: NovelVolumeDetailProto.create) + ..hasRequiredFields = false; + + NovelChapterResponseProto._() : super(); + factory NovelChapterResponseProto({ + $core.int? errno, + $core.String? errmsg, + $core.Iterable? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data.addAll(data); + } + return _result; + } + factory NovelChapterResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NovelChapterResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NovelChapterResponseProto clone() => + NovelChapterResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NovelChapterResponseProto copyWith( + void Function(NovelChapterResponseProto) updates) => + super.copyWith((message) => updates(message as NovelChapterResponseProto)) + as NovelChapterResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NovelChapterResponseProto create() => NovelChapterResponseProto._(); + NovelChapterResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NovelChapterResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NovelChapterResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + $core.List get data => $_getList(2); +} + +class NovelVolumeDetailProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NovelVolumeDetailProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'volumeId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'volumeName') + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'volumeOrder', + $pb.PbFieldType.O3) + ..pc( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'chapters', + $pb.PbFieldType.PM, + subBuilder: NovelChapterDetailProto.create) + ..hasRequiredFields = false; + + NovelVolumeDetailProto._() : super(); + factory NovelVolumeDetailProto({ + $fixnum.Int64? volumeId, + $core.String? volumeName, + $core.int? volumeOrder, + $core.Iterable? chapters, + }) { + final _result = create(); + if (volumeId != null) { + _result.volumeId = volumeId; + } + if (volumeName != null) { + _result.volumeName = volumeName; + } + if (volumeOrder != null) { + _result.volumeOrder = volumeOrder; + } + if (chapters != null) { + _result.chapters.addAll(chapters); + } + return _result; + } + factory NovelVolumeDetailProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NovelVolumeDetailProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NovelVolumeDetailProto clone() => + NovelVolumeDetailProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NovelVolumeDetailProto copyWith( + void Function(NovelVolumeDetailProto) updates) => + super.copyWith((message) => updates(message as NovelVolumeDetailProto)) + as NovelVolumeDetailProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NovelVolumeDetailProto create() => NovelVolumeDetailProto._(); + NovelVolumeDetailProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NovelVolumeDetailProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NovelVolumeDetailProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get volumeId => $_getI64(0); + @$pb.TagNumber(1) + set volumeId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasVolumeId() => $_has(0); + @$pb.TagNumber(1) + void clearVolumeId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get volumeName => $_getSZ(1); + @$pb.TagNumber(2) + set volumeName($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasVolumeName() => $_has(1); + @$pb.TagNumber(2) + void clearVolumeName() => clearField(2); + + @$pb.TagNumber(3) + $core.int get volumeOrder => $_getIZ(2); + @$pb.TagNumber(3) + set volumeOrder($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasVolumeOrder() => $_has(2); + @$pb.TagNumber(3) + void clearVolumeOrder() => clearField(3); + + @$pb.TagNumber(4) + $core.List get chapters => $_getList(3); +} + +class NovelDetailProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NovelDetailProto', + createEmptyInstance: create) + ..aInt64( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'novelId') + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'name') + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'zone') + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'status') + ..aOS( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateVolumeName') + ..aOS( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateChapterName') + ..aInt64( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateVolumeId') + ..aInt64( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateChapterId') + ..aInt64( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lastUpdateTime') + ..aOS( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cover') + ..aInt64( + 11, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'hotHits') + ..aOS( + 12, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'introduction') + ..pPS( + 13, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'types') + ..aOS( + 14, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'authors') + ..aOS( + 15, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'firstLetter') + ..aInt64( + 16, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'subscribeNum') + ..aInt64( + 17, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'redisUpdateTime') + ..pc( + 18, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'volume', + $pb.PbFieldType.PM, + subBuilder: NovelVolumeProto.create) + ..hasRequiredFields = false; + + NovelDetailProto._() : super(); + factory NovelDetailProto({ + $fixnum.Int64? novelId, + $core.String? name, + $core.String? zone, + $core.String? status, + $core.String? lastUpdateVolumeName, + $core.String? lastUpdateChapterName, + $fixnum.Int64? lastUpdateVolumeId, + $fixnum.Int64? lastUpdateChapterId, + $fixnum.Int64? lastUpdateTime, + $core.String? cover, + $fixnum.Int64? hotHits, + $core.String? introduction, + $core.Iterable<$core.String>? types, + $core.String? authors, + $core.String? firstLetter, + $fixnum.Int64? subscribeNum, + $fixnum.Int64? redisUpdateTime, + $core.Iterable? volume, + }) { + final _result = create(); + if (novelId != null) { + _result.novelId = novelId; + } + if (name != null) { + _result.name = name; + } + if (zone != null) { + _result.zone = zone; + } + if (status != null) { + _result.status = status; + } + if (lastUpdateVolumeName != null) { + _result.lastUpdateVolumeName = lastUpdateVolumeName; + } + if (lastUpdateChapterName != null) { + _result.lastUpdateChapterName = lastUpdateChapterName; + } + if (lastUpdateVolumeId != null) { + _result.lastUpdateVolumeId = lastUpdateVolumeId; + } + if (lastUpdateChapterId != null) { + _result.lastUpdateChapterId = lastUpdateChapterId; + } + if (lastUpdateTime != null) { + _result.lastUpdateTime = lastUpdateTime; + } + if (cover != null) { + _result.cover = cover; + } + if (hotHits != null) { + _result.hotHits = hotHits; + } + if (introduction != null) { + _result.introduction = introduction; + } + if (types != null) { + _result.types.addAll(types); + } + if (authors != null) { + _result.authors = authors; + } + if (firstLetter != null) { + _result.firstLetter = firstLetter; + } + if (subscribeNum != null) { + _result.subscribeNum = subscribeNum; + } + if (redisUpdateTime != null) { + _result.redisUpdateTime = redisUpdateTime; + } + if (volume != null) { + _result.volume.addAll(volume); + } + return _result; + } + factory NovelDetailProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NovelDetailProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NovelDetailProto clone() => NovelDetailProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NovelDetailProto copyWith(void Function(NovelDetailProto) updates) => + super.copyWith((message) => updates(message as NovelDetailProto)) + as NovelDetailProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NovelDetailProto create() => NovelDetailProto._(); + NovelDetailProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NovelDetailProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NovelDetailProto? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get novelId => $_getI64(0); + @$pb.TagNumber(1) + set novelId($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasNovelId() => $_has(0); + @$pb.TagNumber(1) + void clearNovelId() => clearField(1); + + @$pb.TagNumber(2) + $core.String get name => $_getSZ(1); + @$pb.TagNumber(2) + set name($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasName() => $_has(1); + @$pb.TagNumber(2) + void clearName() => clearField(2); + + @$pb.TagNumber(3) + $core.String get zone => $_getSZ(2); + @$pb.TagNumber(3) + set zone($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasZone() => $_has(2); + @$pb.TagNumber(3) + void clearZone() => clearField(3); + + @$pb.TagNumber(4) + $core.String get status => $_getSZ(3); + @$pb.TagNumber(4) + set status($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasStatus() => $_has(3); + @$pb.TagNumber(4) + void clearStatus() => clearField(4); + + @$pb.TagNumber(5) + $core.String get lastUpdateVolumeName => $_getSZ(4); + @$pb.TagNumber(5) + set lastUpdateVolumeName($core.String v) { + $_setString(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasLastUpdateVolumeName() => $_has(4); + @$pb.TagNumber(5) + void clearLastUpdateVolumeName() => clearField(5); + + @$pb.TagNumber(6) + $core.String get lastUpdateChapterName => $_getSZ(5); + @$pb.TagNumber(6) + set lastUpdateChapterName($core.String v) { + $_setString(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasLastUpdateChapterName() => $_has(5); + @$pb.TagNumber(6) + void clearLastUpdateChapterName() => clearField(6); + + @$pb.TagNumber(7) + $fixnum.Int64 get lastUpdateVolumeId => $_getI64(6); + @$pb.TagNumber(7) + set lastUpdateVolumeId($fixnum.Int64 v) { + $_setInt64(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasLastUpdateVolumeId() => $_has(6); + @$pb.TagNumber(7) + void clearLastUpdateVolumeId() => clearField(7); + + @$pb.TagNumber(8) + $fixnum.Int64 get lastUpdateChapterId => $_getI64(7); + @$pb.TagNumber(8) + set lastUpdateChapterId($fixnum.Int64 v) { + $_setInt64(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasLastUpdateChapterId() => $_has(7); + @$pb.TagNumber(8) + void clearLastUpdateChapterId() => clearField(8); + + @$pb.TagNumber(9) + $fixnum.Int64 get lastUpdateTime => $_getI64(8); + @$pb.TagNumber(9) + set lastUpdateTime($fixnum.Int64 v) { + $_setInt64(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasLastUpdateTime() => $_has(8); + @$pb.TagNumber(9) + void clearLastUpdateTime() => clearField(9); + + @$pb.TagNumber(10) + $core.String get cover => $_getSZ(9); + @$pb.TagNumber(10) + set cover($core.String v) { + $_setString(9, v); + } + + @$pb.TagNumber(10) + $core.bool hasCover() => $_has(9); + @$pb.TagNumber(10) + void clearCover() => clearField(10); + + @$pb.TagNumber(11) + $fixnum.Int64 get hotHits => $_getI64(10); + @$pb.TagNumber(11) + set hotHits($fixnum.Int64 v) { + $_setInt64(10, v); + } + + @$pb.TagNumber(11) + $core.bool hasHotHits() => $_has(10); + @$pb.TagNumber(11) + void clearHotHits() => clearField(11); + + @$pb.TagNumber(12) + $core.String get introduction => $_getSZ(11); + @$pb.TagNumber(12) + set introduction($core.String v) { + $_setString(11, v); + } + + @$pb.TagNumber(12) + $core.bool hasIntroduction() => $_has(11); + @$pb.TagNumber(12) + void clearIntroduction() => clearField(12); + + @$pb.TagNumber(13) + $core.List<$core.String> get types => $_getList(12); + + @$pb.TagNumber(14) + $core.String get authors => $_getSZ(13); + @$pb.TagNumber(14) + set authors($core.String v) { + $_setString(13, v); + } + + @$pb.TagNumber(14) + $core.bool hasAuthors() => $_has(13); + @$pb.TagNumber(14) + void clearAuthors() => clearField(14); + + @$pb.TagNumber(15) + $core.String get firstLetter => $_getSZ(14); + @$pb.TagNumber(15) + set firstLetter($core.String v) { + $_setString(14, v); + } + + @$pb.TagNumber(15) + $core.bool hasFirstLetter() => $_has(14); + @$pb.TagNumber(15) + void clearFirstLetter() => clearField(15); + + @$pb.TagNumber(16) + $fixnum.Int64 get subscribeNum => $_getI64(15); + @$pb.TagNumber(16) + set subscribeNum($fixnum.Int64 v) { + $_setInt64(15, v); + } + + @$pb.TagNumber(16) + $core.bool hasSubscribeNum() => $_has(15); + @$pb.TagNumber(16) + void clearSubscribeNum() => clearField(16); + + @$pb.TagNumber(17) + $fixnum.Int64 get redisUpdateTime => $_getI64(16); + @$pb.TagNumber(17) + set redisUpdateTime($fixnum.Int64 v) { + $_setInt64(16, v); + } + + @$pb.TagNumber(17) + $core.bool hasRedisUpdateTime() => $_has(16); + @$pb.TagNumber(17) + void clearRedisUpdateTime() => clearField(17); + + @$pb.TagNumber(18) + $core.List get volume => $_getList(17); +} + +class NovelDetailResponseProto extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'NovelDetailResponseProto', + createEmptyInstance: create) + ..a<$core.int>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errno', + $pb.PbFieldType.O3) + ..aOS( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'errmsg') + ..aOM( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'data', + subBuilder: NovelDetailProto.create) + ..hasRequiredFields = false; + + NovelDetailResponseProto._() : super(); + factory NovelDetailResponseProto({ + $core.int? errno, + $core.String? errmsg, + NovelDetailProto? data, + }) { + final _result = create(); + if (errno != null) { + _result.errno = errno; + } + if (errmsg != null) { + _result.errmsg = errmsg; + } + if (data != null) { + _result.data = data; + } + return _result; + } + factory NovelDetailResponseProto.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory NovelDetailResponseProto.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + NovelDetailResponseProto clone() => + NovelDetailResponseProto()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + NovelDetailResponseProto copyWith( + void Function(NovelDetailResponseProto) updates) => + super.copyWith((message) => updates(message as NovelDetailResponseProto)) + as NovelDetailResponseProto; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static NovelDetailResponseProto create() => NovelDetailResponseProto._(); + NovelDetailResponseProto createEmptyInstance() => create(); + static $pb.PbList createRepeated() => + $pb.PbList(); + @$core.pragma('dart2js:noInline') + static NovelDetailResponseProto getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NovelDetailResponseProto? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get errno => $_getIZ(0); + @$pb.TagNumber(1) + set errno($core.int v) { + $_setSignedInt32(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasErrno() => $_has(0); + @$pb.TagNumber(1) + void clearErrno() => clearField(1); + + @$pb.TagNumber(2) + $core.String get errmsg => $_getSZ(1); + @$pb.TagNumber(2) + set errmsg($core.String v) { + $_setString(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasErrmsg() => $_has(1); + @$pb.TagNumber(2) + void clearErrmsg() => clearField(2); + + @$pb.TagNumber(3) + NovelDetailProto get data => $_getN(2); + @$pb.TagNumber(3) + set data(NovelDetailProto v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasData() => $_has(2); + @$pb.TagNumber(3) + void clearData() => clearField(3); + @$pb.TagNumber(3) + NovelDetailProto ensureData() => $_ensure(2); +} diff --git a/lib/models/proto/novel.pbjson.dart b/lib/models/proto/novel.pbjson.dart new file mode 100644 index 0000000..71465c4 --- /dev/null +++ b/lib/models/proto/novel.pbjson.dart @@ -0,0 +1,101 @@ +/// +// Generated code. Do not modify. +// source: novel.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name + +import 'dart:core' as $core; +import 'dart:convert' as $convert; +import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use novelChapterDetailProtoDescriptor instead') +const NovelChapterDetailProto$json = const { + '1': 'NovelChapterDetailProto', + '2': const [ + const {'1': 'chapterId', '3': 1, '4': 1, '5': 3, '10': 'chapterId'}, + const {'1': 'chapterName', '3': 2, '4': 1, '5': 9, '10': 'chapterName'}, + const {'1': 'chapterOrder', '3': 3, '4': 1, '5': 5, '10': 'chapterOrder'}, + ], +}; + +/// Descriptor for `NovelChapterDetailProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List novelChapterDetailProtoDescriptor = $convert.base64Decode('ChdOb3ZlbENoYXB0ZXJEZXRhaWxQcm90bxIcCgljaGFwdGVySWQYASABKANSCWNoYXB0ZXJJZBIgCgtjaGFwdGVyTmFtZRgCIAEoCVILY2hhcHRlck5hbWUSIgoMY2hhcHRlck9yZGVyGAMgASgFUgxjaGFwdGVyT3JkZXI='); +@$core.Deprecated('Use novelVolumeProtoDescriptor instead') +const NovelVolumeProto$json = const { + '1': 'NovelVolumeProto', + '2': const [ + const {'1': 'volume_id', '3': 1, '4': 1, '5': 3, '10': 'volumeId'}, + const {'1': 'lnovel_id', '3': 2, '4': 1, '5': 3, '10': 'lnovelId'}, + const {'1': 'volume_name', '3': 3, '4': 1, '5': 9, '10': 'volumeName'}, + const {'1': 'volume_order', '3': 4, '4': 1, '5': 5, '10': 'volumeOrder'}, + const {'1': 'addtime', '3': 5, '4': 1, '5': 3, '10': 'addtime'}, + const {'1': 'sum_chapters', '3': 6, '4': 1, '5': 5, '10': 'sumChapters'}, + ], +}; + +/// Descriptor for `NovelVolumeProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List novelVolumeProtoDescriptor = $convert.base64Decode('ChBOb3ZlbFZvbHVtZVByb3RvEhsKCXZvbHVtZV9pZBgBIAEoA1IIdm9sdW1lSWQSGwoJbG5vdmVsX2lkGAIgASgDUghsbm92ZWxJZBIfCgt2b2x1bWVfbmFtZRgDIAEoCVIKdm9sdW1lTmFtZRIhCgx2b2x1bWVfb3JkZXIYBCABKAVSC3ZvbHVtZU9yZGVyEhgKB2FkZHRpbWUYBSABKANSB2FkZHRpbWUSIQoMc3VtX2NoYXB0ZXJzGAYgASgFUgtzdW1DaGFwdGVycw=='); +@$core.Deprecated('Use novelChapterResponseProtoDescriptor instead') +const NovelChapterResponseProto$json = const { + '1': 'NovelChapterResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 3, '5': 11, '6': '.NovelVolumeDetailProto', '10': 'data'}, + ], +}; + +/// Descriptor for `NovelChapterResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List novelChapterResponseProtoDescriptor = $convert.base64Decode('ChlOb3ZlbENoYXB0ZXJSZXNwb25zZVByb3RvEhQKBWVycm5vGAEgASgFUgVlcnJubxIWCgZlcnJtc2cYAiABKAlSBmVycm1zZxIrCgRkYXRhGAMgAygLMhcuTm92ZWxWb2x1bWVEZXRhaWxQcm90b1IEZGF0YQ=='); +@$core.Deprecated('Use novelVolumeDetailProtoDescriptor instead') +const NovelVolumeDetailProto$json = const { + '1': 'NovelVolumeDetailProto', + '2': const [ + const {'1': 'volume_id', '3': 1, '4': 1, '5': 3, '10': 'volumeId'}, + const {'1': 'volume_name', '3': 2, '4': 1, '5': 9, '10': 'volumeName'}, + const {'1': 'volume_order', '3': 3, '4': 1, '5': 5, '10': 'volumeOrder'}, + const {'1': 'chapters', '3': 4, '4': 3, '5': 11, '6': '.NovelChapterDetailProto', '10': 'chapters'}, + ], +}; + +/// Descriptor for `NovelVolumeDetailProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List novelVolumeDetailProtoDescriptor = $convert.base64Decode('ChZOb3ZlbFZvbHVtZURldGFpbFByb3RvEhsKCXZvbHVtZV9pZBgBIAEoA1IIdm9sdW1lSWQSHwoLdm9sdW1lX25hbWUYAiABKAlSCnZvbHVtZU5hbWUSIQoMdm9sdW1lX29yZGVyGAMgASgFUgt2b2x1bWVPcmRlchI0CghjaGFwdGVycxgEIAMoCzIYLk5vdmVsQ2hhcHRlckRldGFpbFByb3RvUghjaGFwdGVycw=='); +@$core.Deprecated('Use novelDetailProtoDescriptor instead') +const NovelDetailProto$json = const { + '1': 'NovelDetailProto', + '2': const [ + const {'1': 'novel_id', '3': 1, '4': 1, '5': 3, '10': 'novelId'}, + const {'1': 'name', '3': 2, '4': 1, '5': 9, '10': 'name'}, + const {'1': 'zone', '3': 3, '4': 1, '5': 9, '10': 'zone'}, + const {'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'}, + const {'1': 'last_update_volume_name', '3': 5, '4': 1, '5': 9, '10': 'lastUpdateVolumeName'}, + const {'1': 'last_update_chapter_name', '3': 6, '4': 1, '5': 9, '10': 'lastUpdateChapterName'}, + const {'1': 'last_update_volume_id', '3': 7, '4': 1, '5': 3, '10': 'lastUpdateVolumeId'}, + const {'1': 'last_update_chapter_id', '3': 8, '4': 1, '5': 3, '10': 'lastUpdateChapterId'}, + const {'1': 'last_update_time', '3': 9, '4': 1, '5': 3, '10': 'lastUpdateTime'}, + const {'1': 'cover', '3': 10, '4': 1, '5': 9, '10': 'cover'}, + const {'1': 'hot_hits', '3': 11, '4': 1, '5': 3, '10': 'hotHits'}, + const {'1': 'introduction', '3': 12, '4': 1, '5': 9, '10': 'introduction'}, + const {'1': 'types', '3': 13, '4': 3, '5': 9, '10': 'types'}, + const {'1': 'authors', '3': 14, '4': 1, '5': 9, '10': 'authors'}, + const {'1': 'first_letter', '3': 15, '4': 1, '5': 9, '10': 'firstLetter'}, + const {'1': 'subscribe_num', '3': 16, '4': 1, '5': 3, '10': 'subscribeNum'}, + const {'1': 'redis_update_time', '3': 17, '4': 1, '5': 3, '10': 'redisUpdateTime'}, + const {'1': 'volume', '3': 18, '4': 3, '5': 11, '6': '.NovelVolumeProto', '10': 'volume'}, + ], +}; + +/// Descriptor for `NovelDetailProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List novelDetailProtoDescriptor = $convert.base64Decode('ChBOb3ZlbERldGFpbFByb3RvEhkKCG5vdmVsX2lkGAEgASgDUgdub3ZlbElkEhIKBG5hbWUYAiABKAlSBG5hbWUSEgoEem9uZRgDIAEoCVIEem9uZRIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI1ChdsYXN0X3VwZGF0ZV92b2x1bWVfbmFtZRgFIAEoCVIUbGFzdFVwZGF0ZVZvbHVtZU5hbWUSNwoYbGFzdF91cGRhdGVfY2hhcHRlcl9uYW1lGAYgASgJUhVsYXN0VXBkYXRlQ2hhcHRlck5hbWUSMQoVbGFzdF91cGRhdGVfdm9sdW1lX2lkGAcgASgDUhJsYXN0VXBkYXRlVm9sdW1lSWQSMwoWbGFzdF91cGRhdGVfY2hhcHRlcl9pZBgIIAEoA1ITbGFzdFVwZGF0ZUNoYXB0ZXJJZBIoChBsYXN0X3VwZGF0ZV90aW1lGAkgASgDUg5sYXN0VXBkYXRlVGltZRIUCgVjb3ZlchgKIAEoCVIFY292ZXISGQoIaG90X2hpdHMYCyABKANSB2hvdEhpdHMSIgoMaW50cm9kdWN0aW9uGAwgASgJUgxpbnRyb2R1Y3Rpb24SFAoFdHlwZXMYDSADKAlSBXR5cGVzEhgKB2F1dGhvcnMYDiABKAlSB2F1dGhvcnMSIQoMZmlyc3RfbGV0dGVyGA8gASgJUgtmaXJzdExldHRlchIjCg1zdWJzY3JpYmVfbnVtGBAgASgDUgxzdWJzY3JpYmVOdW0SKgoRcmVkaXNfdXBkYXRlX3RpbWUYESABKANSD3JlZGlzVXBkYXRlVGltZRIpCgZ2b2x1bWUYEiADKAsyES5Ob3ZlbFZvbHVtZVByb3RvUgZ2b2x1bWU='); +@$core.Deprecated('Use novelDetailResponseProtoDescriptor instead') +const NovelDetailResponseProto$json = const { + '1': 'NovelDetailResponseProto', + '2': const [ + const {'1': 'errno', '3': 1, '4': 1, '5': 5, '10': 'errno'}, + const {'1': 'errmsg', '3': 2, '4': 1, '5': 9, '10': 'errmsg'}, + const {'1': 'data', '3': 3, '4': 1, '5': 11, '6': '.NovelDetailProto', '10': 'data'}, + ], +}; + +/// Descriptor for `NovelDetailResponseProto`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List novelDetailResponseProtoDescriptor = $convert.base64Decode('ChhOb3ZlbERldGFpbFJlc3BvbnNlUHJvdG8SFAoFZXJybm8YASABKAVSBWVycm5vEhYKBmVycm1zZxgCIAEoCVIGZXJybXNnEiUKBGRhdGEYAyABKAsyES5Ob3ZlbERldGFpbFByb3RvUgRkYXRh'); diff --git a/lib/models/user/bind_status_model.dart b/lib/models/user/bind_status_model.dart new file mode 100644 index 0000000..a418800 --- /dev/null +++ b/lib/models/user/bind_status_model.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserBindStatusModel { + UserBindStatusModel({ + required this.isBindTel, + required this.isSetPwd, + }); + + factory UserBindStatusModel.fromJson(Map json) => + UserBindStatusModel( + isBindTel: asT(json['is_bind_tel'])!, + isSetPwd: asT(json['is_set_pwd'])!, + ); + + int isBindTel; + int isSetPwd; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'is_bind_tel': isBindTel, + 'is_set_pwd': isSetPwd, + }; +} diff --git a/lib/models/user/comic_history_model.dart b/lib/models/user/comic_history_model.dart new file mode 100644 index 0000000..b7eb7c4 --- /dev/null +++ b/lib/models/user/comic_history_model.dart @@ -0,0 +1,63 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserComicHistoryModel { + UserComicHistoryModel({ + this.uid, + this.type, + required this.comicId, + this.chapterId, + this.record, + this.viewingTime, + required this.comicName, + required this.cover, + this.chapterName, + }); + + factory UserComicHistoryModel.fromJson(Map json) => + // 接口不知道那些可能为空,所以全部变为可空 + UserComicHistoryModel( + uid: asT(json['uid']) ?? 0, + type: asT(json['type']) ?? 0, + comicId: asT(json['comic_id']) ?? 0, + chapterId: asT(json['chapter_id']) ?? 0, + record: asT(json['record']) ?? 0, + viewingTime: asT(json['viewing_time']) ?? 0, + comicName: asT(json['comic_name']) ?? "未知漫画", + cover: asT(json['cover']) ?? "", + chapterName: asT(json['chapter_name']) ?? "-", + ); + + int? uid; + int? type; + int comicId; + int? chapterId; + int? record; + int? viewingTime; + String comicName; + String cover; + String? chapterName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'uid': uid, + 'type': type, + 'comic_id': comicId, + 'chapter_id': chapterId, + 'record': record, + 'viewing_time': viewingTime, + 'comic_name': comicName, + 'cover': cover, + 'chapter_name': chapterName, + }; +} diff --git a/lib/models/user/login_result_model.dart b/lib/models/user/login_result_model.dart new file mode 100644 index 0000000..6415236 --- /dev/null +++ b/lib/models/user/login_result_model.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class LoginResultModel { + LoginResultModel({ + required this.uid, + required this.nickname, + required this.token, + required this.photo, + required this.bindPhone, + required this.email, + required this.setPasswd, + }); + + factory LoginResultModel.fromJson(Map json) => + LoginResultModel( + uid: asT(json['uid'])!, + nickname: asT(json['nickname'])!, + token: asT(json['token'])!, + photo: asT(json['photo'])!, + bindPhone: asT(json['bind_phone'])!, + email: asT(json['email'])!, + setPasswd: asT(json['setPasswd'])!, + ); + + int uid; + String nickname; + String token; + String photo; + String bindPhone; + String email; + int setPasswd; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'uid': uid, + 'nickname': nickname, + 'token': token, + 'photo': photo, + 'bind_phone': bindPhone, + 'email': email, + 'setPasswd': setPasswd + }; +} diff --git a/lib/models/user/novel_history_model.dart b/lib/models/user/novel_history_model.dart new file mode 100644 index 0000000..5eaece2 --- /dev/null +++ b/lib/models/user/novel_history_model.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserNovelHistoryModel { + UserNovelHistoryModel({ + this.uid, + this.type, + required this.lnovelId, + this.volumeId, + this.chapterId, + this.record, + this.viewingTime, + this.totalNum, + required this.cover, + required this.novelName, + this.volumeName, + this.chapterName, + }); + + factory UserNovelHistoryModel.fromJson(Map json) => + // 接口不知道那些可能为空,所以全部变为可空 + UserNovelHistoryModel( + uid: asT(json['uid']) ?? 0, + type: asT(json['type']) ?? 0, + lnovelId: int.tryParse(json['lnovel_id'].toString()) ?? 0, + volumeId: asT(json['volume_id']) ?? 0, + chapterId: asT(json['chapter_id']) ?? 0, + record: asT(json['record']) ?? 0, + viewingTime: asT(json['viewing_time']) ?? 0, + totalNum: asT(json['total_num']) ?? 0, + cover: asT(json['cover']) ?? "", + novelName: asT(json['novel_name']) ?? "未知小说", + volumeName: asT(json['volume_name']) ?? "-", + chapterName: asT(json['chapter_name']) ?? "-", + ); + + int? uid; + int? type; + int lnovelId; + int? volumeId; + int? chapterId; + int? record; + int? viewingTime; + int? totalNum; + String cover; + String novelName; + String? volumeName; + String? chapterName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'uid': uid, + 'type': type, + 'lnovel_id': lnovelId, + 'volume_id': volumeId, + 'chapter_id': chapterId, + 'record': record, + 'viewing_time': viewingTime, + 'total_num': totalNum, + 'cover': cover, + 'novel_name': novelName, + 'volume_name': volumeName, + 'chapter_name': chapterName, + }; +} diff --git a/lib/models/user/subscribe_comic_model.dart b/lib/models/user/subscribe_comic_model.dart new file mode 100644 index 0000000..6963939 --- /dev/null +++ b/lib/models/user/subscribe_comic_model.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserSubscribeComicItemModel { + UserSubscribeComicItemModel({ + required this.id, + required this.title, + required this.cover, + required this.subReaded, + required this.lastUpdateChapterId, + required this.lastUpdateChapterName, + required this.comicPy, + required this.status, + required this.readingRecord, + required this.hasNew, + }); + + factory UserSubscribeComicItemModel.fromJson(Map json) => + UserSubscribeComicItemModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + cover: asT(json['cover'])!, + subReaded: asT(json['sub_readed'])!, + lastUpdateChapterId: asT(json['last_update_chapter_id'])!, + lastUpdateChapterName: asT(json['last_update_chapter_name'])!, + comicPy: asT(json['comic_py'])!, + status: asT(json['status'])!, + readingRecord: ReadingRecord.fromJson( + asT>(json['readingRecord'])!), + hasNew: (asT(json['sub_readed']) == 0).obs, + ); + + int id; + String title; + String cover; + int subReaded; + int lastUpdateChapterId; + String lastUpdateChapterName; + String comicPy; + String status; + ReadingRecord readingRecord; + + var isChecked = false.obs; + var hasNew = false.obs; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'cover': cover, + 'sub_readed': subReaded, + 'last_update_chapter_id': lastUpdateChapterId, + 'last_update_chapter_name': lastUpdateChapterName, + 'comic_py': comicPy, + 'status': status, + 'readingRecord': readingRecord, + }; +} + +class ReadingRecord { + ReadingRecord({ + required this.typeName, + required this.uid, + required this.source, + required this.bizId, + required this.chapterId, + required this.viewingTime, + required this.record, + required this.volumeId, + required this.totalNum, + required this.chapterName, + required this.volumeName, + }); + + factory ReadingRecord.fromJson(Map json) => ReadingRecord( + typeName: asT(json['type_name'])!, + uid: asT(json['uid'])!, + source: asT(json['source'])!, + bizId: asT(json['biz_id'])!, + chapterId: asT(json['chapter_id'])!, + viewingTime: asT(json['viewing_time'])!, + record: asT(json['record'])!, + volumeId: asT(json['volume_id'])!, + totalNum: asT(json['total_num'])!, + chapterName: asT(json['chapter_name'])!, + volumeName: asT(json['volume_name'])!, + ); + + String typeName; + int uid; + int source; + int bizId; + int chapterId; + int viewingTime; + int record; + int volumeId; + int totalNum; + String chapterName; + String volumeName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'type_name': typeName, + 'uid': uid, + 'source': source, + 'biz_id': bizId, + 'chapter_id': chapterId, + 'viewing_time': viewingTime, + 'record': record, + 'volume_id': volumeId, + 'total_num': totalNum, + 'chapter_name': chapterName, + 'volume_name': volumeName, + }; +} diff --git a/lib/models/user/subscribe_news_model.dart b/lib/models/user/subscribe_news_model.dart new file mode 100644 index 0000000..03f2802 --- /dev/null +++ b/lib/models/user/subscribe_news_model.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserSubscribeNewsModel { + UserSubscribeNewsModel({ + required this.subId, + required this.subTime, + required this.title, + required this.authorId, + required this.rowPicUrl, + required this.colPicUrl, + required this.isForeign, + required this.foreignUrl, + required this.userPhoto, + required this.userNickname, + required this.pageUrl, + required this.commentAmount, + required this.moodAmount, + }); + + factory UserSubscribeNewsModel.fromJson(Map json) => + UserSubscribeNewsModel( + subId: asT(json['sub_id'])!, + subTime: asT(json['sub_time'])!, + title: asT(json['title'])!, + authorId: asT(json['author_id'])!, + rowPicUrl: asT(json['row_pic_url'])!, + colPicUrl: asT(json['col_pic_url'])!, + isForeign: asT(json['is_foreign'])!, + foreignUrl: asT(json['foreign_url'])!, + userPhoto: asT(json['user_photo'])!, + userNickname: asT(json['user_nickname'])!, + pageUrl: asT(json['page_url'])!, + commentAmount: int.tryParse(json['comment_amount'].toString()) ?? 0, + moodAmount: int.tryParse(json['mood_amount'].toString()) ?? 0, + ); + + int subId; + int subTime; + String title; + int authorId; + String rowPicUrl; + String colPicUrl; + int isForeign; + String foreignUrl; + String userPhoto; + String userNickname; + String pageUrl; + int commentAmount; + int moodAmount; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'sub_id': subId, + 'sub_time': subTime, + 'title': title, + 'author_id': authorId, + 'row_pic_url': rowPicUrl, + 'col_pic_url': colPicUrl, + 'is_foreign': isForeign, + 'foreign_url': foreignUrl, + 'user_photo': userPhoto, + 'user_nickname': userNickname, + 'page_url': pageUrl, + 'comment_amount': commentAmount, + 'mood_amount': moodAmount, + }; +} diff --git a/lib/models/user/subscribe_novel_model.dart b/lib/models/user/subscribe_novel_model.dart new file mode 100644 index 0000000..b9f3c01 --- /dev/null +++ b/lib/models/user/subscribe_novel_model.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +import 'package:get/get.dart'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserSubscribeNovelModel { + UserSubscribeNovelModel({ + required this.id, + required this.title, + this.cover, + this.subReaded, + this.lastUpdateChapterId, + this.lastUpdateChapterName, + this.comicPy, + this.status, + required this.readingRecord, + required this.hasNew, + }); + + factory UserSubscribeNovelModel.fromJson(Map json) => + UserSubscribeNovelModel( + id: asT(json['id'])!, + title: asT(json['title'])!, + cover: asT(json['cover']), + subReaded: asT(json['sub_readed']), + lastUpdateChapterId: asT(json['last_update_chapter_id']), + lastUpdateChapterName: asT(json['last_update_chapter_name']), + comicPy: asT(json['comic_py']), + status: asT(json['status']), + readingRecord: UserSubscribeNovelReadingRecordModel.fromJson( + asT>(json['readingRecord'])!), + hasNew: (asT(json['sub_readed']) == 0).obs, + ); + + int id; + String title; + String? cover; + int? subReaded; + int? lastUpdateChapterId; + String? lastUpdateChapterName; + String? comicPy; + String? status; + UserSubscribeNovelReadingRecordModel readingRecord; + + var isChecked = false.obs; + var hasNew = false.obs; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'id': id, + 'title': title, + 'cover': cover, + 'sub_readed': subReaded, + 'last_update_chapter_id': lastUpdateChapterId, + 'last_update_chapter_name': lastUpdateChapterName, + 'comic_py': comicPy, + 'status': status, + 'readingRecord': readingRecord, + }; +} + +class UserSubscribeNovelReadingRecordModel { + UserSubscribeNovelReadingRecordModel({ + this.typeName, + this.uid, + this.source, + this.bizId, + this.chapterId, + this.viewingTime, + this.record, + this.volumeId, + this.totalNum, + this.chapterName, + this.volumeName, + }); + + factory UserSubscribeNovelReadingRecordModel.fromJson( + Map json) => + UserSubscribeNovelReadingRecordModel( + typeName: asT(json['type_name']), + uid: asT(json['uid']), + source: asT(json['source']), + bizId: asT(json['biz_id']), + chapterId: asT(json['chapter_id']), + viewingTime: asT(json['viewing_time']), + record: asT(json['record']), + volumeId: asT(json['volume_id']), + totalNum: asT(json['total_num']), + chapterName: asT(json['chapter_name']), + volumeName: asT(json['volume_name']), + ); + + String? typeName; + int? uid; + int? source; + int? bizId; + int? chapterId; + int? viewingTime; + int? record; + int? volumeId; + int? totalNum; + String? chapterName; + String? volumeName; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'type_name': typeName, + 'uid': uid, + 'source': source, + 'biz_id': bizId, + 'chapter_id': chapterId, + 'viewing_time': viewingTime, + 'record': record, + 'volume_id': volumeId, + 'total_num': totalNum, + 'chapter_name': chapterName, + 'volume_name': volumeName, + }; +} diff --git a/lib/models/user/user_profile_model.dart b/lib/models/user/user_profile_model.dart new file mode 100644 index 0000000..6e9778b --- /dev/null +++ b/lib/models/user/user_profile_model.dart @@ -0,0 +1,298 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class UserProfileModel { + UserProfileModel({ + required this.nickname, + this.description, + this.birthday, + this.sex, + this.cover, + this.blood, + this.constellation, + this.bindPhone, + this.email, + this.channel, + this.channelid, + this.isVerify, + this.status, + this.reason, + this.submitLogout, + this.userDelInfo, + this.ip, + this.ipRegion, + this.isModifyName, + this.data, + this.amount, + this.isSetPwd, + this.bind, + this.userfeeinfo, + this.userLevel, + this.cookieVal, + this.isBbsAdmin, + }); + + factory UserProfileModel.fromJson(Map json) { + final List? data = json['data'] is List ? [] : null; + if (data != null) { + for (final dynamic item in json['data']!) { + if (item != null) { + data.add(asT(item)!); + } + } + } + + final List? bind = + json['bind'] is List ? [] : null; + if (bind != null) { + for (final dynamic item in json['bind']!) { + if (item != null) { + bind.add( + UserPorfileBindModel.fromJson(asT>(item)!)); + } + } + } + return UserProfileModel( + nickname: asT(json['nickname'])!, + description: asT(json['description']), + birthday: asT(json['birthday']), + sex: asT(json['sex']), + cover: asT(json['cover']), + blood: asT(json['blood']), + constellation: asT(json['constellation']), + bindPhone: asT(json['bind_phone']), + email: asT(json['email']), + channel: asT(json['channel']), + channelid: asT(json['channelid']), + isVerify: asT(json['is_verify']), + status: asT(json['status']), + reason: asT(json['reason']), + submitLogout: asT(json['submit_logout']), + userDelInfo: json['user_del_info'] == null + ? null + : UserDelInfoModel.fromJson( + asT>(json['user_del_info'])!), + ip: asT(json['ip']), + ipRegion: json['ip_region'] == null + ? null + : UserIpRegionModel.fromJson( + asT>(json['ip_region'])!), + isModifyName: asT(json['is_modify_name']), + data: data, + amount: asT(json['amount']), + isSetPwd: asT(json['is_set_pwd']), + bind: bind, + userfeeinfo: json['userFeeInfo'] == null + ? null + : UserfeeInfo.fromJson( + asT>(json['userFeeInfo'])!), + userLevel: asT(json['user_level']), + cookieVal: asT(json['cookie_val']), + isBbsAdmin: asT(json['is_bbs_admin']), + ); + } + + String nickname; + String? description; + String? birthday; + int? sex; + String? cover; + int? blood; + String? constellation; + String? bindPhone; + String? email; + String? channel; + String? channelid; + int? isVerify; + int? status; + String? reason; + bool? submitLogout; + UserDelInfoModel? userDelInfo; + String? ip; + UserIpRegionModel? ipRegion; + int? isModifyName; + List? data; + int? amount; + int? isSetPwd; + List? bind; + UserfeeInfo? userfeeinfo; + String? userLevel; + String? cookieVal; + int? isBbsAdmin; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'nickname': nickname, + 'description': description, + 'birthday': birthday, + 'sex': sex, + 'cover': cover, + 'blood': blood, + 'constellation': constellation, + 'bind_phone': bindPhone, + 'email': email, + 'channel': channel, + 'channelid': channelid, + 'is_verify': isVerify, + 'status': status, + 'reason': reason, + 'submit_logout': submitLogout, + 'user_del_info': userDelInfo, + 'ip': ip, + 'ip_region': ipRegion, + 'is_modify_name': isModifyName, + 'data': data, + 'amount': amount, + 'is_set_pwd': isSetPwd, + 'bind': bind, + 'userFeeInfo': userfeeinfo, + 'user_level': userLevel, + 'cookie_val': cookieVal, + 'is_bbs_admin': isBbsAdmin, + }; +} + +class UserDelInfoModel { + UserDelInfoModel({ + this.uid, + this.logoutId, + this.status, + this.subTime, + this.cancelTime, + this.cancelUserType, + this.currentTime, + }); + + factory UserDelInfoModel.fromJson(Map json) => + UserDelInfoModel( + uid: asT(json['uid']), + logoutId: asT(json['logout_id']), + status: asT(json['status']), + subTime: asT(json['sub_time']), + cancelTime: asT(json['cancel_time']), + cancelUserType: asT(json['cancel_user_type']), + currentTime: asT(json['current_time']), + ); + + int? uid; + int? logoutId; + int? status; + int? subTime; + int? cancelTime; + int? cancelUserType; + int? currentTime; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'uid': uid, + 'logout_id': logoutId, + 'status': status, + 'sub_time': subTime, + 'cancel_time': cancelTime, + 'cancel_user_type': cancelUserType, + 'current_time': currentTime, + }; +} + +class UserIpRegionModel { + UserIpRegionModel({ + this.country, + this.province, + this.city, + this.provider, + }); + + factory UserIpRegionModel.fromJson(Map json) => + UserIpRegionModel( + country: asT(json['country']), + province: asT(json['province']), + city: asT(json['city']), + provider: asT(json['provider']), + ); + + String? country; + String? province; + String? city; + String? provider; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'country': country, + 'province': province, + 'city': city, + 'provider': provider, + }; +} + +class UserPorfileBindModel { + UserPorfileBindModel({ + this.type, + this.name, + }); + + factory UserPorfileBindModel.fromJson(Map json) => + UserPorfileBindModel( + type: asT(json['type']), + name: asT(json['name']), + ); + + String? type; + String? name; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'type': type, + 'name': name, + }; +} + +class UserfeeInfo { + UserfeeInfo({ + this.mCate, + this.mPeriod, + }); + + factory UserfeeInfo.fromJson(Map json) => UserfeeInfo( + mCate: asT(json['m_cate']), + mPeriod: asT(json['m_period']), + ); + + int? mCate; + int? mPeriod; + + bool get isVip => (mCate ?? 0) > 0; + DateTime get expiresTime => + DateTime.fromMillisecondsSinceEpoch((mPeriod ?? 0) * 1000); + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'm_cate': mCate, + 'm_period': mPeriod, + }; +} diff --git a/lib/models/version_model.dart b/lib/models/version_model.dart new file mode 100644 index 0000000..7b0fb54 --- /dev/null +++ b/lib/models/version_model.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +T? asT(dynamic value) { + if (value is T) { + return value; + } + return null; +} + +class VersionModel { + VersionModel({ + required this.version, + required this.versionNum, + required this.versionDesc, + required this.downloadUrl, + }); + + factory VersionModel.fromJson(Map json) => VersionModel( + version: asT(json['version'])!, + versionNum: asT(json['version_num'])!, + versionDesc: asT(json['version_desc'])!, + downloadUrl: asT(json['download_url'])!, + ); + + String version; + int versionNum; + String versionDesc; + String downloadUrl; + + @override + String toString() { + return jsonEncode(this); + } + + Map toJson() => { + 'version': version, + 'version_num': versionNum, + 'version_desc': versionDesc, + 'download_url': downloadUrl, + }; +} diff --git a/lib/modules/comic/author_detail/author_detail_controller.dart b/lib/modules/comic/author_detail/author_detail_controller.dart new file mode 100644 index 0000000..5d3b6fd --- /dev/null +++ b/lib/modules/comic/author_detail/author_detail_controller.dart @@ -0,0 +1,45 @@ +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comic/author_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:get/get.dart'; + +class ComicAuthorDetailController extends BaseController { + final int id; + ComicAuthorDetailController(this.id); + + final ComicRequest request = ComicRequest(); + + Rx detail = Rx(null); + + @override + void onInit() { + loadData(); + super.onInit(); + } + + void loadData() async { + try { + pageLoadding.value = true; + pageError.value = false; + var result = await request.authorDetail(id: id); + detail.value = result; + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + } + + void subscribeAll() { + if (detail.value == null) { + return; + } + UserService.instance.addSubscribe( + detail.value!.data.map((e) => e.id).toList(), + AppConstant.kTypeComic, + ); + } +} diff --git a/lib/modules/comic/author_detail/author_detail_page.dart b/lib/modules/comic/author_detail/author_detail_page.dart new file mode 100644 index 0000000..083aea6 --- /dev/null +++ b/lib/modules/comic/author_detail/author_detail_page.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comic/author_model.dart'; +import 'package:flutter_dmzj/modules/comic/author_detail/author_detail_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class ComicAuthorDetailPage extends StatelessWidget { + final int id; + final ComicAuthorDetailController controller; + ComicAuthorDetailPage(this.id, {super.key}) + : controller = Get.put( + ComicAuthorDetailController(id), + tag: "$id", + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: Obx( + () => Row( + mainAxisSize: MainAxisSize.min, + children: [ + NetImage( + controller.detail.value?.cover ?? "", + borderRadius: 24, + width: 32, + height: 32, + ), + AppStyle.hGap8, + Text(controller.detail.value?.nickname ?? "作者"), + ], + ), + ), + actions: [ + TextButton.icon( + onPressed: controller.subscribeAll, + icon: const Icon(Remix.heart_line), + label: const Text("全部订阅"), + ), + ], + ), + body: Obx( + () => Stack( + children: [ + Offstage( + offstage: controller.detail.value == null, + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: controller.detail.value?.data.length ?? 0, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (_, i) { + var item = controller.detail.value!.data[i]; + return buildItem(item); + }, + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadData(), + ), + ), + ), + ], + ), + ), + ); + } + + Widget buildItem(ComicAuthorComicModel item) { + return InkWell( + onTap: () { + AppNavigator.toComicDetail(item.id); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text(item.status, + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + Center( + child: Obx( + () => UserService.instance.subscribedComicIds.contains(item.id) + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + UserService.instance.cancelSubscribe( + [item.id], + AppConstant.kTypeComic, + ); + }, + ) + : IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () { + UserService.instance.addSubscribe( + [item.id], + AppConstant.kTypeComic, + ); + }, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/modules/comic/category_detail/category_detail_controller.dart b/lib/modules/comic/category_detail/category_detail_controller.dart new file mode 100644 index 0000000..2c32d9b --- /dev/null +++ b/lib/modules/comic/category_detail/category_detail_controller.dart @@ -0,0 +1,85 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comic/category_comic_model.dart'; +import 'package:flutter_dmzj/models/comic/category_filter_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class CategoryDetailController + extends BasePageController { + final int id; + CategoryDetailController(this.id); + final ComicRequest request = ComicRequest(); + RxList filters = RxList(); + + @override + void onInit() { + loadFilter(); + super.onInit(); + } + + String getTitle() { + var items = filters.where((x) => x.selectId.value != 0 && x.title != "排序"); + + if (items.isEmpty) { + return "全部漫画"; + } else { + return items + .map((e) => + e.items.firstWhere((x) => x.tagId == e.selectId.value).tagName) + .join("-"); + } + } + + void loadFilter() async { + try { + filters.value = await request.categoryFilter(); + for (var item in filters) { + var tag = item.items.firstWhereOrNull((x) => x.tagId == id); + if (tag != null) { + item.selectId.value = tag.tagId; + } + } + filters.insert( + 0, + ComicCategoryFilterModel( + title: "排序", + items: [ + ComicCategoryFilterItemModel(tagId: 1, tagName: "更新排序"), + ComicCategoryFilterItemModel(tagId: 2, tagName: "热度排序"), + ], + )..selectId.value = 1, + ); + filters.insert( + 1, + ComicCategoryFilterModel( + title: "状态", + items: [ + ComicCategoryFilterItemModel(tagId: 0, tagName: "全部"), + ComicCategoryFilterItemModel(tagId: 1, tagName: "连载中"), + ComicCategoryFilterItemModel(tagId: 2, tagName: "已完结"), + ], + ), + ); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + @override + Future> getData(int page, int pageSize) async { + if (filters.isEmpty) { + return await request.categoryComic(id: id, page: page); + } else { + var sort = filters.first.selectId.value; + var status = filters[1].selectId.value; + + return await request.categoryComic( + id: filters.last.selectId.value, + sort: sort, + page: page, + status: status, + ); + } + } +} diff --git a/lib/modules/comic/category_detail/category_detail_page.dart b/lib/modules/comic/category_detail/category_detail_page.dart new file mode 100644 index 0000000..8563e86 --- /dev/null +++ b/lib/modules/comic/category_detail/category_detail_page.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/comic/category_detail/category_detail_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_grid_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class CategoryDetailPage extends StatelessWidget { + final int id; + final CategoryDetailController controller; + CategoryDetailPage(this.id, {super.key}) + : controller = Get.put( + CategoryDetailController(id), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx( + () => Text( + controller.getTitle(), + ), + ), + actions: [ + Builder( + builder: (BuildContext context) => IconButton( + icon: const Icon(Remix.filter_line), + onPressed: () { + Scaffold.of(context).openEndDrawer(); + }, + ), + ) + ], + ), + endDrawer: Drawer( + child: Obx( + () => SafeArea( + child: ListView.builder( + padding: AppStyle.edgeInsetsA12.copyWith(top: 12), + itemCount: controller.filters.length, + itemBuilder: (context, i) { + var item = controller.filters[i]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: AppStyle.edgeInsetsV12, + child: Text( + item.title, + style: Get.textTheme.titleMedium, + ), + ), + Wrap( + spacing: 8, + runSpacing: 8, + children: item.items + .map( + (x) => OutlinedButton( + style: OutlinedButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + foregroundColor: x.tagId == item.selectId.value + ? Theme.of(context).colorScheme.primary + : Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: x.tagId == item.selectId.value + ? Theme.of(context) + .colorScheme + .secondary + : Colors.transparent, + ), + ), + ), + child: Text( + x.tagName, + style: const TextStyle( + fontSize: 14, + ), + ), + onPressed: () async { + item.selectId.value = x.tagId; + + Navigator.pop(context); + controller.refreshData(); + }, + ), + ) + .toList(), + ), + ], + ); + }, + ), + ), + ), + ), + body: LayoutBuilder(builder: (context, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + return PageGridView( + pageController: controller, + firstRefresh: true, + crossAxisCount: count, + padding: AppStyle.edgeInsetsA12, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return ShadowCard( + onTap: () { + AppNavigator.toComicDetail(item.id); + }, + radius: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover ?? "", + borderRadius: 4, + ), + ), + Positioned( + right: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + color: item.status == "连载中" + ? Theme.of(context).colorScheme.primary + : Colors.orange, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + padding: + AppStyle.edgeInsetsH8.copyWith(top: 2, bottom: 2), + child: Text( + item.status ?? "", + style: const TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ), + ], + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + item.authors ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.grey, + fontSize: 12.0, + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + ], + ), + ); + }, + ); + }), + ); + } +} diff --git a/lib/modules/comic/detail/comic_detail_controller.dart b/lib/modules/comic/detail/comic_detail_controller.dart new file mode 100644 index 0000000..893f212 --- /dev/null +++ b/lib/modules/comic/detail/comic_detail_controller.dart @@ -0,0 +1,318 @@ +import 'dart:async'; + +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/models/db/comic_history.dart'; +import 'package:flutter_dmzj/modules/comic/detail/comic_detail_related_page.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class ComicDetailControler extends BaseController { + final int comicId; + ComicDetailControler(this.comicId); + + final ComicRequest request = ComicRequest(); + final UserRequest userRequest = UserRequest(); + + Rx detail = Rx(ComicDetailInfo.empty()); + + var expandDescription = false.obs; + + /// 是否已订阅 + var subscribeStatus = false.obs; + + /// 是否已收藏 + /// 收藏是收藏到本地的,订阅是同步到动漫之家服务器的 + var favorited = false.obs; + + /// 阅读记录 + Rx history = Rx(null); + + /// 更新漫画记录 + StreamSubscription? updateComicSubscription; + + @override + void onInit() { + updateComicSubscription = EventBus.instance.listen( + EventBus.kUpdatedComicHistory, + (id) { + if (id == comicId) { + getHistory(); + } + }, + ); + favorited.value = DBService.instance.hasComicFavorited(comicId: comicId); + // 从本地读取订阅状态 + subscribeStatus.value = + UserService.instance.subscribedComicIds.contains(comicId); + getHistory(); + loadDetail(); + loadSubscribeStatus(); + //updateSubscribeRead(); + super.onInit(); + } + + void refreshDetail() { + getHistory(); + loadDetail(); + loadSubscribeStatus(); + } + + /// 更新订阅的阅读状态 + void updateSubscribeRead() { + try { + userRequest.subscribeRead(id: comicId, type: AppConstant.kTypeComic); + } catch (e) { + Log.logPrint(e); + } + } + + @override + void onClose() { + updateComicSubscription?.cancel(); + super.onClose(); + } + + void getHistory() { + var comicHistory = DBService.instance.getComicHistory(comicId); + if (comicHistory != null) { + history.value = comicHistory; + history.update((val) {}); + } + } + + void refreshV1() async { + try { + var result = + await request.comicDetail(comicId: comicId, priorityV1: true); + if (result.volumes.isEmpty) { + return; + } + if (result.isHide && AppSettingsService.instance.collectHideComic.value) { + favorite(); + } + detail.update((val) { + val!.volumes = result.volumes; + }); + } catch (e) { + SmartDialog.showToast("无法获取章节"); + } + } + + /// 加载信息 + void loadDetail() async { + try { + pageLoadding.value = true; + pageError.value = false; + var result = await request.comicDetail(comicId: comicId); + detail.value = result; + if (result.volumes.isEmpty && !result.isHide) { + refreshV1(); + } + if (result.isHide && AppSettingsService.instance.collectHideComic.value) { + favorite(); + } + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + } + + /// 检查订阅状态 + void loadSubscribeStatus() async { + try { + var result = await userRequest.checkSubscribeStatus( + objId: comicId, + type: AppConstant.kTypeComic, + ); + subscribeStatus.value = result; + if (subscribeStatus.value) { + UserService.instance.subscribedComicIds.add(comicId); + } else { + UserService.instance.subscribedComicIds.remove(comicId); + } + } catch (e) { + Log.logPrint(e); + } + } + + /// 查看评论 + void comment() { + AppNavigator.toComment(objId: comicId, type: AppConstant.kTypeComic); + } + + /// 分享 + void share() { + if (detail.value.id == 0) { + return; + } + Utils.share( + "http://m.idmzj.com/info/${detail.value.comicPy}.html", + content: detail.value.title, + ); + } + + /// 订阅 + void subscribe() async { + var result = await (subscribeStatus.value + ? UserService.instance + .cancelSubscribe([comicId], AppConstant.kTypeComic) + : UserService.instance.addSubscribe([comicId], AppConstant.kTypeComic)); + if (result) { + subscribeStatus.value = !subscribeStatus.value; + } + } + + /// 下载 + void download() { + AppNavigator.toComicDownloadSelect(comicId); + } + + /// 开始/继续阅读 + void read() { + if (detail.value.volumes.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + if (detail.value.volumes.first.chapters.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + //查找记录 + if (history.value != null && history.value!.chapterId != 0) { + ComicDetailVolume? volume; + ComicDetailChapterItem? chapter; + for (var volumeItem in detail.value.volumes) { + var chapterItem = volumeItem.chapters.firstWhereOrNull( + (x) => x.chapterId == history.value!.chapterId, + ); + if (chapterItem != null) { + volume = volumeItem; + chapter = chapterItem; + break; + } + } + if (volume != null && chapter != null) { + var chapters = List.from(volume.chapters); + //正序 + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + AppNavigator.toComicReader( + comicId: comicId, + comicTitle: detail.value.title, + comicCover: detail.value.cover, + chapters: chapters, + chapter: chapter, + isLongComic: detail.value.isLong, + ); + } else { + SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读"); + readStart(); + } + } else { + readStart(); + } + } + + void readStart() { + //从头开始 + var volume = detail.value.volumes.first; + var chapters = List.from(volume.chapters); + //正序 + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + var chapter = chapters.first; + AppNavigator.toComicReader( + comicId: comicId, + comicCover: detail.value.cover, + comicTitle: detail.value.title, + chapters: chapters, + chapter: chapter, + isLongComic: detail.value.isLong, + ); + } + + void readChapter(ComicDetailVolume volume, ComicDetailChapterItem item) { + //禁止观看VIP章节 + if (item.isVip) { + SmartDialog.showToast("请使用动漫之家官方APP观看VIP章节"); + return; + } + var chapters = List.from(volume.chapters); + //正序 + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + AppNavigator.toComicReader( + comicId: comicId, + comicCover: detail.value.cover, + comicTitle: detail.value.title, + chapters: chapters, + chapter: item, + isLongComic: detail.value.isLong, + ); + } + + void related() async { + try { + SmartDialog.showLoading(); + var data = await request.related(id: comicId); + SmartDialog.dismiss(status: SmartStatus.loading); + AppNavigator.showBottomSheet( + ComicDetailRelatedPage(data), + isScrollControlled: true, + ); + } catch (e) { + SmartDialog.showToast(e.toString()); + } finally { + SmartDialog.dismiss(status: SmartStatus.loading); + } + } + + void toAuthorDetail(ComicDetailTag e) { + if (e.tagId == 0) { + //神隐漫画没有ID,直接跳转搜索 + AppNavigator.toComicSearch(keyword: e.tagName); + } else { + AppNavigator.toComicAuthorDetail(e.tagId); + } + } + + void toCategoryDetail(ComicDetailTag e) { + if (e.tagId == 0) { + //神隐漫画没有ID,直接跳转搜索 + AppNavigator.toComicSearch(keyword: e.tagName); + } else { + AppNavigator.toComicCategoryDetail(e.tagId); + } + } + + void favorite() { + if (detail.value.id == 0) { + return; + } + if (!DBService.instance.hasComicFavorited(comicId: comicId)) { + DBService.instance.putComicFavorite( + comicId: comicId, + title: detail.value.title, + cover: detail.value.cover, + ); + favorited.value = true; + SmartDialog.showToast("已将漫画添加至本地收藏"); + } + } + + void cancelFavorite() { + DBService.instance.removeComicFavorite(comicId: comicId); + favorited.value = false; + SmartDialog.showToast("已从本地收藏删除漫画"); + } +} diff --git a/lib/modules/comic/detail/comic_detail_page.dart b/lib/modules/comic/detail/comic_detail_page.dart new file mode 100644 index 0000000..5b81154 --- /dev/null +++ b/lib/modules/comic/detail/comic_detail_page.dart @@ -0,0 +1,533 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/modules/comic/detail/comic_detail_controller.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class ComicDetailPage extends StatelessWidget { + final int id; + final ComicDetailControler controller; + ComicDetailPage(this.id, {super.key}) + : controller = Get.put( + ComicDetailControler(id), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx( + () => Text( + controller.detail.value.title.isEmpty + ? "漫画详情" + : controller.detail.value.title, + ), + ), + actions: [ + Obx( + () => IconButton( + onPressed: controller.favorited.value + ? controller.cancelFavorite + : controller.favorite, + icon: Icon(controller.favorited.value + ? Remix.star_fill + : Remix.star_line), + ), + ), + IconButton( + onPressed: controller.share, + icon: const Icon(Icons.share), + ), + ], + ), + body: Stack( + children: [ + Obx( + () => Offstage( + offstage: controller.detail.value.id == 0, + child: EasyRefresh( + header: const MaterialHeader(), + onRefresh: controller.refreshDetail, + child: ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + _buildHeader(), + Obx( + () => Offstage( + offstage: controller.history.value == null, + child: Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + "上次看到:${controller.history.value?.chapterName ?? ""} 第${controller.history.value?.page}页", + style: Get.textTheme.titleSmall, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () { + controller.read(); + }, + ), + Divider( + color: Colors.grey.withOpacity(.2), + height: 1.0, + ), + ], + ), + ), + ), + _buildChapter(), + ], + ), + ), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadDetail(), + ), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + elevation: 2, + onPressed: controller.read, + child: const Icon(Icons.play_circle_outline_rounded), + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Obx( + () => TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.subscribe, + icon: Icon( + controller.subscribeStatus.value + ? Remix.heart_fill + : Remix.heart_line, + size: 20, + ), + label: Text(controller.subscribeStatus.value ? "取消" : "订阅"), + ), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.comment, + icon: const Icon( + Remix.chat_2_line, + size: 20, + ), + label: const Text("评论"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.download, + icon: const Icon( + Remix.download_line, + size: 20, + ), + label: const Text("下载"), + ), + ), + // Expanded( + // child: TextButton.icon( + // style: TextButton.styleFrom( + // textStyle: const TextStyle(fontSize: 14), + // ), + // onPressed: controller.related, + // icon: const Icon( + // Remix.links_line, + // size: 20, + // ), + // label: const Text("相关"), + // ), + // ), + ], + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //信息 + Stack( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + NetImage( + controller.detail.value.cover, + width: 120, + height: 160, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + controller.detail.value.title, + style: Get.textTheme.titleMedium, + ), + AppStyle.vGap8, + _buildInfoItems( + iconData: Remix.user_smile_line, + children: controller.detail.value.authors + .map( + (e) => GestureDetector( + onTap: () => controller.toAuthorDetail(e), + child: Text( + e.tagName, + style: TextStyle( + fontSize: 14, + height: 1.2, + decoration: TextDecoration.underline, + color: Get.isDarkMode + ? Colors.white + : AppColor.black333, + ), + ), + ), + ) + .toList(), + ), + + // _buildInfo( + // title: controller.detail.value.types + // .map((e) => e.tagName) + // .join("/"), + // iconData: Remix.hashtag, + // ), + _buildInfoItems( + iconData: Remix.hashtag, + children: controller.detail.value.types + .map( + (e) => GestureDetector( + onTap: () => controller.toCategoryDetail(e), + child: Text( + e.tagName, + style: TextStyle( + fontSize: 14, + height: 1.2, + decoration: TextDecoration.underline, + color: Get.isDarkMode + ? Colors.white + : AppColor.black333, + ), + ), + ), + ) + .toList(), + ), + // _buildInfo( + // title: "人气 ${controller.detail.value.hitNum}", + // iconData: Remix.fire_line, + // ), + // _buildInfo( + // title: "订阅 ${controller.detail.value.subscribeNum}", + // iconData: Remix.heart_line, + // ), + _buildInfo( + title: + "${Utils.formatTimestampToDate(controller.detail.value.lastUpdatetime)} ${controller.detail.value.status.map((e) => e.tagName).join("/")}", + iconData: Icons.schedule, + ), + ], + ), + ), + ], + ), + Obx( + () => Positioned( + right: 0, + top: 0, + child: Offstage( + offstage: !controller.detail.value.isVip, + child: Image.asset( + "assets/images/vip_comic.png", + width: 36, + height: 36, + ), + ), + ), + ), + ], + ), + AppStyle.vGap12, + GestureDetector( + onTap: () { + controller.expandDescription.value = + !controller.expandDescription.value; + }, + child: Text( + controller.detail.value.description, + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + ), + maxLines: controller.expandDescription.value ? 999 : 2, + overflow: TextOverflow.ellipsis, + ), + ), + AppStyle.vGap12, + Divider( + color: Colors.grey.withOpacity(.2), + height: 1.0, + ), + ], + ); + } + + Widget _buildChapter() { + return Column( + children: controller.detail.value.volumes.isEmpty + ? [ + const Padding( + padding: AppStyle.edgeInsetsA24, + child: Text( + "(~ ̄▽ ̄)~\n没有可阅读的章节\n漫画可能已下架或您没有阅读的权限", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ) + ] + : controller.detail.value.volumes + .map( + (item) => Obx( + () => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: AppStyle.edgeInsetsV8, + child: Row( + children: [ + Expanded( + child: Text( + "${item.title}(共${item.chapters.length}话)", + style: Get.textTheme.titleSmall, + ), + ), + item.sortType.value == 1 + ? TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + item.sortType.value = 0; + item.sort(); + }, + icon: const Icon( + Remix.sort_asc, + size: 20, + ), + label: const Text("升序"), + ) + : TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + item.sortType.value = 1; + item.sort(); + }, + icon: const Icon( + Remix.sort_desc, + size: 20, + ), + label: const Text("倒序"), + ), + ], + ), + ), + LayoutBuilder(builder: (ctx, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + + return Obx( + () => MasonryGridView.count( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemCount: + (item.showMoreButton && !item.showAll.value) + ? 15 + : item.chapters.length, + itemBuilder: (_, i) { + if (item.showMoreButton && + !item.showAll.value && + i == 14) { + return Tooltip( + message: "展开全部章节", + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.grey, + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size.fromHeight(40), + ), + onPressed: () { + item.showAll.value = true; + }, + child: const Icon(Icons.arrow_drop_down), + ), + ); + } + return Tooltip( + message: item.chapters[i].chapterTitle, + child: Obx( + () => Stack( + children: [ + OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: item + .chapters[i].chapterId == + controller + .history.value?.chapterId + ? Theme.of(ctx).colorScheme.primary + : Get.textTheme.bodyMedium!.color, + textStyle: + const TextStyle(fontSize: 14), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + minimumSize: + const Size.fromHeight(40), + ), + onPressed: () { + controller.readChapter( + item, item.chapters[i]); + }, + child: Text( + item.chapters[i].chapterTitle, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + Positioned( + left: -2, + top: 0, + child: Offstage( + offstage: !item.chapters[i].isVip, + child: Image.asset( + "assets/images/vip_chapter.png", + height: 16, + ), + ), + ), + ], + ), + ), + ); + }, + crossAxisCount: count, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + ); + }) + ], + ), + ), + ) + .toList(), + ); + } + + Widget _buildInfo({ + required String title, + IconData iconData = Icons.tag, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + iconData, + color: Colors.grey, + size: 16, + ), + AppStyle.hGap8, + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14, + color: Get.isDarkMode ? Colors.white : AppColor.black333, + ), + ), + ), + ], + ), + ); + } + + Widget _buildInfoItems({ + required List children, + IconData iconData = Icons.tag, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + iconData, + color: Colors.grey, + size: 16, + ), + AppStyle.hGap8, + Expanded( + child: Wrap( + spacing: 8, + children: children, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/comic/detail/comic_detail_related_page.dart b/lib/modules/comic/detail/comic_detail_related_page.dart new file mode 100644 index 0000000..6f74b87 --- /dev/null +++ b/lib/modules/comic/detail/comic_detail_related_page.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comic/comic_related_model.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:get/get.dart'; + +class ComicDetailRelatedPage extends StatelessWidget { + final ComicRelatedModel related; + const ComicDetailRelatedPage(this.related, {super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.7, + child: Column( + children: [ + ListTile( + title: const Text("作品相关"), + trailing: IconButton( + onPressed: () { + AppNavigator.closePage(); + }, + icon: const Icon(Icons.close), + ), + contentPadding: AppStyle.edgeInsetsL12, + ), + const Divider( + height: 1, + ), + Expanded( + child: ListView( + padding: AppStyle.edgeInsetsA12.copyWith(top: 0), + children: [ + ...related.authorComics + .map( + (e) => + buildCard("${e.authorName}的其他作品", e.data, onTap: () { + AppNavigator.toComicAuthorDetail(e.authorId); + }), + ) + .toList(), + buildCard("同类题材作品", related.themeComics), + buildCard("轻小说", related.novels, isComic: false), + ], + ), + ), + ], + ), + ); + } + + Widget buildCard(String title, List list, + {Function()? onTap, bool isComic = true}) { + return Visibility( + visible: list.isNotEmpty, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: AppStyle.edgeInsetsV8, + child: Row( + children: [ + Expanded( + child: Text( + title, + style: Get.textTheme.titleSmall, + ), + ), + Visibility( + visible: onTap != null, + child: IconButton( + onPressed: onTap, + icon: const Icon(Icons.chevron_right), + ), + ), + ], + )), + LayoutBuilder(builder: (ctx, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + + return MasonryGridView.count( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemCount: list.length, + crossAxisCount: count, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + itemBuilder: (_, i) { + var item = list[i]; + return ShadowCard( + onTap: () { + if (isComic) { + AppNavigator.toComicDetail(item.id); + } else { + AppNavigator.toNovelDetail(item.id); + } + }, + radius: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover, + borderRadius: 4, + ), + ), + Positioned( + right: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + color: item.status == "连载中" + ? Get.theme.colorScheme.primary + : Colors.orange, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + padding: AppStyle.edgeInsetsH8 + .copyWith(top: 2, bottom: 2), + child: Text( + item.status, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ), + ], + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsA4, + child: Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + height: 1.2, + ), + ), + ), + ], + ), + ); + }, + ); + }) + ], + ), + ); + } +} diff --git a/lib/modules/comic/home/category/comic_category_controller.dart b/lib/modules/comic/home/category/comic_category_controller.dart new file mode 100644 index 0000000..8026189 --- /dev/null +++ b/lib/modules/comic/home/category/comic_category_controller.dart @@ -0,0 +1,23 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comic/category_item_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +class ComicCategoryController + extends BasePageController { + final ComicRequest request = ComicRequest(); + + @override + Future> getData(int page, int pageSize) async { + if (page > 1) { + return []; + } + var ls = await request.categores(); + + return ls; + } + + void toDetail(ComicCategoryItemModel item) { + AppNavigator.toComicCategoryDetail(item.tagId); + } +} diff --git a/lib/modules/comic/home/category/comic_category_view.dart b/lib/modules/comic/home/category/comic_category_view.dart new file mode 100644 index 0000000..0b336a6 --- /dev/null +++ b/lib/modules/comic/home/category/comic_category_view.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/comic/home/category/comic_category_controller.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_grid_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; + +class ComicCategoryView extends StatelessWidget { + final ComicCategoryController controller; + ComicCategoryView({Key? key}) + : controller = Get.put(ComicCategoryController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + return KeepAliveWrapper( + child: PageGridView( + pageController: controller, + firstRefresh: true, + loadMore: false, + crossAxisCount: count, + padding: AppStyle.edgeInsetsH12.copyWith(bottom: 12), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return ShadowCard( + onTap: () { + controller.toDetail(item); + }, + child: Column( + children: [ + AspectRatio( + aspectRatio: 1.0, + child: NetImage( + item.cover, + borderRadius: 8, + ), + ), + Padding( + padding: AppStyle.edgeInsetsA8, + child: Text( + item.title, + textAlign: TextAlign.center, + style: const TextStyle(height: 1), + ), + ), + ], + ), + ); + }, + ), + ); + }); + } +} diff --git a/lib/modules/comic/home/comic_home_controller.dart b/lib/modules/comic/home/comic_home_controller.dart new file mode 100644 index 0000000..e50e54d --- /dev/null +++ b/lib/modules/comic/home/comic_home_controller.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/modules/comic/home/category/comic_category_controller.dart'; +//import 'package:flutter_dmzj/modules/comic/home/category/comic_category_controller.dart'; +import 'package:flutter_dmzj/modules/comic/home/latest/comic_latest_controller.dart'; +import 'package:flutter_dmzj/modules/comic/home/rank/comic_rank_controller.dart'; +import 'package:flutter_dmzj/modules/comic/home/recommend/comic_recommend_controller.dart'; +//import 'package:flutter_dmzj/modules/comic/home/special/comic_special_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:get/get.dart'; + +class ComicHomeController extends GetxController + with GetTickerProviderStateMixin { + late TabController tabController; + + StreamSubscription? streamSubscription; + + @override + void onInit() { + streamSubscription = EventBus.instance.listen( + EventBus.kBottomNavigationBarClicked, + (index) { + if (index == 0) { + refreshOrScrollTop(); + } + }, + ); + tabController = TabController(length: 4, vsync: this); + + super.onInit(); + } + + void refreshOrScrollTop() { + var tabIndex = tabController.index; + BasePageController? controller; + if (tabIndex == 0) { + controller = Get.find(); + } else if (tabIndex == 1) { + controller = Get.find(); + } else if (tabIndex == 2) { + controller = Get.find(); + } else if (tabIndex == 3) { + controller = Get.find(); + } + // else if (tabIndex == 4) { + // controller = Get.find(); + // } + controller?.scrollToTopOrRefresh(); + } + + void search() { + AppNavigator.toComicSearch(); + } +} diff --git a/lib/modules/comic/home/comic_home_page.dart b/lib/modules/comic/home/comic_home_page.dart new file mode 100644 index 0000000..31bde39 --- /dev/null +++ b/lib/modules/comic/home/comic_home_page.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/modules/comic/home/category/comic_category_view.dart'; +import 'package:flutter_dmzj/modules/comic/home/comic_home_controller.dart'; +import 'package:flutter_dmzj/modules/comic/home/latest/comic_latest_view.dart'; +import 'package:flutter_dmzj/modules/comic/home/rank/comic_rank_view.dart'; +import 'package:flutter_dmzj/modules/comic/home/recommend/comic_recommend_view.dart'; +//import 'package:flutter_dmzj/modules/comic/home/special/comic_special_view.dart'; +import 'package:flutter_dmzj/widgets/tab_appbar.dart'; +import 'package:flutter_dmzj/widgets/windows_tab_page.dart'; +import 'package:get/get.dart'; + +class ComicHomePage extends GetView { + const ComicHomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (PlatformUtils.isWindows) { + return WindowsTabPage( + tabs: [ + WindowsTabItem(label: '推荐', body: ComicRecommendView()), + WindowsTabItem(label: '更新', body: ComicLatestView()), + WindowsTabItem(label: '分类', body: ComicCategoryView()), + WindowsTabItem(label: '排行', body: ComicRankView()), + ], + headerAction: IconButton( + onPressed: controller.search, + icon: const Icon(Icons.search), + ), + ); + } + return Scaffold( + appBar: TabAppBar( + tabs: const [ + Tab(text: "推荐"), + Tab(text: "更新"), + Tab(text: "分类"), + Tab(text: "排行"), + // Tab(text: "专题"), + ], + controller: controller.tabController, + action: IconButton( + onPressed: controller.search, + icon: const Icon( + Icons.search, + ), + ), + ), + body: TabBarView( + controller: controller.tabController, + children: [ + ComicRecommendView(), + ComicLatestView(), + ComicCategoryView(), + ComicRankView(), + //ComicSpecialView(), + ], + ), + ); + } +} + diff --git a/lib/modules/comic/home/latest/comic_latest_controller.dart b/lib/modules/comic/home/latest/comic_latest_controller.dart new file mode 100644 index 0000000..bb50a54 --- /dev/null +++ b/lib/modules/comic/home/latest/comic_latest_controller.dart @@ -0,0 +1,21 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comic/update_item_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:get/get.dart'; + +class ComicLatestController extends BasePageController { + final ComicRequest request = ComicRequest(); + Map types = { + "全部漫画": 100, + "原创漫画": 1, + "译制漫画": 0, + }; + var type = 100.obs; + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.latest(type: type.value, page: page); + + return ls; + } +} diff --git a/lib/modules/comic/home/latest/comic_latest_view.dart b/lib/modules/comic/home/latest/comic_latest_view.dart new file mode 100644 index 0000000..2fd4b0e --- /dev/null +++ b/lib/modules/comic/home/latest/comic_latest_view.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/comic/update_item_model.dart'; +import 'package:flutter_dmzj/modules/comic/home/latest/comic_latest_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class ComicLatestView extends StatelessWidget { + final ComicLatestController controller; + ComicLatestView({Key? key}) + : controller = Get.put(ComicLatestController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: Column( + children: [ + Row( + children: [ + AppStyle.hGap12, + ...controller.types.keys.map( + (e) => buildFilterButton( + title: e, + value: controller.types[e], + ), + ), + ], + ), + AppStyle.vGap12, + Expanded( + child: PageListView( + pageController: controller, + firstRefresh: true, + showPageLoadding: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ), + ], + ), + ); + } + + Widget buildItem(ComicUpdateItemModel item) { + return InkWell( + onTap: () { + AppNavigator.toComicDetail(item.comicId.toInt()); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover ?? '', + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text.rich( + TextSpan(children: [ + const WidgetSpan( + child: Icon( + Icons.account_circle, + color: Colors.grey, + size: 18, + )), + const TextSpan( + text: " ", + ), + TextSpan( + text: item.authors, + style: + const TextStyle(color: Colors.grey, fontSize: 14)) + ]), + ), + const SizedBox(height: 2), + Text(item.types ?? '', + style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 2), + Text(item.lastUpdateChapterName ?? '', + style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 2), + Text("更新于${Utils.formatTimestamp(item.lastUpdatetime ?? 0)}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + Center( + child: Obx( + () => UserService.instance.subscribedComicIds + .contains(item.comicId.toInt()) + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + UserService.instance.cancelSubscribe( + [item.comicId.toInt()], + AppConstant.kTypeComic, + ); + }, + ) + : IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () { + UserService.instance.addSubscribe( + [item.comicId.toInt()], + AppConstant.kTypeComic, + ); + }, + ), + ), + ) + ], + ), + ), + ); + } + + Widget buildFilterButton({required String title, required int value}) { + return Container( + height: 32, + margin: AppStyle.edgeInsetsR8, + child: Obx( + () => TextButton( + style: TextButton.styleFrom( + foregroundColor: + controller.type.value == value ? Get.theme.colorScheme.primary : Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: AppStyle.radius24, + side: BorderSide( + color: + controller.type.value == value ? Get.theme.colorScheme.primary : Colors.grey, + ), + ), + textStyle: const TextStyle(fontSize: 14), + padding: AppStyle.edgeInsetsH16, + ), + onPressed: () { + if (controller.type.value == value) { + return; + } + controller.type.value = value; + controller.refreshData(); + }, + child: Text(title), + ), + ), + ); + } +} diff --git a/lib/modules/comic/home/rank/comic_rank_controller.dart b/lib/modules/comic/home/rank/comic_rank_controller.dart new file mode 100644 index 0000000..b26ba7f --- /dev/null +++ b/lib/modules/comic/home/rank/comic_rank_controller.dart @@ -0,0 +1,54 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comic/rank_item_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class ComicRankController extends BasePageController { + final ComicRequest request = ComicRequest(); + RxMap tags = { + 0: "全部分类", + }.obs; + var tag = 0.obs; + + Map byTimes = { + 0: "日排行", + 1: "周排行", + 2: "月排行", + 3: "总排行", + }; + var byTime = 3.obs; + + Map rankTypes = { + 0: "人气排行", + 1: "吐槽排行", + 2: "订阅排行", + }; + var rankType = 0.obs; + + @override + void onInit() { + loadFilter(); + super.onInit(); + } + + void loadFilter() async { + try { + tags.value = await request.rankFilter(); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.rank( + tagId: tag.value, + byTime: byTime.value, + rankType: rankType.value, + page: page, + ); + + return ls; + } +} diff --git a/lib/modules/comic/home/rank/comic_rank_view.dart b/lib/modules/comic/home/rank/comic_rank_view.dart new file mode 100644 index 0000000..7c881b9 --- /dev/null +++ b/lib/modules/comic/home/rank/comic_rank_view.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/comic/rank_item_model.dart'; +import 'package:flutter_dmzj/modules/comic/home/rank/comic_rank_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class ComicRankView extends StatelessWidget { + final ComicRankController controller; + ComicRankView({Key? key}) + : controller = Get.put(ComicRankController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: Column( + children: [ + Obx( + () => Row( + children: [ + buildFilter( + // ignore: invalid_use_of_protected_member + types: controller.tags.value, + value: controller.tag.value, + onSelected: (e) { + controller.tag.value = e; + controller.refreshData(); + }, + ), + buildFilter( + types: controller.byTimes, + value: controller.byTime.value, + onSelected: (e) { + controller.byTime.value = e; + controller.refreshData(); + }, + ), + buildFilter( + types: controller.rankTypes, + value: controller.rankType.value, + onSelected: (e) { + controller.rankType.value = e; + controller.refreshData(); + }, + ), + ], + ), + ), + AppStyle.vGap12, + Expanded( + child: PageListView( + pageController: controller, + firstRefresh: true, + showPageLoadding: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ), + ], + ), + ); + } + + Widget buildFilter({ + required Map types, + required int value, + required Function(int) onSelected, + }) { + return Expanded( + child: PopupMenuButton( + onSelected: onSelected, + itemBuilder: (c) => types.keys + .map( + (k) => CheckedPopupMenuItem( + value: k, + checked: k == value, + child: Text(types[k] ?? ""), + ), + ) + .toList(), + child: SizedBox( + height: 36, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + types[value] ?? "", + ), + const Icon( + Icons.arrow_drop_down, + color: Colors.grey, + ) + ], + ), + ), + ), + ); + } + + Widget buildItem(ComicRankListItemModel item) { + return InkWell( + onTap: () { + AppNavigator.toComicDetail(item.comicId.toInt()); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover ?? '', + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text.rich( + TextSpan(children: [ + const WidgetSpan( + child: Icon( + Icons.account_circle, + color: Colors.grey, + size: 18, + )), + const TextSpan( + text: " ", + ), + TextSpan( + text: item.authors, + style: + const TextStyle(color: Colors.grey, fontSize: 14)) + ]), + ), + const SizedBox(height: 2), + Text(item.types ?? '-', + style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 2), + Text(item.lastUpdateChapterName ?? '-', + style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 2), + Text("更新于${Utils.formatTimestamp(item.lastUpdatetime ?? 0)}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + Center( + child: Obx( + () => UserService.instance.subscribedComicIds + .contains(item.comicId.toInt()) + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + UserService.instance.cancelSubscribe( + [item.comicId.toInt()], + AppConstant.kTypeComic, + ); + }, + ) + : IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () { + UserService.instance.addSubscribe( + [item.comicId.toInt()], + AppConstant.kTypeComic, + ); + }, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/modules/comic/home/recommend/comic_recommend_controller.dart b/lib/modules/comic/home/recommend/comic_recommend_controller.dart new file mode 100644 index 0000000..3d7a955 --- /dev/null +++ b/lib/modules/comic/home/recommend/comic_recommend_controller.dart @@ -0,0 +1,188 @@ +import 'dart:async'; + +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/comic/recommend_model.dart'; +import 'package:flutter_dmzj/modules/comic/home/comic_home_controller.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class ComicRecommendController extends BasePageController { + final ComicRequest request = ComicRequest(); + StreamSubscription? subLogin; + StreamSubscription? subLogout; + + @override + void onInit() { + subLogin = UserService.loginedStream.listen((event) { + loadSubscribe(); + }); + subLogout = UserService.logoutStream.listen((event) { + list.removeWhere((x) => x.categoryId == 49); + }); + super.onInit(); + } + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.recommend(); + + // ls.insert( + // ls.length > 3 ? 2 : 1, + // ComicRecommendModel( + // categoryId: 50, + // title: "随便看看", + // sort: 6, + // data: [], + // ), + // ); + //loadRandom(); + if (UserService.instance.logined.value) { + loadSubscribe(); + } + return ls; + } + + // /// 加载随机漫画 + // Future loadRandom() async { + // try { + // var result = await request.refreshRecommend(50); + // var index = list.indexWhere((x) => x.categoryId == 50); + // if (index != -1) { + // list[index].data = result; + // } else { + // list.insert( + // list.length > 3 ? 2 : 1, + // result, + // ); + // } + // } catch (e) { + // Log.logPrint(e); + // } + // } + + /// 刷新国漫 + Future refreshGuoman() async { + try { + var index = list.indexWhere((x) => x.categoryId == 111); + var result = + await request.refreshRecommend(111, size: 6, page: list[index].page); + + if (index != -1) { + list[index].data = result; + list[index].page++; + list.refresh(); + } + } catch (e) { + Log.logPrint(e); + } + } + + /// 刷新近期必看 + Future refreshRecommend() async { + try { + var index = list.indexWhere((x) => x.categoryId == 110); + + var result = await request.refreshRecommend(110, page: list[index].page); + + if (index != -1) { + list[index].data = result; + list[index].page++; + list.refresh(); + } + } catch (e) { + Log.logPrint(e); + } + } + + /// 加载订阅 + void loadSubscribe() async { + try { + var result = await request.recommendSubscribe(); + var index = list.indexWhere((x) => x.categoryId == 49); + if (index != -1) { + list[index] = result; + } else { + list.insert(1, result); + } + } catch (e) { + Log.logPrint(e); + } + } + + /// 刷新热门连载 + Future refreshHot() async { + try { + var index = list.indexWhere((x) => x.categoryId == 112); + var result = + await request.refreshRecommend(112, page: list[index].page, size: 6); + + if (index != -1) { + list[index].data = result; + list[index].page++; + list.refresh(); + } + } catch (e) { + Log.logPrint(e); + } + } + + void openDetail(ComicRecommendItemModel item) { + //漫画=1 + if (item.type == null || item.type == 1) { + AppNavigator.toComicDetail( + item.objId ?? item.id ?? 0, + ); + } else if (item.type == 5) { + //专题=5 + AppNavigator.toSpecialDetail( + item.objId ?? 0, + ); + } else if (item.type == 6) { + //网页=6 + AppNavigator.toWebView(item.url ?? ""); + } else if (item.type == 7) { + //新闻=7 + AppNavigator.toNewsDetail( + url: item.url ?? "", + newsId: item.objId ?? 0, + title: item.title, + ); + } else if (item.type == 8) { + //作者=8 + AppNavigator.toComicAuthorDetail(item.objId ?? 0); + } else if (item.type == 13) { + //社区=13 + //直接跳转至网页 + launchUrlString( + "http://m.forum.idmzj.com/thread/detail?tid=${item.objId}", + mode: LaunchMode.externalApplication, + ); + // AppNavigator.toWebView( + // "http://m.forum.dmzj.com/thread/detail?tid=${item.objId}", + // ); + } else { + SmartDialog.showToast("未知类型,无法跳转"); + } + } + + void toSpecial() { + var homeController = Get.find(); + homeController.tabController.animateTo(4); + } + + void toMySubscribe() { + AppNavigator.toUserSubscribe(); + } + + @override + void onClose() { + subLogin?.cancel(); + subLogout?.cancel(); + super.onClose(); + } +} diff --git a/lib/modules/comic/home/recommend/comic_recommend_view.dart b/lib/modules/comic/home/recommend/comic_recommend_view.dart new file mode 100644 index 0000000..ee78998 --- /dev/null +++ b/lib/modules/comic/home/recommend/comic_recommend_view.dart @@ -0,0 +1,372 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comic/recommend_model.dart'; +import 'package:flutter_dmzj/modules/comic/home/recommend/comic_recommend_controller.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:flutter_dmzj/widgets/refresh_until_widget.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_swiper_view/flutter_swiper_view.dart'; +import 'package:get/get.dart'; + +class ComicRecommendView extends StatelessWidget { + final ComicRecommendController controller; + ComicRecommendView({Key? key}) + : controller = Get.put(ComicRecommendController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + padding: AppStyle.edgeInsetsH12, + firstRefresh: true, + loadMore: false, + showPageLoadding: true, + itemBuilder: (context, i) { + var item = controller.list[i]; + //大图推荐 + if (item.categoryId == 109) { + return buildBanner(item); + } + //随便看看 + // if (item.categoryId == 50) { + // return buildCard( + // context, + // child: buildTreeColumnGridView(item.data), + // title: item.title.toString(), + // action: buildRefresh(onRefresh: controller.loadRandom), + // ); + // } + //我的订阅 + if (item.categoryId == 49) { + return buildCard( + context, + child: buildTreeColumnGridView(item.data), + title: item.title.toString(), + action: buildShowMore(onTap: controller.toMySubscribe), + ); + } + //近期必看\国漫\热门连载\最新上架 + if (item.categoryId == 110 || + item.categoryId == 111 || + item.categoryId == 112 || + item.categoryId == 56) { + Widget? action; + //刷新国漫 + if (item.categoryId == 110) { + action = buildRefresh(onRefresh: controller.refreshRecommend); + } + if (item.categoryId == 111) { + action = buildRefresh(onRefresh: controller.refreshGuoman); + } + if (item.categoryId == 112) { + action = buildRefresh(onRefresh: controller.refreshHot); + } + return buildCard( + context, + child: buildTreeColumnGridView(item.data), + title: item.title.toString(), + action: action, + ); + } + //火热专题\美漫大事件\条漫 + if (item.categoryId == 48 || + item.categoryId == 53 || + item.categoryId == 55) { + return buildCard( + context, + child: buildTwoColumnGridView(item.data), + title: item.title.toString(), + action: item.categoryId == 48 + ? buildShowMore(onTap: controller.toSpecial) + : null, + ); + } + //大师 + if (item.categoryId == 51) { + return buildCard( + context, + child: buildAuthorGridView(item.data), + title: item.title.toString(), + ); + } + return buildCard( + context, + child: buildTreeColumnGridView(item.data), + title: item.title.toString(), + ); + }, + ), + ); + } + + Widget buildCard( + BuildContext context, { + required Widget child, + required String title, + Widget? action, + }) { + return Padding( + padding: AppStyle.edgeInsetsB8, + child: Container( + decoration: BoxDecoration( + borderRadius: AppStyle.radius8, + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 16, height: 1.0, fontWeight: FontWeight.bold), + ), + ), + SizedBox( + height: 48, + child: action, + ), + ], + ), + child, + ], + ), + ), + ); + } + + Widget buildShowMore({required Function() onTap}) { + return GestureDetector( + onTap: onTap, + child: const Row( + children: [ + Text( + "查看更多", + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + Icon(Icons.chevron_right, size: 18, color: Colors.grey), + ], + ), + ); + } + + Widget buildRefresh({required Future Function() onRefresh}) { + return RefreshUntilWidget(onRefresh: onRefresh, text: "换一批"); + } + + Widget buildBanner(ComicRecommendModel item) { + return Padding( + padding: AppStyle.edgeInsetsB12, + child: ClipRRect( + borderRadius: AppStyle.radius4, + child: AspectRatio( + aspectRatio: 75 / 40, + child: Swiper( + itemWidth: 750, + itemHeight: 400, + autoplay: true, + itemCount: item.data.length, + itemBuilder: (_, i) => NetImage( + item.data[i].cover, + width: 750, + height: 400, + ), + onTap: (i) { + controller.openDetail(item.data[i]); + }, + pagination: SwiperCustomPagination( + builder: (BuildContext context, SwiperPluginConfig config) { + return Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.only( + left: 8, + right: 12, + top: 4, + bottom: 4, + ), + //color: Colors.black12, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black38, + Colors.transparent, + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + item.data[config.activeIndex].title, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, color: Colors.white), + ), + ), + AppStyle.hGap8, + PageIndicator( + controller: config.pageController!, + count: config.itemCount, + size: 10, + layout: PageIndicatorLayout.SCALE, + ), + ], + ), + ), + ); + }, + ), + ), + ), + ), + ); + } + + Widget buildTreeColumnGridView(List items) { + return MasonryGridView.count( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemCount: items.length, + itemBuilder: (_, i) { + var item = items[i]; + return InkWell( + onTap: () => controller.openDetail(item), + borderRadius: AppStyle.radius4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: AppStyle.radius4, + child: AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover, + width: 270, + height: 360, + ), + ), + ), + AppStyle.vGap8, + Text( + item.title, + maxLines: 1, + style: const TextStyle(height: 1.2), + overflow: TextOverflow.ellipsis, + ), + Text( + item.subTitle ?? item.status ?? '', + maxLines: 1, + style: const TextStyle( + height: 1.2, + fontSize: 12, + color: Colors.grey, + overflow: TextOverflow.ellipsis, + ), + ), + AppStyle.vGap8, + ], + ), + ); + }, + ); + } + + Widget buildAuthorGridView(List items) { + return MasonryGridView.count( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + itemCount: items.length, + itemBuilder: (_, i) { + var item = items[i]; + return InkWell( + onTap: () => controller.openDetail(item), + borderRadius: AppStyle.radius8, + child: Padding( + padding: AppStyle.edgeInsetsA12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + NetImage( + item.cover, + width: 56, + height: 56, + borderRadius: 32, + ), + Padding( + padding: AppStyle.edgeInsetsV8, + child: Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(height: 1.2, fontSize: 12), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget buildTwoColumnGridView(List items) { + return MasonryGridView.count( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemCount: items.length, + itemBuilder: (_, i) { + var item = items[i]; + return InkWell( + onTap: () => controller.openDetail(item), + borderRadius: AppStyle.radius4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: AppStyle.radius4, + child: AspectRatio( + aspectRatio: 32 / 17, + child: NetImage( + item.cover, + width: 320, + height: 170, + ), + ), + ), + Padding( + padding: AppStyle.edgeInsetsV8, + child: Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(height: 1.2), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/modules/comic/home/special/comic_special_controller.dart b/lib/modules/comic/home/special/comic_special_controller.dart new file mode 100644 index 0000000..ed54915 --- /dev/null +++ b/lib/modules/comic/home/special/comic_special_controller.dart @@ -0,0 +1,23 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comic/special_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +class ComicSpecialController extends BasePageController { + final ComicRequest request = ComicRequest(); + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.special(page: page - 1); + + return ls; + } + + void toDetail(ComicSpecialModel item) { + if (item.pageType == 3) { + AppNavigator.toSpecialDetail(item.id); + } else { + AppNavigator.toWebView(item.pageUrl); + } + } +} diff --git a/lib/modules/comic/home/special/comic_special_view.dart b/lib/modules/comic/home/special/comic_special_view.dart new file mode 100644 index 0000000..2b5dc72 --- /dev/null +++ b/lib/modules/comic/home/special/comic_special_view.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/modules/comic/home/special/comic_special_controller.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; + +class ComicSpecialView extends StatelessWidget { + final ComicSpecialController controller; + ComicSpecialView({Key? key}) + : controller = Get.put(ComicSpecialController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + showPageLoadding: false, + padding: AppStyle.edgeInsetsA12.copyWith(top: 0), + separatorBuilder: (context, i) => AppStyle.vGap12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return ShadowCard( + onTap: () { + controller.toDetail(item); + }, + radius: 8, + child: Column( + children: [ + AspectRatio( + aspectRatio: 710 / 284, + child: NetImage( + item.smallCover, + width: 710, + height: 354, + ), + ), + Padding( + padding: AppStyle.edgeInsetsA8, + child: Row( + children: [ + Expanded(child: Text(item.title)), + Text( + Utils.formatTimestampToDate(item.createTime), + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/modules/comic/reader/comic_reader_controller.dart b/lib/modules/comic/reader/comic_reader_controller.dart new file mode 100644 index 0000000..2a1c839 --- /dev/null +++ b/lib/modules/comic/reader/comic_reader_controller.dart @@ -0,0 +1,843 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:battery_plus/battery_plus.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_error.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/comic/chapter_info.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/models/comic/view_point_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:preload_page_view/preload_page_view.dart'; +import 'package:remixicon/remixicon.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +class ComicReaderController extends BaseController { + /// 是否为条漫 + final bool isLongComic; + final int comicId; + final String comicTitle; + final String comicCover; + final ComicDetailChapterItem chapter; + final List chapters; + final FocusNode focusNode = FocusNode(); + final ComicRequest request = ComicRequest(); + ComicReaderController({ + required this.comicId, + required this.comicTitle, + required this.chapters, + required this.chapter, + required this.comicCover, + required this.isLongComic, + }) { + chapterIndex.value = chapters.indexOf(chapter); + } + + /// APP设置控制器 + final settings = AppSettingsService.instance; + + /// 预加载控制器 + final PreloadPageController preloadPageController = PreloadPageController(); + + /// 上下模式控制器 + final ItemScrollController itemScrollController = ItemScrollController(); + + /// 监听上下滚动 + final ItemPositionsListener itemPositionsListener = + ItemPositionsListener.create(); + + /// 章节详情 + Rx detail = + Rx(ComicChapterDetail.empty()); + + /// 连接信息监听 + StreamSubscription? connectivitySubscription; + + /// 电量信息监听 + StreamSubscription? batterySubscription; + + /// 当处于放大图片时,锁定滑动手势 + var lockSwipe = false.obs; + + /// 当前章节索引 + var chapterIndex = 0.obs; + + /// 当前页面 + var currentIndex = 0.obs; + + /// 初始化 + var initialIndex = 0; + + /// 是否显示控制器 + var showControls = false.obs; + + /// 阅读方向 + var direction = 0.obs; + + /// 左手模式 + bool get leftHandMode => settings.comicReaderLeftHandMode.value; + + /// 翻页动画 + bool get pageAnimation => settings.comicReaderPageAnimation.value; + + /// 观点、吐槽 + RxList viewPoints = RxList(); + + /// 连接类型 + Rx connectivityType = + Rx(ConnectivityResult.other); + + /// 电量信息 + Rx batteryLevel = 0.obs; + + /// 显示电量 + RxBool showBattery = true.obs; + + @override + void onInit() { + initConnectivity(); + initBattery(); + if (isLongComic) { + direction.value = ReaderDirection.kUpToDown; + } else { + direction.value = settings.comicReaderDirection.value; + } + + if (settings.comicReaderFullScreen.value) { + setFull(); + } + + itemPositionsListener.itemPositions.addListener(updateItemPosition); + loadDetail(); + super.onInit(); + } + + /// 初始化电池信息 + void initBattery() async { + try { + //没有电池的Mac似乎会闪退,暂时屏蔽Mac + //https://github.com/xiaoyaocz/flutter_dmzj/discussions/146 + if (Platform.isMacOS) { + showBattery.value = false; + return; + } + var battery = Battery(); + batterySubscription = + battery.onBatteryStateChanged.listen((BatteryState state) async { + try { + var level = await battery.batteryLevel; + batteryLevel.value = level; + showBattery.value = true; + } catch (e) { + showBattery.value = false; + } + }); + batteryLevel.value = await battery.batteryLevel; + showBattery.value = true; + } catch (e) { + showBattery.value = false; + } + } + + /// 初始化连接状态 + void initConnectivity() async { + var connectivity = Connectivity(); + connectivitySubscription = + connectivity.onConnectivityChanged.listen((ConnectivityResult result) { + //提醒 + if (connectivityType.value != result && + result == ConnectivityResult.mobile) { + SmartDialog.showToast("您已切换至数据网络,请注意流量消耗"); + } + connectivityType.value = result; + }); + connectivityType.value = await connectivity.checkConnectivity(); + } + + @override + void onClose() { + focusNode.dispose(); + connectivitySubscription?.cancel(); + batterySubscription?.cancel(); + exitFull(); + itemPositionsListener.itemPositions.removeListener(updateItemPosition); + uploadHistory(); + super.onClose(); + } + + void updateItemPosition() { + var items = itemPositionsListener.itemPositions.value; + if (items.isEmpty) { + return; + } + + var index = items + .where((ItemPosition position) => position.itemTrailingEdge > 0) + .reduce((ItemPosition min, ItemPosition position) => + position.itemTrailingEdge < min.itemTrailingEdge ? position : min) + .index; + + currentIndex.value = index; + } + + /// 加载信息 + void loadDetail() async { + try { + pageLoadding.value = true; + pageError.value = false; + + detail.value = ComicChapterDetail.empty(); + var chapterId = chapters[chapterIndex.value].chapterId; + if (chapters[chapterIndex.value].isVip) { + //禁止观看VIP章节 + throw AppError("请使用动漫之家官方APP观看VIP章节"); + } + //loadViewPoints(); + + var result = await request.chapterDetail( + comicId: comicId, + chapterId: chapterId, + useHD: AppSettingsService.instance.comicReaderHD.value, + ); + var his = DBService.instance.getComicHistory(comicId); + if (his != null && his.chapterId == chapterId && his.page != 0) { + var hisIndex = (his.page - 1) < 0 ? 0 : his.page - 1; + if (hisIndex >= result.pageUrls.length - 1) { + hisIndex = 0; + } + initialIndex = hisIndex; + } else { + initialIndex = 0; + } + currentIndex.value = initialIndex; + // if (settings.comicReaderShowViewPoint.value) { + // result.pageUrls.add("TC"); + // } + + detail.value = result; + Future.delayed(const Duration(milliseconds: 100), () { + jumpToPage(initialIndex); + }); + //上传记录 + uploadHistory(); + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + setShowControls(); + } finally { + pageLoadding.value = false; + } + } + + /// 加载吐槽、观点 + void loadViewPoints() async { + try { + viewPoints.clear(); + var result = await request.viewPoints( + comicId: comicId, + chapterId: chapters[chapterIndex.value].chapterId, + ); + result.sort((a, b) => b.num.value.compareTo(a.num.value)); + viewPoints.value = result; + } catch (e) { + //SmartDialog.showToast("读取吐槽失败"); + Log.logPrint(e.toString()); + } + } + + /// 设置显示/隐藏控制按钮 + void setShowControls() { + if (settings.comicReaderFullScreen.value) { + if (showControls.value) { + setFull(); + } else { + setFullEdge(); + } + } + Future.delayed(const Duration(milliseconds: 100), () { + showControls.value = !showControls.value; + }); + } + + /// 显示目录 + void showMenu() async { + setShowControls(); + showModalBottomSheet( + context: Get.context!, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + constraints: const BoxConstraints( + maxWidth: 500, + ), + builder: (context) => Column( + children: [ + ListTile( + title: Text("目录(${chapters.length})"), + trailing: IconButton( + onPressed: Get.back, + icon: const Icon(Icons.close), + ), + contentPadding: AppStyle.edgeInsetsL12, + ), + Divider( + height: 1.0, + color: Theme.of(context).dividerColor.withOpacity(.2), + ), + Expanded( + child: ScrollablePositionedList.separated( + initialScrollIndex: chapterIndex.value, + itemCount: chapters.length, + separatorBuilder: (_, i) => Divider( + indent: 12, + endIndent: 12, + height: 1.0, + color: Theme.of(context).dividerColor.withOpacity(.2), + ), + itemBuilder: (_, i) { + var item = chapters[i]; + return ListTile( + selected: i == chapterIndex.value, + selectedTileColor: + Theme.of(context).colorScheme.secondaryContainer, + selectedColor: + Theme.of(context).colorScheme.onSecondaryContainer, + title: Text(item.chapterTitle), + subtitle: item.updateTime != 0 + ? Text( + "更新于${Utils.formatTimestampToDate(item.updateTime)}") + : null, + onTap: () { + chapterIndex.value = i; + loadDetail(); + Get.back(); + }, + ); + }, + ), + ), + ], + ), + routeSettings: const RouteSettings(name: "/modalBottomSheet"), + ); + } + + /// 下一章 + void nextChapter() { + if (chapterIndex.value == chapters.length - 1) { + SmartDialog.showToast("后面没有了"); + return; + } + + chapterIndex.value += 1; + loadDetail(); + } + + /// 上一章 + void forwardChapter() { + if (chapterIndex.value == 0) { + SmartDialog.showToast("前面没有了"); + return; + } + + chapterIndex.value -= 1; + loadDetail(); + } + + /// 下一页 + void nextPage() { + var value = currentIndex.value; + Log.w("下一页$value"); + var max = detail.value.pageUrls.length; + if (value >= max - 1) { + nextChapter(); + } else { + jumpToPage(value + 1, anime: true); + } + } + + /// 上一页 + void forwardPage() { + var value = currentIndex.value; + Log.w("上一页$value"); + if (value == 0) { + forwardChapter(); + } else { + jumpToPage(value - 1, anime: true); + } + } + + /// 跳转页数 + void jumpToPage(int page, {bool anime = false}) { + //竖向 + if (direction.value == ReaderDirection.kUpToDown) { + itemScrollController.jumpTo(index: page); + } else { + anime && pageAnimation + ? preloadPageController.animateToPage(page, + duration: const Duration(milliseconds: 200), curve: Curves.linear) + : preloadPageController.jumpToPage(page); + } + } + + /// 查看吐槽 + void showComment() { + setShowControls(); + TextEditingController tucaoController = TextEditingController(); + showModalBottomSheet( + context: Get.context!, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + constraints: const BoxConstraints( + maxWidth: 500, + ), + isScrollControlled: true, + useSafeArea: true, + builder: (context) => Column( + children: [ + ListTile( + title: Text("吐槽(${viewPoints.length})"), + trailing: IconButton( + onPressed: Get.back, + icon: const Icon(Icons.close), + ), + contentPadding: AppStyle.edgeInsetsL12, + ), + Divider( + height: 1.0, + color: Theme.of(context).dividerColor.withOpacity(.2), + ), + Expanded( + child: EasyRefresh( + header: const MaterialHeader(), + onRefresh: () async { + loadViewPoints(); + }, + child: Obx( + () => settings.comicReaderOldViewPoint.value + ? SingleChildScrollView( + child: Padding( + padding: AppStyle.edgeInsetsA12, + child: Wrap( + spacing: 8, + runSpacing: 8, + children: viewPoints.map((item) { + return InkWell( + onTap: () { + likeViewPoint(item); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: + Theme.of(context).colorScheme.primary, + borderRadius: AppStyle.radius8, + ), + child: Text( + item.content, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimary), + ), + ), + ); + }).toList(), + ), + ), + ) + : ListView.separated( + padding: EdgeInsets.zero, + itemCount: viewPoints.length, + separatorBuilder: (_, i) => Divider( + indent: 12, + endIndent: 12, + height: 1.0, + color: Theme.of(context).dividerColor.withOpacity(.2), + ), + itemBuilder: (_, i) { + var item = viewPoints[i]; + return Padding( + padding: AppStyle.edgeInsetsA12 + .copyWith(top: 8, bottom: 8), + child: Row( + children: [ + Expanded( + child: Text( + item.content, + style: const TextStyle( + fontSize: 15, + ), + ), + ), + AppStyle.hGap12, + TextButton.icon( + style: TextButton.styleFrom( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + likeViewPoint(item); + }, + icon: const Icon( + Remix.thumb_up_line, + size: 16, + ), + label: Obx(() => Text("${item.num.value}")), + ), + ], + ), + ); + }, + ), + ), + ), + ), + Container( + padding: AppStyle.edgeInsetsA8.copyWith( + bottom: 8 + AppStyle.bottomBarHeight, + ), + child: TextField( + controller: tucaoController, + onSubmitted: (e) { + sendViewPoint(e); + }, + decoration: InputDecoration( + hintText: "发表吐槽", + contentPadding: AppStyle.edgeInsetsH12, + border: const OutlineInputBorder(), + suffixIcon: TextButton( + onPressed: () { + sendViewPoint(tucaoController.text); + }, + child: const Text("发布"), + ), + ), + ), + ), + ], + ), + routeSettings: const RouteSettings(name: "/modalBottomSheet"), + ); + } + + /// 显示设置 + void showSettings() { + setShowControls(); + + showModalBottomSheet( + context: Get.context!, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + constraints: const BoxConstraints( + maxWidth: 500, + ), + builder: (context) => Column( + children: [ + ListTile( + title: const Text("设置"), + trailing: IconButton( + onPressed: Get.back, + icon: const Icon(Icons.close), + ), + contentPadding: AppStyle.edgeInsetsL12, + ), + Expanded( + child: Obx( + () => ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + buildBGItem( + context, + child: SwitchListTile( + value: settings.comicReaderHD.value, + onChanged: (e) { + settings.setComicReaderHD(e); + loadDetail(); + }, + title: const Text("优先加载高清图"), + subtitle: const Text("部分单行本可能未分页"), + ), + ), + //AppStyle.vGap12, + Visibility( + //条漫不允许修改阅读方向 + visible: !isLongComic, + child: Padding( + padding: AppStyle.edgeInsetsT12, + child: buildBGItem( + context, + child: ListTile( + title: const Text("阅读方向"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + buildSelectedButton( + onTap: () { + setDirection(ReaderDirection.kLeftToRight); + }, + selected: settings.comicReaderDirection.value == + ReaderDirection.kLeftToRight, + child: const Icon(Remix.arrow_right_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + setDirection(ReaderDirection.kRightToLeft); + }, + selected: settings.comicReaderDirection.value == + ReaderDirection.kRightToLeft, + child: const Icon(Remix.arrow_left_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + setDirection(ReaderDirection.kUpToDown); + }, + selected: settings.comicReaderDirection.value == + ReaderDirection.kUpToDown, + child: const Icon(Remix.arrow_down_line), + ) + ], + ), + ), + ), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: SwitchListTile( + value: settings.comicReaderLeftHandMode.value, + onChanged: (e) { + settings.setComicReaderLeftHandMode(e); + }, + title: const Text("操作反转"), + subtitle: const Text("点击左侧下一页,右侧上一页"), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: SwitchListTile( + value: settings.comicReaderFullScreen.value, + onChanged: (e) { + settings.setComicReaderFullScreen(e); + if (e) { + setFull(); + } else { + exitFull(); + } + }, + title: const Text("全屏阅读"), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: SwitchListTile( + value: settings.comicReaderShowStatus.value, + onChanged: (e) { + settings.setComicReaderShowStatus(e); + }, + title: const Text("显示状态信息"), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: SwitchListTile( + value: settings.comicReaderPageAnimation.value, + onChanged: (e) { + settings.setComicReaderPageAnimation(e); + }, + title: const Text("翻页动画"), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget buildBGItem(BuildContext context, {required Widget child}) { + return Container( + decoration: BoxDecoration( + borderRadius: AppStyle.radius8, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + child: child, + ); + } + + Widget buildSelectedButton( + {required Widget child, bool selected = false, Function()? onTap}) { + return Builder(builder: (context) { + return OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: + selected ? Theme.of(context).colorScheme.primary : Colors.grey, + side: BorderSide( + color: + selected ? Theme.of(context).colorScheme.primary : Colors.grey, + ), + ), + onPressed: onTap, + child: child, + ); + }); + } + + void setDirection(int value) { + initialIndex = currentIndex.value; + settings.setComicReaderDirection(value); + direction.value = value; + if (initialIndex != 0) { + Future.delayed(const Duration(milliseconds: 200), () { + jumpToPage(initialIndex); + }); + } + } + + void setShowViewPoint(bool value) { + if (value) { + if (!detail.value.pageUrls.contains("TC")) { + detail.update((val) { + val!.pageUrls.add("TC"); + }); + } + } else { + if (detail.value.pageUrls.contains("TC")) { + detail.update((val) { + val!.pageUrls.remove("TC"); + }); + } + } + } + + void uploadHistory() { + var chapter = chapters[chapterIndex.value]; + UserService.instance.updateComicHistory( + comicId: comicId, + chapterId: chapter.chapterId, + page: currentIndex.value + 1, + comicName: comicTitle, + comicCover: comicCover, + chapterName: chapter.chapterTitle, + ); + } + + /// 进入全屏 + void setFull() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [], + ); + } + + /// 进入全屏edgeToEdge模式 + void setFullEdge() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + overlays: SystemUiOverlay.values, + ); + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.light, + )); + } + + /// 退出全屏 + void exitFull() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + overlays: SystemUiOverlay.values, + ); + } + + void likeViewPoint(ComicViewPointModel item) async { + try { + await request.likeViewPoint(comicId: comicId, id: item.id); + + item.num.value += 1; + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + void sendViewPoint(String content) async { + if (!await UserService.instance.login()) { + SmartDialog.showToast("请先登录"); + return; + } + if (content.isEmpty) { + SmartDialog.showToast("内容不能为空"); + return; + } + Get.back(); + try { + SmartDialog.showLoading(); + await request.sendViewPoint( + comicId: comicId, + chapterId: chapters[chapterIndex.value].chapterId, + content: content, + page: currentIndex.value + 1, + ); + loadViewPoints(); + } catch (e) { + SmartDialog.showToast(e.toString()); + } finally { + SmartDialog.dismiss(status: SmartStatus.loading); + } + } + + void keyDown(LogicalKeyboardKey key) { + if (key == LogicalKeyboardKey.arrowLeft || + key == LogicalKeyboardKey.pageUp) { + if (leftHandMode) { + nextPage(); + } else { + forwardPage(); + } + } else if (key == LogicalKeyboardKey.arrowRight || + key == LogicalKeyboardKey.pageDown) { + if (leftHandMode) { + forwardPage(); + } else { + nextPage(); + } + } + } +} diff --git a/lib/modules/comic/reader/comic_reader_page.dart b/lib/modules/comic/reader/comic_reader_page.dart new file mode 100644 index 0000000..9b92661 --- /dev/null +++ b/lib/modules/comic/reader/comic_reader_page.dart @@ -0,0 +1,612 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; + +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/modules/comic/reader/comic_reader_controller.dart'; +import 'package:flutter_dmzj/widgets/custom_header.dart'; +import 'package:flutter_dmzj/widgets/local_image.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:get/get.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:preload_page_view/preload_page_view.dart'; +import 'package:remixicon/remixicon.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +class ComicReaderPage extends GetView { + const ComicReaderPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return KeyboardListener( + onKeyEvent: (e) { + if (e.runtimeType == KeyUpEvent) { + controller.keyDown(e.logicalKey); + Log.d(e.toString()); + } + }, + focusNode: controller.focusNode, + autofocus: true, + child: Theme( + data: Theme.of(context), + child: Scaffold( + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + Obx( + () => Offstage( + offstage: controller.detail.value.chapterId == 0, + child: GestureDetector( + onTap: () { + controller.setShowControls(); + }, + child: + controller.direction.value == ReaderDirection.kUpToDown + ? buildVertical(context) + : buildHorizontal(context), + ), + ), + ), + Positioned.fill( + child: Row( + children: [ + Expanded( + flex: 1, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + controller.leftHandMode + ? controller.nextPage() + : controller.forwardPage(); + }, + child: Container( + width: double.infinity, + height: double.infinity, + color: Colors.transparent, + ), + ), + ), + Expanded( + flex: 8, + child: Container(), + ), + Expanded( + flex: 1, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + controller.leftHandMode + ? controller.forwardPage() + : controller.nextPage(); + }, + child: Container( + width: double.infinity, + height: double.infinity, + color: Colors.transparent, + ), + ), + ), + ], + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadDetail(), + ), + ), + ), + Positioned( + right: 0, + bottom: 0, + child: Obx( + () => Offstage( + offstage: !controller.settings.comicReaderShowStatus.value, + child: Container( + decoration: const BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + ), + ), + padding: + AppStyle.edgeInsetsA12.copyWith(top: 4, bottom: 4), + child: Obx( + () => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + buildConnectivity(), + buildBattery(), + Container( + constraints: const BoxConstraints(maxWidth: 100), + child: Text( + controller.detail.value.chapterTitle, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + AppStyle.hGap8, + Text( + "${controller.currentIndex.value + 1} / ${controller.detail.value.pageUrls.length}", + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ), + ), + //顶部 + Obx( + () => AnimatedPositioned( + top: controller.showControls.value + ? 0 + : -(64 + AppStyle.statusBarHeight), + left: 0, + right: 0, + duration: const Duration(milliseconds: 100), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + height: 64 + AppStyle.statusBarHeight, + padding: EdgeInsets.only(top: AppStyle.statusBarHeight), + child: Row( + children: [ + IconButton( + onPressed: Get.back, + icon: const Icon(Icons.arrow_back), + ), + AppStyle.hGap12, + Expanded( + child: Obx( + () => Text( + controller.chapters[controller.chapterIndex.value] + .chapterTitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ], + ), + ), + ), + ), + //底部 + Obx( + () => AnimatedPositioned( + bottom: controller.showControls.value + ? 0 + : -(136 + AppStyle.bottomBarHeight), + left: 0, + right: 0, + duration: const Duration(milliseconds: 100), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + height: 136 + AppStyle.bottomBarHeight, + padding: EdgeInsets.only(bottom: AppStyle.bottomBarHeight), + alignment: Alignment.center, + child: Container( + constraints: const BoxConstraints( + maxWidth: 600, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + buildSilderBar(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton.filledTonal( + onPressed: controller.forwardChapter, + icon: const Icon(Remix.skip_back_line), + tooltip: "上一话", + ), + IconButton.filledTonal( + onPressed: controller.showMenu, + icon: const Icon(Remix.file_list_line), + tooltip: "目录", + ), + IconButton.filledTonal( + onPressed: controller.showSettings, + icon: const Icon(Remix.settings_line), + tooltip: "设置", + ), + IconButton.filledTonal( + onPressed: controller.nextChapter, + icon: const Icon(Remix.skip_forward_line), + tooltip: "下一话", + ), + ], + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget buildHorizontal(BuildContext context) { + return EasyRefresh( + header: MaterialHeader2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_left, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + footer: MaterialFooter2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_right, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + refreshOnStart: false, + onRefresh: () async { + controller.forwardChapter(); + }, + onLoad: () async { + controller.nextChapter(); + }, + child: PreloadPageView.builder( + controller: controller.preloadPageController, + onPageChanged: (e) { + controller.currentIndex.value = e; + }, + reverse: controller.direction.value == ReaderDirection.kRightToLeft, + physics: controller.lockSwipe.value + ? const NeverScrollableScrollPhysics() + : null, + itemCount: controller.detail.value.pageUrls.length, + preloadPagesCount: 4, + itemBuilder: (_, i) { + var url = controller.detail.value.pageUrls[i]; + // if (i == controller.detail.value.pageUrls.length - 1 && url == "TC") { + // return buildViewPoints(); + // } + return PhotoView.customChild( + wantKeepAlive: true, + initialScale: 1.0, + onScaleEnd: (context, detail, e) { + controller.lockSwipe.value = (e.scale ?? 1) > 1.0; + }, + child: controller.detail.value.isLocal + ? LocalImage(url, fit: BoxFit.contain) + : NetImage( + url, + fit: BoxFit.contain, + progress: true, + ), + ); + }, + ), + ); + } + + Widget buildVertical(BuildContext context) { + return EasyRefresh( + header: MaterialHeader2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_up, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + footer: MaterialFooter2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_down, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + refreshOnStart: false, + onRefresh: () async { + controller.forwardChapter(); + }, + onLoad: () async { + controller.nextChapter(); + }, + child: ScrollablePositionedList.builder( + itemScrollController: controller.itemScrollController, + itemCount: controller.detail.value.pageUrls.length, + itemPositionsListener: controller.itemPositionsListener, + itemBuilder: (_, i) { + // if (i == controller.detail.value.pageUrls.length - 1 && + // controller.detail.value.pageUrls[i] == "TC") { + // return buildViewPoints(shrinkWrap: true); + // } + var url = controller.detail.value.pageUrls[i]; + return Container( + constraints: const BoxConstraints( + minHeight: 200, + ), + child: controller.detail.value.isLocal + ? LocalImage(url, fit: BoxFit.contain) + : NetImage( + url, + fit: BoxFit.fitWidth, + progress: true, + ), + ); + }, + ), + ); + } + + Widget buildSilderBar() { + return Obx( + () { + var value = controller.currentIndex.value + 1.0; + var max = controller.detail.value.pageUrls.length.toDouble(); + if (value > max) { + return const SizedBox( + height: 48, + ); + } + return SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Slider( + value: value, + max: max, + onChanged: (e) { + controller.jumpToPage((e - 1).toInt()); + }, + ), + ), + ], + ), + ); + }, + ); + } + + Widget buildViewPoints({bool shrinkWrap = false}) { + return Obx( + () => ListView( + shrinkWrap: shrinkWrap, + physics: shrinkWrap ? const NeverScrollableScrollPhysics() : null, + padding: EdgeInsets.zero, + children: [ + ListTile( + title: Text("吐槽(${controller.viewPoints.length})"), + ), + Padding( + padding: AppStyle.edgeInsetsH12, + child: Wrap( + spacing: 8, + runSpacing: 8, + children: controller.viewPoints + .take(10) + .map( + (e) => OutlinedButton( + style: OutlinedButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + controller.likeViewPoint(e); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + e.content, + style: const TextStyle( + fontSize: 14, color: Colors.white), + ), + AppStyle.hGap12, + const Icon( + Remix.thumb_up_line, + size: 16, + ), + AppStyle.hGap4, + Obx( + () => Text( + "${e.num.value}", + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ], + ), + ), + ) + .toList(), + ), + ), + Container( + alignment: Alignment.center, + width: 100, + margin: AppStyle.edgeInsetsA12, + child: OutlinedButton( + onPressed: () { + controller.showComment(); + }, + child: const Text("查看更多"), + ), + ), + AppStyle.vGap12, + ], + ), + ); + } + + Widget buildConnectivity() { + var connectivityType = controller.connectivityType.value; + IconData icon = Remix.wifi_line; + var name = "WiFi"; + switch (connectivityType) { + case ConnectivityResult.bluetooth: + icon = Remix.wifi_line; + name = "蓝牙"; + break; + case ConnectivityResult.ethernet: + icon = Remix.computer_line; + name = "有线"; + break; + case ConnectivityResult.mobile: + icon = Remix.base_station_line; + name = "流量"; + break; + case ConnectivityResult.wifi: + icon = Remix.wifi_line; + name = "WiFi"; + break; + case ConnectivityResult.vpn: + icon = Remix.shield_keyhole_line; + name = "VPN"; + break; + case ConnectivityResult.none: + icon = Remix.wifi_off_line; + name = "无网络"; + break; + case ConnectivityResult.other: + icon = Remix.question_line; + name = "未知"; + break; + default: + } + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + icon, + size: 12, + color: Colors.white, + ), + AppStyle.hGap4, + Text( + name, + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + AppStyle.hGap8, + ], + ); + } + + Widget buildBattery() { + var battery = controller.batteryLevel.value; + // IconData icon = Icons.battery_0_bar; + // if (battery >= 90) { + // icon = Icons.battery_full; + // } else if (battery < 90 && battery >= 80) { + // icon = Icons.battery_6_bar; + // } else if (battery < 80 && battery >= 70) { + // icon = Icons.battery_5_bar; + // } else if (battery < 70 && battery >= 50) { + // icon = Icons.battery_4_bar; + // } else if (battery < 50 && battery >= 30) { + // icon = Icons.battery_3_bar; + // } else if (battery < 30 && battery >= 20) { + // icon = Icons.battery_2_bar; + // } else if (battery < 20 && battery >= 10) { + // icon = Icons.battery_1_bar; + // } else { + // icon = Icons.battery_0_bar; + // } + return Visibility( + visible: controller.showBattery.value, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icon( + // icon, + // size: 16, + // ), + // AppStyle.hGap4, + Text( + "电量 $battery%", + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + AppStyle.hGap8, + ], + ), + ); + } +} diff --git a/lib/modules/comic/search/comic_search_controller.dart b/lib/modules/comic/search/comic_search_controller.dart new file mode 100644 index 0000000..0046a13 --- /dev/null +++ b/lib/modules/comic/search/comic_search_controller.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/comic/search_item.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:get/get.dart'; + +class ComicSearchController extends BasePageController { + final String keyword; + ComicSearchController(this.keyword) { + searchController = TextEditingController(text: keyword); + showHotWord.value = keyword.isEmpty; + } + late TextEditingController searchController; + final ComicRequest request = ComicRequest(); + + String _keyword = ""; + + RxMap hotWords = {}.obs; + + var showHotWord = true.obs; + + @override + void onInit() { + // loadHotWord(); + if (keyword.isNotEmpty) { + submit(); + } + super.onInit(); + } + + void submit() async { + if (searchController.text.isEmpty) { + list.clear(); + showHotWord.value = true; + return; + } + + if (int.tryParse(searchController.text) != null && + await numberJumpComic()) { + return; + } + + if (searchController.text.startsWith("id:\\") && await handelJumpComic()) { + return; + } + + showHotWord.value = false; + _keyword = searchController.text; + refreshData(); + } + + Future handelJumpComic() async { + var id = int.tryParse(searchController.text.replaceAll("id:\\", "")) ?? 0; + if (id != 0) { + AppNavigator.toComicDetail(id); + return true; + } else { + return false; + } + } + + Future numberJumpComic() async { + if (!await DialogUtils.showAlertDialog( + "你输入了纯数字,是否跳转至对应的漫画?", + title: "漫画ID跳转", + )) { + return false; + } + return await handelJumpComic(); + } + + @override + Future> getData(int page, int pageSize) async { + if (searchController.text.isEmpty) { + return []; + } + // if (AppSettingsService.instance.comicSearchUseWebApi.value) { + // //WEB接口不能分页 + // if (page > 1) { + // return []; + // } + // return await request.searchWeb(keyword: _keyword); + // } else { + return await request.search(keyword: _keyword, page: page); + //} + } + + void loadHotWord() async { + try { + hotWords.value = await request.searchHotWord(); + } catch (e) { + Log.logPrint(e); + } + } +} diff --git a/lib/modules/comic/search/comic_search_page.dart b/lib/modules/comic/search/comic_search_page.dart new file mode 100644 index 0000000..d51ab37 --- /dev/null +++ b/lib/modules/comic/search/comic_search_page.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comic/search_item.dart'; +import 'package:flutter_dmzj/modules/comic/search/comic_search_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class ComicSearchPage extends StatelessWidget { + final String keyword; + final ComicSearchController controller; + ComicSearchPage({this.keyword = "", super.key}) + : controller = Get.put(ComicSearchController(keyword)); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 8, + title: SizedBox( + height: 40, + child: TextField( + controller: controller.searchController, + autofocus: true, + decoration: InputDecoration( + hintText: "搜索漫画", + contentPadding: AppStyle.edgeInsetsH12, + border: const OutlineInputBorder(), + prefixIcon: SizedBox( + width: 48, + child: IconButton( + onPressed: () { + AppNavigator.closePage(); + }, + icon: const Icon(Icons.arrow_back), + ), + ), + suffixIcon: SizedBox( + width: 48, + child: IconButton( + onPressed: controller.submit, + icon: const Icon(Icons.search), + ), + ), + ), + onSubmitted: (e) { + controller.submit(); + }, + ), + ), + ), + body: Stack( + children: [ + PageListView( + pageController: controller, + firstRefresh: false, + showPageLoadding: true, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + // Positioned.fill( + // child: Obx( + // () => Offstage( + // offstage: !controller.showHotWord.value, + // child: SingleChildScrollView( + // child: Column( + // children: [ + // const ListTile( + // title: Text("热门搜索"), + // ), + // Padding( + // padding: AppStyle.edgeInsetsH12.copyWith(bottom: 12), + // child: Wrap( + // spacing: 8, + // runSpacing: 8, + // children: controller.hotWords.keys + // .map( + // (e) => OutlinedButton( + // style: OutlinedButton.styleFrom( + // tapTargetSize: + // MaterialTapTargetSize.shrinkWrap, + // ), + // onPressed: () { + // AppNavigator.toComicDetail(e); + // }, + // child: Text(controller.hotWords[e] ?? ""), + // ), + // ) + // .toList(), + // ), + // ) + // ], + // ), + // ), + // ), + // ), + // ), + ], + ), + ); + } + + Widget buildItem(SearchComicItem item) { + return InkWell( + onTap: () { + AppNavigator.toComicDetail(item.comicId); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text.rich( + TextSpan(children: [ + const WidgetSpan( + child: Icon( + Icons.account_circle, + color: Colors.grey, + size: 18, + )), + const TextSpan( + text: " ", + ), + TextSpan( + text: item.author, + style: + const TextStyle(color: Colors.grey, fontSize: 14)) + ]), + ), + AppStyle.vGap4, + Text(item.tags, + style: const TextStyle(color: Colors.grey, fontSize: 14)), + AppStyle.vGap4, + Text(item.lastChapterName, + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/comic/select_chapter/comic_select_chapter_controller.dart b/lib/modules/comic/select_chapter/comic_select_chapter_controller.dart new file mode 100644 index 0000000..a2e0651 --- /dev/null +++ b/lib/modules/comic/select_chapter/comic_select_chapter_controller.dart @@ -0,0 +1,147 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class ComicSelectChapterController extends BaseController { + final int comicId; + ComicSelectChapterController(this.comicId); + final ComicRequest request = ComicRequest(); + + RxList volumes = RxList(); + + RxSet chapterIds = RxSet(); + + String comicTitle = ""; + String comicCover = ""; + bool islong = false; + + @override + void onInit() { + loadDetail(); + + super.onInit(); + } + + void refreshV1() async { + try { + var result = + await request.comicDetail(comicId: comicId, priorityV1: true); + if (result.volumes.isEmpty) { + SmartDialog.showToast("没有找到任何章节"); + return; + } + comicTitle = result.title; + comicCover = result.cover; + islong = result.isLong; + for (var volume in result.volumes) { + volume.sortType.value = 1; + volume.sort(); + } + volumes.value = result.volumes; + } catch (e) { + SmartDialog.showToast("无法获取章节"); + } + } + + /// 加载信息 + void loadDetail() async { + try { + pageLoadding.value = true; + pageError.value = false; + var result = await request.comicDetail(comicId: comicId); + comicTitle = result.title; + comicCover = result.cover; + islong = result.isLong; + if (result.volumes.isEmpty && !result.isHide) { + refreshV1(); + } else { + for (var volume in result.volumes) { + volume.sortType.value = 1; + volume.sort(); + } + volumes.value = result.volumes; + } + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + } + + void selectItem(ComicDetailChapterItem item) { + //禁止下载VIP章节 + if (item.isVip) { + SmartDialog.showToast("请使用动漫之家官方APP下载VIP章节"); + return; + } + if (chapterIds.contains(item.chapterId)) { + chapterIds.remove(item.chapterId); + } else { + chapterIds.add(item.chapterId); + } + } + + void selectAll() { + for (var volume in volumes) { + for (var chapter in volume.chapters) { + if (chapter.isVip) { + continue; + } + var id = "${comicId}_${chapter.chapterId}"; + if (!ComicDownloadService.instance.downloadIds.contains(id)) { + chapterIds.add(chapter.chapterId); + } + } + } + } + + void cleanAll() { + chapterIds.clear(); + } + + void toDownloadManage() { + AppNavigator.toComicDownloadManage(1); + } + + void startDownload() { + if (chapterIds.isEmpty) { + SmartDialog.showToast("请选择需要下载的章节"); + return; + } + for (var id in chapterIds) { + //搜索章节 + ComicDetailVolume? volume; + ComicDetailChapterItem? chapter; + for (var item in volumes) { + var chapterItem = + item.chapters.firstWhereOrNull((y) => y.chapterId == id); + if (chapterItem != null) { + volume = item; + chapter = chapterItem; + break; + } + } + if (volume == null || chapter == null) { + continue; + } + ComicDownloadService.instance.addTask( + comicId: comicId, + chapterId: chapter.chapterId, + chapterSort: chapter.chapterOrder, + volumeName: volume.title, + comicTitle: comicTitle, + comicCover: comicCover, + chapterName: chapter.chapterTitle, + isVip: chapter.isVip, + isLongComic: islong, + ); + } + chapterIds.clear(); + SmartDialog.showToast("已添加到下载列表,下载过程中请保持APP在前台运行"); + } +} diff --git a/lib/modules/comic/select_chapter/comic_select_chapter_page.dart b/lib/modules/comic/select_chapter/comic_select_chapter_page.dart new file mode 100644 index 0000000..ed6e2d1 --- /dev/null +++ b/lib/modules/comic/select_chapter/comic_select_chapter_page.dart @@ -0,0 +1,259 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/modules/comic/select_chapter/comic_select_chapter_controller.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class ComicSelectChapterPage extends StatelessWidget { + final int comicId; + final ComicSelectChapterController controller; + ComicSelectChapterPage(this.comicId, {super.key}) + : controller = Get.put( + ComicSelectChapterController(comicId), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("选择下载章节"), + actions: [ + TextButton( + onPressed: controller.toDownloadManage, + child: const Text("下载管理"), + ), + ], + ), + body: Stack( + children: [ + EasyRefresh( + header: const MaterialHeader(), + onRefresh: controller.loadDetail, + child: _buildVolumes(), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadDetail(), + ), + ), + ), + ], + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.selectAll, + icon: const Icon( + Remix.checkbox_line, + size: 20, + ), + label: const Text("全选"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.cleanAll, + icon: const Icon( + Remix.checkbox_blank_line, + size: 20, + ), + label: const Text("取消选中"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.startDownload, + icon: const Icon( + Remix.download_line, + size: 20, + ), + label: + Obx(() => Text("下载选中(${controller.chapterIds.length})")), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildVolumes() { + return Obx( + () => ListView.builder( + padding: AppStyle.edgeInsetsA12, + itemCount: controller.volumes.length, + itemBuilder: (_, i) { + var item = controller.volumes[i]; + return _buildChapters(item); + }, + ), + ); + } + + Widget _buildChapters(ComicDetailVolume item) { + return Obx( + () => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: AppStyle.edgeInsetsV8, + child: Row( + children: [ + Expanded( + child: Text( + "${item.title}(共${item.chapters.length}话)", + style: Get.textTheme.titleSmall, + ), + ), + item.sortType.value == 1 + ? TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + item.sortType.value = 0; + item.sort(); + }, + icon: const Icon( + Remix.sort_asc, + size: 20, + ), + label: const Text("升序"), + ) + : TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + item.sortType.value = 1; + item.sort(); + }, + icon: const Icon( + Remix.sort_desc, + size: 20, + ), + label: const Text("倒序"), + ), + ], + ), + ), + LayoutBuilder(builder: (ctx, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + + return Obx( + () => MasonryGridView.count( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemCount: (item.showMoreButton && !item.showAll.value) + ? 15 + : item.chapters.length, + itemBuilder: (_, i) { + if (item.showMoreButton && !item.showAll.value && i == 14) { + return Tooltip( + message: "展开全部章节", + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.grey, + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size.fromHeight(40), + ), + onPressed: () { + item.showAll.value = true; + }, + child: const Icon(Icons.arrow_drop_down), + ), + ); + } + var chapter = item.chapters[i]; + + return Tooltip( + message: chapter.chapterTitle, + child: Obx( + () => Stack( + children: [ + OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: controller.chapterIds + .contains(chapter.chapterId) + ? Get.theme.colorScheme.primary + : Get.textTheme.bodyMedium!.color, + side: controller.chapterIds + .contains(chapter.chapterId) + ? BorderSide(color: Get.theme.colorScheme.primary) + : null, + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size.fromHeight(40), + ), + onPressed: ComicDownloadService.instance.downloadIds + .contains("${comicId}_${chapter.chapterId}") + ? null + : () => controller.selectItem(chapter), + child: Text( + item.chapters[i].chapterTitle, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + Positioned( + left: -2, + top: 0, + child: Offstage( + offstage: !item.chapters[i].isVip, + child: Image.asset( + "assets/images/vip_chapter.png", + height: 16, + ), + ), + ), + ], + ), + ), + ); + }, + crossAxisCount: count, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + ); + }) + ], + ), + ); + } +} diff --git a/lib/modules/comic/special_detail/special_detail_controller.dart b/lib/modules/comic/special_detail/special_detail_controller.dart new file mode 100644 index 0000000..072239b --- /dev/null +++ b/lib/modules/comic/special_detail/special_detail_controller.dart @@ -0,0 +1,63 @@ +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/comic/special_detail_model.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:get/get.dart'; + +class SpecialDetailController extends BaseController { + final int id; + SpecialDetailController(this.id); + + final ComicRequest request = ComicRequest(); + + Rx detail = Rx(null); + + @override + void onInit() { + loadData(); + super.onInit(); + } + + void loadData() async { + try { + pageLoadding.value = true; + pageError.value = false; + var result = await request.specialDetail(id: id); + detail.value = result; + } catch (e) { + handleError(e, showPageError: true); + } finally { + pageLoadding.value = false; + } + } + + void subscribeAll() { + if (detail.value == null) { + return; + } + UserService.instance.addSubscribe( + detail.value!.comics.map((e) => e.id).toList(), + AppConstant.kTypeComic, + ); + } + + void share() { + if (detail.value == null) { + return; + } + Utils.share( + "http://m.idmzj.com/zhuanti/${detail.value!.pageUrl}", + content: detail.value?.title ?? "", + ); + } + + void comment() { + if (detail.value == null) { + return; + } + AppNavigator.toComment(objId: id, type: AppConstant.kTypeSpecial); + } +} diff --git a/lib/modules/comic/special_detail/special_detail_page.dart b/lib/modules/comic/special_detail/special_detail_page.dart new file mode 100644 index 0000000..7f0e917 --- /dev/null +++ b/lib/modules/comic/special_detail/special_detail_page.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comic/special_detail_model.dart'; +import 'package:flutter_dmzj/modules/comic/special_detail/special_detail_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class SpecialDetailPage extends StatelessWidget { + final int id; + final SpecialDetailController controller; + SpecialDetailPage(this.id, {super.key}) + : controller = Get.put( + SpecialDetailController(id), + tag: "$id", + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx( + () => Text(controller.detail.value?.title ?? "专题"), + ), + ), + body: Obx( + () => Stack( + children: [ + Offstage( + offstage: controller.detail.value == null, + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: (controller.detail.value?.comics.length ?? 0) + 1, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (_, i) { + if (i == 0) { + return buildHeader(); + } + var item = controller.detail.value!.comics[i - 1]; + return buildItem(item); + }, + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadData(), + ), + ), + ), + ], + ), + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.subscribeAll, + icon: const Icon( + Remix.heart_line, + size: 20, + ), + label: const Text("订阅全部"), + ), + ), + Expanded( + child: Obx( + () => TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.comment, + icon: const Icon( + Remix.chat_2_line, + size: 20, + ), + label: Text( + "评论(${controller.detail.value?.commentAmount ?? 0})"), + ), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.share, + icon: const Icon( + Remix.share_box_line, + size: 20, + ), + label: const Text("分享"), + ), + ), + ], + ), + ), + ), + ); + } + + Widget buildItem(ComicSpecialComicModel item) { + return InkWell( + onTap: () { + AppNavigator.toComicDetail(item.id); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text(item.recommendBrief, + style: const TextStyle(color: Colors.grey, fontSize: 14)), + AppStyle.vGap4, + Text(item.recommendReason, + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + Center( + child: Obx( + () => UserService.instance.subscribedComicIds.contains(item.id) + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + UserService.instance.cancelSubscribe( + [item.id], + AppConstant.kTypeComic, + ); + }, + ) + : IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () { + UserService.instance.addSubscribe( + [item.id], + AppConstant.kTypeComic, + ); + }, + ), + ), + ) + ], + ), + ), + ); + } + + Widget buildHeader() { + if (controller.detail.value == null) { + return const SizedBox(); + } + var detail = controller.detail.value!; + return Padding( + padding: AppStyle.edgeInsetsA12, + child: Column( + children: [ + Container( + constraints: const BoxConstraints( + maxWidth: 500, + ), + child: AspectRatio( + aspectRatio: 710 / 354, + child: NetImage( + detail.mobileHeaderPic, + borderRadius: 8, + width: 710, + height: 354, + ), + ), + ), + AppStyle.vGap12, + Text( + detail.description, + ), + ], + ), + ); + } +} diff --git a/lib/modules/common/comment/add_comment_controller.dart b/lib/modules/common/comment/add_comment_controller.dart new file mode 100644 index 0000000..9a382fe --- /dev/null +++ b/lib/modules/common/comment/add_comment_controller.dart @@ -0,0 +1,52 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comment/comment_item.dart'; +import 'package:flutter_dmzj/requests/comment_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +class AddCommentController extends BaseController { + final int type; + final int objId; + final CommentItem? replyItem; + AddCommentController({ + required this.objId, + required this.type, + this.replyItem, + }); + final CommentRequest request = CommentRequest(); + final TextEditingController textEditingController = TextEditingController(); + + void submit() async { + if (textEditingController.text.isEmpty) { + SmartDialog.showToast("内容不能为空"); + return; + } + try { + SmartDialog.showLoading(); + if (replyItem == null) { + await request.sendComment( + objId: objId, + type: type, + content: textEditingController.text, + ); + } else { + await request.sendComment( + objId: objId, + type: type, + content: textEditingController.text, + toCommentId: replyItem!.id.toString(), + originCommentId: replyItem!.originId.toString(), + toUid: replyItem!.userId.toString(), + ); + } + + SmartDialog.showToast("发表成功"); + AppNavigator.closePage(); + } catch (e) { + SmartDialog.showToast(e.toString()); + } finally { + SmartDialog.dismiss(status: SmartStatus.loading); + } + } +} diff --git a/lib/modules/common/comment/add_comment_page.dart b/lib/modules/common/comment/add_comment_page.dart new file mode 100644 index 0000000..3fec07b --- /dev/null +++ b/lib/modules/common/comment/add_comment_page.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comment/comment_item.dart'; +import 'package:flutter_dmzj/modules/common/comment/add_comment_controller.dart'; +import 'package:get/get.dart'; + +class AddCommentPage extends StatelessWidget { + final int type; + final int objId; + final CommentItem? replyItem; + final AddCommentController controller; + AddCommentPage({ + Key? key, + required this.objId, + required this.type, + this.replyItem, + }) : controller = Get.put( + AddCommentController(objId: objId, type: type, replyItem: replyItem), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ), + super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("添加评论"), + ), + body: ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + Visibility( + visible: replyItem != null, + child: Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.2), + borderRadius: AppStyle.radius4, + ), + margin: AppStyle.edgeInsetsB12, + padding: AppStyle.edgeInsetsA8, + child: Text("${replyItem?.nickname}:${replyItem?.content}"), + ), + ), + TextField( + controller: controller.textEditingController, + decoration: const InputDecoration( + hintText: "你想说点什么...", + border: OutlineInputBorder(), + ), + onSubmitted: (e) { + controller.submit(); + }, + minLines: 4, + maxLines: 6, + maxLength: 1000, + ), + AppStyle.vGap12, + ElevatedButton( + onPressed: controller.submit, + child: const Text("发布"), + ), + ], + ), + ); + } +} diff --git a/lib/modules/common/comment/comment_controller.dart b/lib/modules/common/comment/comment_controller.dart new file mode 100644 index 0000000..10c8d03 --- /dev/null +++ b/lib/modules/common/comment/comment_controller.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class CommentController extends GetxController + with GetSingleTickerProviderStateMixin { + final int type; + final int objId; + CommentController(this.type, this.objId); + late TabController tabController = TabController(length: 2, vsync: this); +} diff --git a/lib/modules/common/comment/comment_list_controller.dart b/lib/modules/common/comment/comment_list_controller.dart new file mode 100644 index 0000000..607f6bc --- /dev/null +++ b/lib/modules/common/comment/comment_list_controller.dart @@ -0,0 +1,33 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comment/comment_item.dart'; +import 'package:flutter_dmzj/requests/comment_request.dart'; + +class CommentListController extends BasePageController { + final int type; + final int objId; + final bool isHot; + final CommentRequest request = CommentRequest(); + CommentListController({ + required this.type, + required this.objId, + required this.isHot, + }); + + @override + Future> getData(int page, int pageSize) async { + if (isHot) { + return await request.getComment( + type: type, + objId: objId, + page: page, + sort: 2, + ); + } else { + return await request.getComment( + type: type, + objId: objId, + page: page, + ); + } + } +} diff --git a/lib/modules/common/comment/comment_list_view.dart b/lib/modules/common/comment/comment_list_view.dart new file mode 100644 index 0000000..6e3227d --- /dev/null +++ b/lib/modules/common/comment/comment_list_view.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/modules/common/comment/comment_list_controller.dart'; +import 'package:flutter_dmzj/widgets/comment_item_widget.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class CommentListView extends StatelessWidget { + final int type; + final int objId; + final bool isHot; + final CommentListController controller; + CommentListView({ + Key? key, + required this.objId, + required this.type, + required this.isHot, + }) : controller = Get.put( + CommentListController(objId: objId, type: type, isHot: isHot), + tag: "${objId}_${type}_${isHot ? 1 : 0}", + ), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 4, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return CommentItemWidget(item); + }, + ), + ); + } +} diff --git a/lib/modules/common/comment/comment_page.dart b/lib/modules/common/comment/comment_page.dart new file mode 100644 index 0000000..80868ff --- /dev/null +++ b/lib/modules/common/comment/comment_page.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/common/comment/comment_list_view.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:get/get.dart'; + +class CommentPage extends StatelessWidget { + final int objId; + final int type; + const CommentPage({required this.objId, required this.type, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + child: TabBar( + isScrollable: true, + labelPadding: AppStyle.edgeInsetsH24, + tabAlignment: TabAlignment.start, + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).colorScheme.primary, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: const [ + Tab(text: "最新评论"), + Tab(text: "热门评论"), + ], + ), + ), + ), + body: TabBarView( + children: [ + CommentListView(objId: objId, type: type, isHot: false), + CommentListView(objId: objId, type: type, isHot: true), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + AppNavigator.toAddComment(objId: objId, type: type); + }, + child: const Icon(Icons.add), + ), + ), + ); + } +} diff --git a/lib/modules/common/download/comic/comic_download_page.dart b/lib/modules/common/download/comic/comic_download_page.dart new file mode 100644 index 0000000..be2c946 --- /dev/null +++ b/lib/modules/common/download/comic/comic_download_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/modules/common/download/comic/comic_downloaded_view.dart'; +import 'package:flutter_dmzj/modules/common/download/comic/comic_downloading_view.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:get/get.dart'; + +class ComicDownloadPage extends StatelessWidget { + final int type; + const ComicDownloadPage(this.type, {super.key}); + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + initialIndex: type, + child: Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 56), + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).colorScheme.primary, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: [ + const Tab(text: "已完成"), + Obx( + () => Tab( + text: ComicDownloadService.instance.taskQueues.isEmpty + ? "下载中" + : "下载中(${ComicDownloadService.instance.taskQueues.length})"), + ) + ], + ), + ), + ), + body: const TabBarView( + children: [ + ComicDownloadedView(), + ComicDownloadingView(), + ], + ), + ), + ); + } +} diff --git a/lib/modules/common/download/comic/comic_downloaded_detail_controller.dart b/lib/modules/common/download/comic/comic_downloaded_detail_controller.dart new file mode 100644 index 0000000..e3b7251 --- /dev/null +++ b/lib/modules/common/download/comic/comic_downloaded_detail_controller.dart @@ -0,0 +1,182 @@ +import 'dart:async'; + +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/models/db/comic_history.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class ComicDownloadedDetailController extends GetxController { + final ComicDownloadedItem info; + ComicDownloadedDetailController(this.info); + + /// 阅读记录 + Rx history = Rx(null); + + /// 更新漫画记录 + StreamSubscription? updateComicSubscription; + + /// 编辑模式 + var editMode = false.obs; + + RxSet selectItems = RxSet(); + + @override + void onInit() { + updateComicSubscription = EventBus.instance.listen( + EventBus.kUpdatedComicHistory, + (id) { + if (id == info.comicId) { + getHistory(); + } + }, + ); + + getHistory(); + + super.onInit(); + } + + @override + void onClose() { + updateComicSubscription?.cancel(); + super.onClose(); + } + + void getHistory() { + var comicHistory = DBService.instance.getComicHistory(info.comicId); + if (comicHistory != null) { + history.value = comicHistory; + history.update((val) {}); + } + } + + /// 开始/继续阅读 + void read() { + if (info.volumes.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + if (info.volumes.first.chapters.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + //查找记录 + if (history.value != null && history.value!.chapterId != 0) { + ComicDetailVolume? volume; + ComicDetailChapterItem? chapter; + for (var volumeItem in info.volumes) { + var chapterItem = volumeItem.chapters.firstWhereOrNull( + (x) => x.chapterId == history.value!.chapterId, + ); + if (chapterItem != null) { + volume = volumeItem; + chapter = chapterItem; + break; + } + } + if (volume != null && chapter != null) { + var chapters = List.from(volume.chapters); + //正序 + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + AppNavigator.toComicReader( + comicId: info.comicId, + comicTitle: info.comicName, + comicCover: info.comicCover, + chapters: chapters, + chapter: chapter, + isLongComic: info.isLongComic, + ); + } else { + SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读"); + readStart(); + } + } else { + readStart(); + } + } + + void readStart() { + //从头开始 + var volume = info.volumes.first; + var chapters = List.from(volume.chapters); + //正序 + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + var chapter = chapters.first; + AppNavigator.toComicReader( + comicId: info.comicId, + comicCover: info.comicCover, + comicTitle: info.comicName, + chapters: chapters, + chapter: chapter, + isLongComic: info.isLongComic, + ); + } + + void readChapter(ComicDetailVolume volume, ComicDetailChapterItem item) { + var chapters = List.from(volume.chapters); + //正序 + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + AppNavigator.toComicReader( + comicId: info.comicId, + comicCover: info.comicCover, + comicTitle: info.comicName, + chapters: chapters, + chapter: item, + isLongComic: info.isLongComic, + ); + } + + void toDetail() { + AppNavigator.toComicDetail(info.comicId); + } + + void toAddDownload() { + AppNavigator.toComicDownloadSelect(info.comicId); + } + + void setEditMode() { + selectItems.clear(); + editMode.value = true; + } + + void exitEditMode() { + selectItems.clear(); + editMode.value = false; + } + + var isSelectAll = false; + void selectAll() { + if (isSelectAll) { + selectItems.clear(); + isSelectAll = false; + return; + } + for (var volume in info.volumes) { + for (var chapter in volume.chapters) { + selectItems.add(chapter); + } + } + isSelectAll = true; + } + + void delete() { + for (var item in selectItems) { + ComicDownloadService.instance.deleteChapter(info.comicId, item.chapterId); + } + exitEditMode(); + SmartDialog.showToast("删除成功"); + AppNavigator.closePage(); + } + + void selectItem(ComicDetailChapterItem item) { + if (selectItems.contains(item)) { + selectItems.remove(item.chapterId); + } else { + selectItems.add(item); + } + } +} diff --git a/lib/modules/common/download/comic/comic_downloaded_detail_page.dart b/lib/modules/common/download/comic/comic_downloaded_detail_page.dart new file mode 100644 index 0000000..77f264e --- /dev/null +++ b/lib/modules/common/download/comic/comic_downloaded_detail_page.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/modules/common/download/comic/comic_downloaded_detail_controller.dart'; + +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class ComicDownloadedDetailPage extends StatelessWidget { + final ComicDownloadedItem info; + final ComicDownloadedDetailController controller; + ComicDownloadedDetailPage(this.info, {super.key}) + : controller = Get.put( + ComicDownloadedDetailController(info), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(info.comicName), + ), + body: ListView.builder( + padding: AppStyle.edgeInsetsA12, + itemCount: info.volumes.length, + itemBuilder: (_, i) { + var item = info.volumes[i]; + return _buildChapters(item); + }, + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Obx( + () => Column( + children: [ + Visibility( + visible: !controller.editMode.value, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.setEditMode, + icon: const Icon( + Remix.checkbox_line, + size: 20, + ), + label: const Text("选择"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.toDetail, + icon: const Icon( + Remix.information_line, + size: 20, + ), + label: const Text("详情"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.toAddDownload, + icon: const Icon( + Remix.add_line, + size: 20, + ), + label: const Text("追加"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.read, + icon: const Icon( + Remix.play_line, + size: 20, + ), + label: const Text("阅读"), + ), + ), + ], + ), + ), + Visibility( + visible: controller.editMode.value, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.selectAll, + icon: const Icon( + Remix.checkbox_line, + size: 20, + ), + label: const Text("全选"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.delete, + icon: const Icon( + Remix.delete_bin_line, + size: 20, + ), + label: const Text("删除"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.exitEditMode, + icon: const Icon( + Remix.close_line, + size: 20, + ), + label: const Text("取消"), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildChapters(ComicDetailVolume item) { + return Obx( + () => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: AppStyle.edgeInsetsV8, + child: Row( + children: [ + Expanded( + child: Text( + "${item.title}(共${item.chapters.length}话)", + style: Get.textTheme.titleSmall, + ), + ), + item.sortType.value == 1 + ? TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + item.sortType.value = 0; + item.sort(); + }, + icon: const Icon( + Remix.sort_asc, + size: 20, + ), + label: const Text("升序"), + ) + : TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + item.sortType.value = 1; + item.sort(); + }, + icon: const Icon( + Remix.sort_desc, + size: 20, + ), + label: const Text("倒序"), + ), + ], + ), + ), + LayoutBuilder(builder: (ctx, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + + return Obx( + () => MasonryGridView.count( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemCount: (item.showMoreButton && !item.showAll.value) + ? 15 + : item.chapters.length, + itemBuilder: (_, i) { + if (item.showMoreButton && !item.showAll.value && i == 14) { + return Tooltip( + message: "展开全部章节", + child: OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: Colors.grey, + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size.fromHeight(40), + ), + onPressed: () { + item.showAll.value = true; + }, + child: const Icon(Icons.arrow_drop_down), + ), + ); + } + var chapter = item.chapters[i]; + + return Tooltip( + message: chapter.chapterTitle, + child: Obx( + () => controller.editMode.value + ? OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: + controller.selectItems.contains(chapter) + ? Get.theme.colorScheme.primary + : Get.textTheme.bodyMedium!.color, + side: controller.selectItems.contains(chapter) + ? BorderSide(color: Get.theme.colorScheme.primary) + : null, + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size.fromHeight(40), + ), + onPressed: () { + controller.selectItem(chapter); + }, + child: Text( + item.chapters[i].chapterTitle, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ) + : OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: item.chapters[i].chapterId == + controller.history.value?.chapterId + ? Get.theme.colorScheme.primary + : Get.textTheme.bodyMedium!.color, + textStyle: const TextStyle(fontSize: 14), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size.fromHeight(40), + ), + onPressed: () { + controller.readChapter(item, chapter); + }, + child: Text( + item.chapters[i].chapterTitle, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ); + }, + crossAxisCount: count, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + ); + }) + ], + ), + ); + } +} diff --git a/lib/modules/common/download/comic/comic_downloaded_view.dart b/lib/modules/common/download/comic/comic_downloaded_view.dart new file mode 100644 index 0000000..a3e4576 --- /dev/null +++ b/lib/modules/common/download/comic/comic_downloaded_view.dart @@ -0,0 +1,89 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart'; +import 'package:get/get.dart'; + +class ComicDownloadedView extends StatelessWidget { + const ComicDownloadedView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => Stack( + children: [ + EasyRefresh( + header: const MaterialHeader(), + onRefresh: () async { + ComicDownloadService.instance.updateDownlaoded(); + }, + child: ListView.separated( + itemCount: ComicDownloadService.instance.downloaded.length, + separatorBuilder: (_, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (_, i) { + var item = ComicDownloadService.instance.downloaded[i]; + return buildItem(item); + }, + ), + ), + Offstage( + offstage: ComicDownloadService.instance.downloaded.isNotEmpty, + child: AppEmptyWidget( + onRefresh: () { + ComicDownloadService.instance.updateDownlaoded(); + }, + ), + ), + ], + ), + ); + } + + Widget buildItem(ComicDownloadedItem item) { + return InkWell( + onTap: () { + AppNavigator.toComicDownloadDetail(item); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.comicCover, + width: 60, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.comicName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text( + "已下载${item.chapterCount}章", + style: const TextStyle(color: Colors.grey, fontSize: 14), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/common/download/comic/comic_downloading_view.dart b/lib/modules/common/download/comic/comic_downloading_view.dart new file mode 100644 index 0000000..89272c1 --- /dev/null +++ b/lib/modules/common/download/comic/comic_downloading_view.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/services/download_task/comic_downloader.dart'; +import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class ComicDownloadingView extends StatelessWidget { + const ComicDownloadingView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: Obx( + () => Stack( + children: [ + ListView.separated( + itemCount: ComicDownloadService.instance.taskQueues.length, + separatorBuilder: (_, i) => const Divider( + height: 1, + ), + itemBuilder: (_, i) { + var task = ComicDownloadService.instance.taskQueues[i]; + return buildItem(task); + }, + ), + Offstage( + offstage: ComicDownloadService.instance.taskQueues.isNotEmpty, + child: const AppEmptyWidget(), + ), + ], + ), + ), + ), + BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: ComicDownloadService.instance.pauseAll, + icon: const Icon( + Remix.pause_line, + size: 20, + ), + label: const Text("暂停全部"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: ComicDownloadService.instance.resumeAll, + icon: const Icon( + Remix.download_line, + size: 20, + ), + label: const Text("开始全部"), + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget buildItem(ComicDownloader task) { + return Obx( + () => Padding( + padding: AppStyle.edgeInsetsA12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "${task.info.value.volumeName} - ${task.info.value.chapterName}", + ), + Text( + task.info.value.comicName, + style: Get.textTheme.bodySmall, + ), + Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: AppStyle.radius4, + child: LinearProgressIndicator( + value: task.info.value.total > 0 + ? (task.info.value.index + 1) / task.info.value.total + : 0, + ), + ), + ), + AppStyle.hGap8, + Text( + "${task.info.value.index + 1}/${task.info.value.total}", + style: Get.textTheme.bodySmall, + ), + ], + ), + Row( + children: [ + Expanded( + child: Text( + parseStatus(task.info.value.status), + style: Get.textTheme.bodySmall, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + buildButton( + icon: Icons.refresh_rounded, + text: "重试", + visible: task.status == DownloadStatus.error || + task.status == DownloadStatus.errorLoad, + onPressed: () { + task.retry(); + }, + ), + buildButton( + icon: Icons.play_arrow_rounded, + visible: task.status == DownloadStatus.wait || + task.status == DownloadStatus.pauseCellular, + text: "开始", + onPressed: () { + task.start(); + }, + ), + buildButton( + icon: Icons.play_arrow_rounded, + visible: task.status == DownloadStatus.pause, + text: "继续", + onPressed: () { + task.resume(); + }, + ), + buildButton( + icon: Icons.pause_rounded, + visible: task.status == DownloadStatus.downloading, + text: "暂停", + onPressed: () { + task.pause(); + }, + ), + buildButton( + icon: Icons.cancel_outlined, + text: "取消", + onPressed: () { + task.cancel(); + }, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } + + String parseStatus(DownloadStatus status) { + switch (status) { + case DownloadStatus.cancel: + return "已取消"; + case DownloadStatus.complete: + return "已完成"; + case DownloadStatus.downloading: + return "下载中"; + case DownloadStatus.error: + return "下载失败"; + case DownloadStatus.errorLoad: + return "无法读取信息"; + case DownloadStatus.loadding: + return "读取信息中"; + case DownloadStatus.pause: + return "暂停中"; + case DownloadStatus.pauseCellular: + return "等待Wi-Fi"; + case DownloadStatus.wait: + return "等待下载"; + case DownloadStatus.waitNetwork: + return "等待网络连接"; + default: + return status.toString(); + } + } + + Widget buildButton({ + required String text, + required IconData icon, + Function()? onPressed, + bool visible = true, + }) { + return Visibility( + visible: visible, + child: Padding( + padding: AppStyle.edgeInsetsL4, + child: TextButton.icon( + style: TextButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: onPressed, + icon: Icon( + icon, + size: 16, + ), + label: Text(text), + ), + ), + ); + } +} diff --git a/lib/modules/common/download/novel/novel_download_page.dart b/lib/modules/common/download/novel/novel_download_page.dart new file mode 100644 index 0000000..de8d805 --- /dev/null +++ b/lib/modules/common/download/novel/novel_download_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/modules/common/download/novel/novel_downloaded_view.dart'; +import 'package:flutter_dmzj/modules/common/download/novel/novel_downloading_view.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:get/get.dart'; + +class NovelDownloadPage extends StatelessWidget { + final int type; + const NovelDownloadPage(this.type, {super.key}); + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + initialIndex: type, + child: Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 56), + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).colorScheme.primary, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: [ + const Tab(text: "已完成"), + Obx( + () => Tab( + text: NovelDownloadService.instance.taskQueues.isEmpty + ? "下载中" + : "下载中(${NovelDownloadService.instance.taskQueues.length})"), + ) + ], + ), + ), + ), + body: const TabBarView( + children: [ + NovelDownloadedView(), + NovelDownloadingView(), + ], + ), + ), + ); + } +} diff --git a/lib/modules/common/download/novel/novel_downloaded_detail_controller.dart b/lib/modules/common/download/novel/novel_downloaded_detail_controller.dart new file mode 100644 index 0000000..9b375d5 --- /dev/null +++ b/lib/modules/common/download/novel/novel_downloaded_detail_controller.dart @@ -0,0 +1,182 @@ +import 'dart:async'; + +import 'package:flutter_dmzj/app/event_bus.dart'; + +import 'package:flutter_dmzj/models/db/novel_history.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class NovelDownloadedDetailController extends GetxController { + final NovelDownloadedItem info; + NovelDownloadedDetailController(this.info); + + /// 阅读记录 + Rx history = Rx(null); + + /// 更新漫画记录 + StreamSubscription? updateNovelSubscription; + + /// 编辑模式 + var editMode = false.obs; + + RxSet selectItems = RxSet(); + + @override + void onInit() { + updateNovelSubscription = EventBus.instance.listen( + EventBus.kUpdatedNovelHistory, + (id) { + if (id == info.novelId) { + getHistory(); + } + }, + ); + + getHistory(); + + super.onInit(); + } + + @override + void onClose() { + updateNovelSubscription?.cancel(); + super.onClose(); + } + + void getHistory() { + var novelHistory = DBService.instance.getNovelHistory(info.novelId); + if (novelHistory != null) { + history.value = novelHistory; + history.update((val) {}); + } + } + + /// 开始/继续阅读 + void read() { + if (info.volumes.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + if (info.volumes.first.chapters.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + //查找记录 + if (history.value != null && history.value!.chapterId != 0) { + NovelDetailChapter? chapter; + for (var volumeItem in info.volumes) { + var chapterItem = volumeItem.chapters.firstWhereOrNull( + (x) => x.chapterId == history.value!.chapterId, + ); + if (chapterItem != null) { + chapter = chapterItem; + break; + } + } + + if (chapter != null) { + List chapters = []; + for (var volume in info.volumes) { + chapters.addAll(volume.chapters); + } + + AppNavigator.toNovelReader( + novelId: info.novelId, + novelCover: info.novelCover, + novelTitle: info.novelName, + chapter: chapter, + chapters: chapters, + ); + } else { + SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读"); + readStart(); + } + } else { + readStart(); + } + } + + void readStart() { + //从头开始 + List chapters = []; + for (var volume in info.volumes) { + chapters.addAll(volume.chapters); + } + var chapter = chapters.first; + AppNavigator.toNovelReader( + novelId: info.novelId, + novelCover: info.novelCover, + novelTitle: info.novelName, + chapter: chapter, + chapters: chapters, + ); + } + + void readChapter(NovelDetailVolume volume, NovelDetailChapter item) { + List chapters = []; + for (var volume in info.volumes) { + chapters.addAll(volume.chapters); + } + + AppNavigator.toNovelReader( + novelId: info.novelId, + novelCover: info.novelCover, + novelTitle: info.novelName, + chapters: chapters, + chapter: item, + ); + } + + void toDetail() { + AppNavigator.toNovelDetail(info.novelId); + } + + void toAddDownload() { + AppNavigator.toNovelDownloadSelect(info.novelId); + } + + void setEditMode() { + selectItems.clear(); + editMode.value = true; + } + + void exitEditMode() { + selectItems.clear(); + editMode.value = false; + } + + var isSelectAll = false; + void selectAll() { + if (isSelectAll) { + selectItems.clear(); + isSelectAll = false; + return; + } + for (var volume in info.volumes) { + selectItems.addAll(volume.chapters); + } + isSelectAll = true; + } + + void delete() { + for (var item in selectItems) { + NovelDownloadService.instance + .deleteChapter(info.novelId, item.volumeId, item.chapterId); + } + exitEditMode(); + SmartDialog.showToast("删除成功"); + AppNavigator.closePage(); + } + + void selectItem(NovelDetailChapter item) { + if (selectItems.contains(item)) { + selectItems.remove(item.chapterId); + } else { + selectItems.add(item); + } + } +} diff --git a/lib/modules/common/download/novel/novel_downloaded_detail_page.dart b/lib/modules/common/download/novel/novel_downloaded_detail_page.dart new file mode 100644 index 0000000..e15bbb0 --- /dev/null +++ b/lib/modules/common/download/novel/novel_downloaded_detail_page.dart @@ -0,0 +1,289 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; +import 'package:flutter_dmzj/modules/common/download/novel/novel_downloaded_detail_controller.dart'; + +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class NovelDownloadedDetailPage extends StatelessWidget { + final NovelDownloadedItem info; + final NovelDownloadedDetailController controller; + NovelDownloadedDetailPage(this.info, {super.key}) + : controller = Get.put( + NovelDownloadedDetailController(info), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(info.novelName), + ), + body: ListView.builder( + padding: AppStyle.edgeInsetsA12, + itemCount: info.volumes.length, + itemBuilder: (_, i) { + var item = info.volumes[i]; + return _buildChapters(item); + }, + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Obx( + () => Column( + children: [ + Visibility( + visible: !controller.editMode.value, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.setEditMode, + icon: const Icon( + Remix.checkbox_line, + size: 20, + ), + label: const Text("选择"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.toDetail, + icon: const Icon( + Remix.information_line, + size: 20, + ), + label: const Text("详情"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.toAddDownload, + icon: const Icon( + Remix.add_line, + size: 20, + ), + label: const Text("追加"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.read, + icon: const Icon( + Remix.play_line, + size: 20, + ), + label: const Text("阅读"), + ), + ), + ], + ), + ), + Visibility( + visible: controller.editMode.value, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.selectAll, + icon: const Icon( + Remix.checkbox_line, + size: 20, + ), + label: const Text("全选"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.delete, + icon: const Icon( + Remix.delete_bin_line, + size: 20, + ), + label: const Text("删除"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.exitEditMode, + icon: const Icon( + Remix.close_line, + size: 20, + ), + label: const Text("取消"), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildChapters(NovelDetailVolume item) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: AppStyle.edgeInsetsV8, + child: Row(children: [ + Expanded( + child: Text( + "${item.volumeName}(共${item.chapters.length}话)", + style: Get.textTheme.titleSmall, + ), + ), + ]), + ), + ListView.separated( + itemCount: item.chapters.length, + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (_, i) => const Divider( + height: 1, + ), + itemBuilder: (_, i) { + var chapter = item.chapters[i]; + + return Obx( + () => controller.editMode.value + ? CheckboxListTile( + title: Text( + chapter.chapterName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Get.textTheme.bodyMedium!.copyWith( + color: controller.history.value?.chapterId == + chapter.chapterId + ? Get.theme.colorScheme.primary + : null, + ), + ), + contentPadding: AppStyle.edgeInsetsA4, + visualDensity: const VisualDensity( + vertical: VisualDensity.minimumDensity), + value: controller.selectItems.contains(chapter), + onChanged: (e) { + controller.selectItem(chapter); + }, + ) + : ListTile( + title: Text( + chapter.chapterName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Get.textTheme.bodyMedium!.copyWith( + color: controller.history.value?.chapterId == + chapter.chapterId + ? Get.theme.colorScheme.primary + : null, + ), + ), + contentPadding: AppStyle.edgeInsetsA4, + visualDensity: const VisualDensity( + vertical: VisualDensity.minimumDensity), + onTap: () { + controller.readChapter(item, chapter); + }, + ), + ); + }) + // LayoutBuilder(builder: (ctx, constraints) { + // var count = constraints.maxWidth ~/ 160; + // if (count < 3) count = 3; + + // return MasonryGridView.count( + // shrinkWrap: true, + // padding: EdgeInsets.zero, + // physics: const NeverScrollableScrollPhysics(), + // itemCount: item.chapters.length, + // itemBuilder: (_, i) { + // var chapter = item.chapters[i]; + + // return Tooltip( + // message: chapter.chapterName, + // child: Obx( + // () => controller.editMode.value + // ? OutlinedButton( + // style: OutlinedButton.styleFrom( + // foregroundColor: + // controller.selectItems.contains(chapter) + // ? Colors.blue + // : Get.textTheme.bodyMedium!.color, + // side: controller.selectItems.contains(chapter) + // ? const BorderSide(color: Colors.blue) + // : null, + // textStyle: const TextStyle(fontSize: 14), + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // minimumSize: const Size.fromHeight(40), + // ), + // onPressed: () { + // controller.selectItem(chapter); + // }, + // child: Text( + // item.chapters[i].chapterName, + // textAlign: TextAlign.center, + // overflow: TextOverflow.ellipsis, + // ), + // ) + // : OutlinedButton( + // style: OutlinedButton.styleFrom( + // foregroundColor: item.chapters[i].chapterId == + // controller.history.value?.chapterId + // ? Colors.blue + // : Get.textTheme.bodyMedium!.color, + // textStyle: const TextStyle(fontSize: 14), + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // minimumSize: const Size.fromHeight(40), + // ), + // onPressed: () { + // controller.readChapter(item, chapter); + // }, + // child: Text( + // item.chapters[i].chapterName, + // textAlign: TextAlign.center, + // overflow: TextOverflow.ellipsis, + // ), + // ), + // ), + // ); + // }, + // crossAxisCount: count, + // crossAxisSpacing: 8, + // mainAxisSpacing: 8, + // ); + // }) + ], + ); + } +} diff --git a/lib/modules/common/download/novel/novel_downloaded_view.dart b/lib/modules/common/download/novel/novel_downloaded_view.dart new file mode 100644 index 0000000..36ab315 --- /dev/null +++ b/lib/modules/common/download/novel/novel_downloaded_view.dart @@ -0,0 +1,89 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart'; +import 'package:get/get.dart'; + +class NovelDownloadedView extends StatelessWidget { + const NovelDownloadedView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => Stack( + children: [ + EasyRefresh( + header: const MaterialHeader(), + onRefresh: () async { + NovelDownloadService.instance.updateDownlaoded(); + }, + child: ListView.separated( + itemCount: NovelDownloadService.instance.downloaded.length, + separatorBuilder: (_, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (_, i) { + var item = NovelDownloadService.instance.downloaded[i]; + return buildItem(item); + }, + ), + ), + Offstage( + offstage: NovelDownloadService.instance.downloaded.isNotEmpty, + child: AppEmptyWidget( + onRefresh: () { + NovelDownloadService.instance.updateDownlaoded(); + }, + ), + ), + ], + ), + ); + } + + Widget buildItem(NovelDownloadedItem item) { + return InkWell( + onTap: () { + AppNavigator.toNovelDownloadDetail(item); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.novelCover, + width: 60, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.novelName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text( + "已下载${item.chapterCount}章", + style: const TextStyle(color: Colors.grey, fontSize: 14), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/common/download/novel/novel_downloading_view.dart b/lib/modules/common/download/novel/novel_downloading_view.dart new file mode 100644 index 0000000..e5bed34 --- /dev/null +++ b/lib/modules/common/download/novel/novel_downloading_view.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_dmzj/services/download_task/novel_downloader.dart'; +import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class NovelDownloadingView extends StatelessWidget { + const NovelDownloadingView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: Obx( + () => Stack( + children: [ + ListView.separated( + itemCount: NovelDownloadService.instance.taskQueues.length, + separatorBuilder: (_, i) => const Divider( + height: 1, + ), + itemBuilder: (_, i) { + var task = NovelDownloadService.instance.taskQueues[i]; + return buildItem(task); + }, + ), + Offstage( + offstage: NovelDownloadService.instance.taskQueues.isNotEmpty, + child: const AppEmptyWidget(), + ), + ], + ), + ), + ), + BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: NovelDownloadService.instance.pauseAll, + icon: const Icon( + Remix.pause_line, + size: 20, + ), + label: const Text("暂停全部"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: NovelDownloadService.instance.resumeAll, + icon: const Icon( + Remix.download_line, + size: 20, + ), + label: const Text("开始全部"), + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget buildItem(NovelDownloader task) { + return Obx( + () => Padding( + padding: AppStyle.edgeInsetsA12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "${task.info.value.volumeName} - ${task.info.value.chapterName}", + ), + Text( + task.info.value.novelName, + style: Get.textTheme.bodySmall, + ), + Row( + children: [ + Expanded( + child: Text( + parseStatus(task.info.value.status), + style: Get.textTheme.bodySmall, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + buildButton( + icon: Icons.refresh_rounded, + text: "重试", + visible: task.status == DownloadStatus.error || + task.status == DownloadStatus.errorLoad, + onPressed: () { + task.retry(); + }, + ), + buildButton( + icon: Icons.play_arrow_rounded, + visible: task.status == DownloadStatus.wait || + task.status == DownloadStatus.pauseCellular, + text: "开始", + onPressed: () { + task.start(); + }, + ), + buildButton( + icon: Icons.play_arrow_rounded, + visible: task.status == DownloadStatus.pause, + text: "继续", + onPressed: () { + task.resume(); + }, + ), + buildButton( + icon: Icons.pause_rounded, + visible: task.status == DownloadStatus.downloading, + text: "暂停", + onPressed: () { + task.pause(); + }, + ), + buildButton( + icon: Icons.cancel_outlined, + text: "取消", + onPressed: () { + task.cancel(); + }, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } + + String parseStatus(DownloadStatus status) { + switch (status) { + case DownloadStatus.cancel: + return "已取消"; + case DownloadStatus.complete: + return "已完成"; + case DownloadStatus.downloading: + return "下载中"; + case DownloadStatus.error: + return "下载失败"; + case DownloadStatus.errorLoad: + return "无法读取信息"; + case DownloadStatus.loadding: + return "读取信息中"; + case DownloadStatus.pause: + return "暂停中"; + case DownloadStatus.pauseCellular: + return "等待Wi-Fi"; + case DownloadStatus.wait: + return "等待下载"; + case DownloadStatus.waitNetwork: + return "等待网络连接"; + default: + return status.toString(); + } + } + + Widget buildButton({ + required String text, + required IconData icon, + Function()? onPressed, + bool visible = true, + }) { + return Visibility( + visible: visible, + child: Padding( + padding: AppStyle.edgeInsetsL4, + child: TextButton.icon( + style: TextButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: onPressed, + icon: Icon( + icon, + size: 16, + ), + label: Text(text), + ), + ), + ); + } +} diff --git a/lib/modules/common/empty_page.dart b/lib/modules/common/empty_page.dart new file mode 100644 index 0000000..bab1279 --- /dev/null +++ b/lib/modules/common/empty_page.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; + +class EmptyPage extends StatelessWidget { + const EmptyPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MediaQuery.of(context).size.width <= AppConstant.kTabletWidth + ? Container() + : Scaffold( + resizeToAvoidBottomInset: false, + body: Center( + child: Image.asset( + "assets/images/logo_dmzj.png", + height: 80, + ), + ), + ); + } +} diff --git a/lib/modules/common/test_subroute_page.dart b/lib/modules/common/test_subroute_page.dart new file mode 100644 index 0000000..0fef260 --- /dev/null +++ b/lib/modules/common/test_subroute_page.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +class TestSubRoutePage extends StatelessWidget { + const TestSubRoutePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("测试路由"), + leading: IconButton( + onPressed: () { + AppNavigator.closePage(); + }, + icon: const Icon(Icons.arrow_back), + ), + ), + body: Center( + child: ElevatedButton( + child: const Text("Back"), + onPressed: () { + AppNavigator.closePage(); + }, + ), + ), + ); + } +} diff --git a/lib/modules/common/webview/webview_controller.dart b/lib/modules/common/webview/webview_controller.dart new file mode 100644 index 0000000..7a5f0f0 --- /dev/null +++ b/lib/modules/common/webview/webview_controller.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:get/get.dart'; + +import 'package:webview_flutter/webview_flutter.dart'; + +class WebViewPageController extends BaseController { + final String url; + WebViewPageController(this.url); + final WebViewController webViewController = WebViewController(); + var title = "加载中".obs; + @override + void onInit() { + initWebView(); + super.onInit(); + } + + void initWebView() async { + webViewController.setJavaScriptMode(JavaScriptMode.unrestricted); + + webViewController.setBackgroundColor( + Get.isDarkMode ? Colors.black : AppColor.backgroundColor); + webViewController.setNavigationDelegate( + NavigationDelegate( + onPageStarted: (String url) { + pageLoadding.value = true; + }, + onPageFinished: (String url) async { + pageLoadding.value = false; + title.value = (await webViewController.getTitle()) ?? ""; + }, + onNavigationRequest: (NavigationRequest request) { + var uri = Uri.parse(request.url); + Log.d(request.url); + if (uri.scheme == "https" || uri.scheme == "http") { + return NavigationDecision.navigate; + } + + return NavigationDecision.prevent; + }, + ), + ); + webViewController.loadRequest(Uri.parse(url), headers: { + "Cookie": UserService.instance.userProfile.value?.cookieVal ?? "", + }); + + /// TODO 无法加载Mixed Content + /// 19年的问题了,Flutter还没解决... + /// https://github.com/flutter/flutter/issues/43595 + } + + void refreshWeb() { + webViewController.reload(); + } +} diff --git a/lib/modules/common/webview/webview_page.dart b/lib/modules/common/webview/webview_page.dart new file mode 100644 index 0000000..eee869b --- /dev/null +++ b/lib/modules/common/webview/webview_page.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/modules/common/webview/webview_controller.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebViewPage extends StatelessWidget { + final String url; + final WebViewPageController controller; + WebViewPage({required this.url, Key? key}) + : controller = Get.put( + WebViewPageController(url), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ), + super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx(() => Text(controller.title.value)), + ), + body: Stack( + children: [ + Obx( + () => Offstage( + offstage: controller.pageLoadding.value, + child: WebViewWidget( + controller: controller.webViewController, + ), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.refreshWeb(), + ), + ), + ), + ], + ), + bottomNavigationBar: Stack( + children: [ + BottomAppBar( + child: SizedBox( + height: 56, + child: Row( + children: [ + Expanded( + child: IconButton( + onPressed: () { + controller.webViewController.goBack(); + }, + icon: const Icon( + Icons.chevron_left, + ), + ), + ), + Expanded( + child: IconButton( + onPressed: () { + controller.webViewController.reload(); + }, + icon: const Icon( + Icons.refresh, + ), + ), + ), + Expanded( + child: IconButton( + onPressed: () { + controller.webViewController.goForward(); + }, + icon: const Icon( + Icons.chevron_right, + ), + ), + ), + Expanded( + child: IconButton( + onPressed: () async { + Utils.share( + (await controller.webViewController.currentUrl()) + .toString(), + ); + }, + icon: const Icon( + Icons.share, + size: 20, + ), + ), + ), + Expanded( + child: IconButton( + onPressed: () async { + var url = + await controller.webViewController.currentUrl(); + if (url != null) { + launchUrlString(url, + mode: LaunchMode.externalApplication); + } + }, + icon: const Icon( + Icons.open_in_browser, + ), + ), + ), + ], + ), + ), + ), + Positioned.fill( + top: 0, + left: 0, + child: Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: Container( + alignment: Alignment.topLeft, + child: const LinearProgressIndicator(), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/modules/index/index_controller.dart b/lib/modules/index/index_controller.dart new file mode 100644 index 0000000..fac0f56 --- /dev/null +++ b/lib/modules/index/index_controller.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/modules/comic/home/comic_home_page.dart'; +import 'package:flutter_dmzj/modules/news/home/news_home_controller.dart'; +import 'package:flutter_dmzj/modules/news/home/news_home_page.dart'; +import 'package:flutter_dmzj/modules/novel/home/novel_home_controller.dart'; +import 'package:flutter_dmzj/modules/novel/home/novel_home_page.dart'; +import 'package:flutter_dmzj/modules/user/user_home_page.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:multi_split_view/multi_split_view.dart'; + +class IndexController extends GetxController { + final index = 0.obs; + final showContent = false.obs; + final GlobalKey indexKey = GlobalKey(); + final GlobalKey subRouterKey = GlobalKey(); + + final MultiSplitViewController multiSplitViewController = + MultiSplitViewController(areas: [ + Area(minimalSize: 400, size: 500), + ]); + + /// 双击退出Flag + bool doubleClickExit = false; + + /// 双击退出Timer + Timer? doubleClickTimer; + + final pages = [ + const ComicHomePage(), + const SizedBox(), + const SizedBox(), + const UserHomePage(), + ]; + @override + void onInit() { + if (PlatformUtils.isWindows) { + // Windows: 预先初始化所有分区控制器,确保NavigationView所有PaneItem.body可用 + if (!Get.isRegistered()) { + Get.put(NewsHomeController()); + } + if (!Get.isRegistered()) { + Get.put(NovelHomeController()); + } + pages[1] = const NewsHomePage(); + pages[2] = const NovelHomePage(); + } + Future.delayed(Duration.zero, showFirstRun); + super.onInit(); + } + + @override + void onClose() {} + + void setIndex(i) { + if (i == 1 && pages[i] is SizedBox) { + Get.put(NewsHomeController()); + pages[i] = const NewsHomePage(); + } else if (i == 2 && pages[i] is SizedBox) { + Get.put(NovelHomeController()); + pages[i] = const NovelHomePage(); + } + if (index.value == i) { + EventBus.instance.emit(EventBus.kBottomNavigationBarClicked, i); + } + index.value = i; + } + + void showFirstRun() async { + if (AppSettingsService.instance.firstRun) { + AppSettingsService.instance.setNoFirstRun(); + DialogUtils.showStatement(); + Utils.checkUpdate(); + } else { + Utils.checkUpdate(); + } + } + + void setDoubleExitFlag() { + if (doubleClickExit) { + doubleClickTimer?.cancel(); + Get.back(); + return; + } + doubleClickExit = true; + SmartDialog.showToast("再按一次退出应用"); + doubleClickTimer = Timer(const Duration(seconds: 2), () { + doubleClickExit = false; + doubleClickTimer!.cancel(); + }); + } +} diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart new file mode 100644 index 0000000..399e249 --- /dev/null +++ b/lib/modules/index/index_page.dart @@ -0,0 +1,223 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/modules/common/empty_page.dart'; +import 'package:flutter_dmzj/modules/index/index_controller.dart'; +import 'package:flutter_dmzj/modules/index/windows_index_page.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/routes/app_pages.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class IndexPage extends GetView { + const IndexPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // Windows平台使用Fluent UI的NavigationView + if (PlatformUtils.isWindows) { + return const WindowsIndexPage(); + } + final content = _buildContentNavigator(); + final indexStack = _buildIndexStack(); + return OrientationBuilder( + builder: (context, orientation) { + return orientation == Orientation.landscape + ? _buildWide(context, indexStack, content) + : _buildNarrow(context, indexStack, content); + }, + ); + } + + Widget _buildNarrow(BuildContext context, Widget indexStack, Widget content) { + return Stack( + children: [ + Obx( + () => Scaffold( + body: indexStack, + bottomNavigationBar: NavigationBar( + selectedIndex: controller.index.value, + onDestinationSelected: controller.setIndex, + destinations: const [ + NavigationDestination( + icon: Icon(Remix.bear_smile_line), + selectedIcon: Icon(Remix.bear_smile_fill), + label: "漫画", + ), + NavigationDestination( + icon: Icon(Remix.article_line), + selectedIcon: Icon(Remix.article_fill), + label: "资讯", + ), + NavigationDestination( + icon: Icon(Remix.book_open_line), + selectedIcon: Icon(Remix.book_open_fill), + label: "轻小说", + ), + NavigationDestination( + icon: Icon(Remix.user_smile_line), + selectedIcon: Icon(Remix.user_smile_fill), + label: "我的", + ), + ], + ), + ), + ), + Obx( + () => IgnorePointer( + ignoring: !controller.showContent.value, + child: content, + ), + ) + ], + ); + } + + Widget _buildWide(BuildContext context, Widget indexStack, Widget content) { + return Scaffold( + body: Row( + children: [ + Obx( + () => Padding( + padding: const EdgeInsets.only(right: 2), + child: NavigationRail( + elevation: 2, + labelType: NavigationRailLabelType.all, + onDestinationSelected: controller.setIndex, + selectedIndex: controller.index.value, + leading: SizedBox( + height: AppStyle.statusBarHeight, + ), + selectedLabelTextStyle: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.secondary, + ), + unselectedLabelTextStyle: TextStyle( + fontSize: 10, + color: Theme.of(context).textTheme.bodyLarge?.color, + ), + destinations: const [ + NavigationRailDestination( + icon: Icon(Remix.bear_smile_line), + label: Text("漫画"), + ), + NavigationRailDestination( + icon: Icon(Remix.article_line), + label: Text("资讯"), + ), + NavigationRailDestination( + icon: Icon(Remix.book_open_line), + label: Text("轻小说"), + ), + NavigationRailDestination( + icon: Icon(Remix.user_smile_line), + label: Text("我的"), + ), + ], + ), + ), + ), + Container( + // constraints: const BoxConstraints(maxWidth: 450), + width: 450, + decoration: BoxDecoration( + border: Border( + right: BorderSide( + color: Colors.grey.withOpacity(.1), + ), + ), + ), + child: indexStack, + ), + Expanded( + child: content, + ), + ], + ), + ); + } + + Widget _buildIndexStack() { + return Obx( + () => IndexedStack( + key: controller.indexKey, + index: controller.index.value, + children: controller.pages, + ), + ); + } + + /// 子路由 + Widget _buildContentNavigator() { + /// 拦截子路由的返回 + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (!didPop) { + if (Navigator.canPop(Get.context!)) { + Get.back(); + return; + } else if (AppNavigator.subNavigatorKey!.currentState!.canPop()) { + AppNavigator.subNavigatorKey!.currentState!.pop(); + return; + } + + if (controller.doubleClickExit) { + controller.doubleClickTimer?.cancel(); + SystemNavigator.pop(); + return; + } + controller.setDoubleExitFlag(); + } + }, + // onWillPop: () async { + // if (Navigator.canPop(Get.context!)) { + // return true; + // } + // if (AppNavigator.subNavigatorKey!.currentState!.canPop()) { + // AppNavigator.subNavigatorKey!.currentState!.pop(); + // return false; + // } + // return true; + // }, + child: ClipRect( + child: Navigator( + key: AppNavigator.subNavigatorKey, + initialRoute: '/', + onUnknownRoute: (settings) => GetPageRoute( + page: () => const EmptyPage(), + ), + observers: [ + SubNavigatorObserver(), + ], + onGenerateRoute: AppPages.generateSubRoute, + ), + ), + ); + } +} + +/// 子路由监听 +class SubNavigatorObserver extends NavigatorObserver { + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + if (previousRoute != null) { + var routeName = route.settings.name ?? ""; + AppNavigator.currentContentRouteName = routeName; + Get.find().showContent.value = routeName != '/'; + } + } + + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + + var routeName = previousRoute?.settings.name ?? ""; + AppNavigator.currentContentRouteName = routeName; + Get.find().showContent.value = routeName != '/'; + } +} diff --git a/lib/modules/index/windows_index_page.dart b/lib/modules/index/windows_index_page.dart new file mode 100644 index 0000000..f38896a --- /dev/null +++ b/lib/modules/index/windows_index_page.dart @@ -0,0 +1,150 @@ +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/modules/common/empty_page.dart'; +import 'package:flutter_dmzj/modules/index/index_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/routes/app_pages.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +/// Windows平台专用导航页面 - 使用Fluent UI的NavigationView +class WindowsIndexPage extends GetView { + const WindowsIndexPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return fluent.FluentTheme( + data: PlatformUtils.getFluentTheme(context), + child: Obx( + () => fluent.NavigationView( + paneBodyBuilder: (item, body) { + // Builder ensures ctx is INSIDE the FluentTheme ancestor tree + return Builder( + builder: (ctx) => KeyedSubtree( + key: const ValueKey('windows_main_content'), + child: _buildMasterDetail(ctx), + ), + ); + }, + pane: fluent.NavigationPane( + selected: controller.index.value, + onChanged: controller.setIndex, + displayMode: fluent.PaneDisplayMode.auto, + indicator: const fluent.StickyNavigationIndicator(), + items: [ + fluent.PaneItem( + icon: const Icon(Remix.bear_smile_line), + title: const Text('漫画'), + body: const SizedBox.shrink(), + ), + fluent.PaneItem( + icon: const Icon(Remix.article_line), + title: const Text('资讯'), + body: const SizedBox.shrink(), + ), + fluent.PaneItem( + icon: const Icon(Remix.book_open_line), + title: const Text('轻小说'), + body: const SizedBox.shrink(), + ), + ], + footerItems: [ + fluent.PaneItem( + icon: const Icon(Remix.user_smile_line), + title: const Text('我的'), + body: const SizedBox.shrink(), + ), + ], + ), + ), + ), + ); + } + + /// 主内容区:section列表(左) + 子路由详情(右) + /// 使用Material主题颜色,避免FluentTheme.of()需要特定祖先 + Widget _buildMasterDetail(BuildContext context) { + final materialTheme = Theme.of(context); + final isDark = materialTheme.brightness == Brightness.dark; + // 使用Material主题颜色衍生背景色 + final scaffoldBg = materialTheme.scaffoldBackgroundColor; + final panelBg = isDark ? const Color(0xff202020) : const Color(0xfff0f0f0); + final dividerColor = materialTheme.dividerColor; + return ColoredBox( + color: scaffoldBg, + child: Row( + children: [ + // 左侧:各模块首页(IndexedStack切换) + SizedBox( + width: 450, + child: ColoredBox( + color: panelBg, + child: Obx( + () => IndexedStack( + key: controller.indexKey, + index: controller.index.value, + children: controller.pages, + ), + ), + ), + ), + // 分隔线 + Container(width: 1, color: dividerColor), + // 右侧:子路由(详情页、阅读器等) + Expanded( + child: _buildContentNavigator(), + ), + ], + ), + ); + } + + /// 子路由导航器(处理详情页、阅读器等) + Widget _buildContentNavigator() { + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (!didPop) { + if (Navigator.canPop(Get.context!)) { + Get.back(); + return; + } + if (AppNavigator.subNavigatorKey!.currentState!.canPop()) { + AppNavigator.subNavigatorKey!.currentState!.pop(); + } + } + }, + child: ClipRect( + child: Navigator( + key: AppNavigator.subNavigatorKey, + initialRoute: '/', + onUnknownRoute: (settings) => GetPageRoute( + page: () => const EmptyPage(), + ), + observers: [WindowsSubNavigatorObserver()], + onGenerateRoute: AppPages.generateSubRoute, + ), + ), + ); + } +} + +/// Windows子路由监听(不需要更新showContent,因为采用固定master-detail布局) +class WindowsSubNavigatorObserver extends NavigatorObserver { + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + if (previousRoute != null) { + final routeName = route.settings.name ?? ''; + AppNavigator.currentContentRouteName = routeName; + } + } + + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + final routeName = previousRoute?.settings.name ?? ''; + AppNavigator.currentContentRouteName = routeName; + } +} diff --git a/lib/modules/news/detail/news_detail_controller.dart b/lib/modules/news/detail/news_detail_controller.dart new file mode 100644 index 0000000..c965e4f --- /dev/null +++ b/lib/modules/news/detail/news_detail_controller.dart @@ -0,0 +1,398 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/requests/news_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:universal_html/html.dart' as html; +import 'package:universal_html/parsing.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class NewsDetailController extends BaseController { + final String newsUrl; + final String title; + final int id; + final NewsRequest request = NewsRequest(); + AppSettingsService get settings => AppSettingsService.instance; + NewsDetailController( + {required this.newsUrl, this.title = "资讯详情", required this.id}) { + newsTitle.value = title; + if (id == 0) { + newsId = int.tryParse( + RegExp(r"/(\d+).html").firstMatch(newsUrl)?.group(1) ?? "0") ?? + 0; + } else { + newsId = id; + } + } + WebViewController? webViewController = + (Platform.isAndroid || Platform.isIOS) ? WebViewController() : null; + + /// 评论数 + var commentAmount = 0.obs; + + /// 点赞数 + var moodAmount = 0.obs; + + /// 是否点过赞 + var liked = false.obs; + + /// 是否已经收藏 + var collected = false.obs; + + var newsId = 0; + + var newsTitle = "资讯详情".obs; + + var htmlContent = "".obs; + var author = "".obs; + var photo = "".obs; + var src = "".obs; + var time = "".obs; + + var images = []; + + @override + void onInit() { + liked.value = DBService.instance.newsLikeBox.containsKey(newsId); + if (Platform.isAndroid || Platform.isIOS) { + initWebView(); + } else { + loadHtml(); + } + // loadStat(); + // checkCollected(); + super.onInit(); + } + + var currentUrl = ""; + void initWebView() { + webViewController!.setJavaScriptMode(JavaScriptMode.unrestricted); + webViewController!.setBackgroundColor( + Get.isDarkMode ? Colors.black : AppColor.backgroundColor); + webViewController!.setNavigationDelegate( + NavigationDelegate( + onPageStarted: (String url) { + pageLoadding.value = true; + }, + onPageFinished: (String url) async { + try { + await setFontSize(); + //防止亮瞎24K钛合金狗眼 + if (Get.isDarkMode) { + await webViewController!.runJavaScript(""" +document.body.style.background="#000000"; +document.getElementsByClassName("min_box")[0].style.background="#000000"; +document.getElementsByClassName("news_box")[0].style.color="#f1f2f6"; +document.getElementsByClassName("min_box_tit")[0].style.color="#fff"; +"""); + } + //加载前5张图片 + //当Web没有滚动条时,图片不会加载,这里手动给他加载出来 + await webViewController!.runJavaScript(""" +\$('.news_box img:lt(5)').each(function () { + \$(this).lazyload({ + effect: "fadeIn" + }); +});"""); + //读取全部的图片 + + var imagesResult = + await webViewController?.runJavaScriptReturningResult(''' +function getImgLinks(){ + var imgLinks = []; + \$('img').each(function() { + var src = \$(this).attr('data-original'); + if (src && src.startsWith('https://images')) { + imgLinks.push(src); + } + }); + console.log(imgLinks); + return ${Platform.isIOS ? "JSON.stringify(imgLinks)" : "imgLinks"}; +} +getImgLinks(); +'''); + if (imagesResult != null && imagesResult != "null") { + List list = json.decode(imagesResult.toString()); + images = list.map((e) => e.toString()).toList(); + } + } finally { + pageLoadding.value = false; + } + }, + onWebResourceError: (WebResourceError error) {}, + onNavigationRequest: (NavigationRequest request) async { + var result = await onTapUrl(request.url); + return result + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + ), + ); + Log.d(newsUrl); + currentUrl = "https://v3api.zaimanhua.com/v3/article/show/$newsId.html"; + + webViewController!.loadRequest(Uri.parse(currentUrl)); + } + + void loadHtml() async { + try { + pageError.value = false; + pageLoadding.value = true; + var result = await Dio().get( + newsUrl, + options: Options( + responseType: ResponseType.plain, + ), + ); + final htmlDocument = parseHtmlDocument(result.data); + var news = htmlDocument.documentElement!.querySelector('.news_box'); + + htmlContent.value = news!.innerHtml ?? ""; + + author.value = + htmlDocument.documentElement?.querySelector('.txt1')?.innerText ?? ""; + src.value = + htmlDocument.documentElement?.querySelector('.txt2')?.innerText ?? ""; + time.value = + htmlDocument.documentElement?.querySelector('.txt3')?.innerText ?? ""; + + var imgList = htmlDocument.documentElement?.querySelectorAll('img'); + var imagesList = []; + for (html.Element img in imgList ?? []) { + var imgSrc = img.getAttribute("data-original"); + if (imgSrc != null) { + imagesList.add(imgSrc); + } + } + images = imagesList; + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + } + + void loadStat() async { + try { + var result = await request.stat(newsId); + commentAmount.value = result.commentAmount; + moodAmount.value = result.moodAmount; + newsTitle.value = result.title; + } catch (e) { + SmartDialog.showToast(e.toString()); + SmartDialog.showToast("读取新闻数据失败:$e"); + } + } + + void checkCollected() async { + if (!UserService.instance.logined.value) { + return; + } + try { + collected.value = await request.checkCollect(newsId); + } catch (e) { + Log.logPrint(e); + SmartDialog.showToast("检查用户收藏状态失败:$e"); + } + } + + void refershContent() { + webViewController!.reload(); + } + + void collect() async { + if (!await UserService.instance.login()) { + return; + } + try { + SmartDialog.showLoading(); + await (collected.value + ? request.delCollect(newsId) + : request.collect(newsId)); + collected.value = !collected.value; + } catch (e) { + Log.logPrint(e); + SmartDialog.showToast(e.toString()); + } finally { + SmartDialog.dismiss(status: SmartStatus.loading); + } + } + + void like() async { + if (liked.value) { + SmartDialog.showToast("已经点过赞了"); + return; + } + try { + SmartDialog.showLoading(); + await request.like(newsId); + liked.value = true; + moodAmount.value += 1; + DBService.instance.newsLikeBox.put(newsId, true); + } catch (e) { + SmartDialog.showToast(e.toString()); + } finally { + SmartDialog.dismiss(status: SmartStatus.loading); + } + } + + void share() { + Utils.share(newsUrl, content: title); + } + + void comment() async { + AppNavigator.toComment(objId: newsId, type: AppConstant.kTypeNews); + } + + void photoView() { + DialogUtils.showImageViewer(0, images); + } + + void showImageView(String imgSrc) { + if (imgSrc.isEmpty) { + return; + } + if (images.contains(imgSrc)) { + DialogUtils.showImageViewer( + images.indexOf(imgSrc), + images, + ); + } else { + DialogUtils.showImageViewer(0, [imgSrc]); + } + } + + Future onTapUrl(url) async { + //iOS处理 + if (url == currentUrl) { + return false; + } + var uri = Uri.parse(url); + Log.d(url); + if (uri.scheme == "dmzjimage") { + //打开图片 + showImageView(uri.queryParameters['src'].toString()); + return true; + } else if (uri.scheme == "dmzjandroid") { + var id = int.tryParse(uri.queryParameters["id"].toString()) ?? 0; + if (uri.path == "/cartoon_description") { + AppNavigator.toComicDetail(id); + } else { + AppNavigator.toNovelDetail(id); + } + return true; + } else if (uri.scheme == "https" || uri.scheme == "http") { + if (uri.path.contains("article/")) { + AppNavigator.toNewsDetail(url: url); + } else { + AppNavigator.toWebView(url); + } + + return true; + } else { + SmartDialog.showToast("无法打开链接:$url"); + return true; + } + } + + void showSettings() { + AppNavigator.showBottomSheet( + SizedBox( + height: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ListTile( + title: Text("设置"), + trailing: IconButton( + onPressed: AppNavigator.closePage, + icon: Icon(Icons.close), + ), + contentPadding: AppStyle.edgeInsetsL12, + ), + Divider( + height: 1.0, + color: Colors.grey.withOpacity(.2), + ), + Obx( + () => ListTile( + title: const Text("字体大小"), + leading: const Icon(Icons.text_fields_rounded), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + OutlinedButton( + onPressed: () { + settings.setNewsFontSize( + settings.newsFontSize.value + 1, + ); + setFontSize(); + }, + child: const Icon( + Icons.add, + color: Colors.grey, + ), + ), + AppStyle.hGap12, + Text("${settings.newsFontSize.value}"), + AppStyle.hGap12, + OutlinedButton( + onPressed: () { + settings.setNewsFontSize( + settings.newsFontSize.value - 1, + ); + setFontSize(); + }, + child: const Icon( + Icons.remove, + color: Colors.grey, + ), + ), + ], + ), + ), + ), + ListTile( + leading: const Icon(Icons.photo), + title: const Text("进入看图模式"), + onTap: () { + AppNavigator.closePage(); + photoView(); + }, + trailing: const Icon(Icons.chevron_right), + ), + ], + ), + ), + ); + } + + Future setFontSize() async { + try { + if (webViewController == null) { + return; + } + await webViewController!.runJavaScript( + '''document.getElementsByClassName("news_box")[0].style.fontSize="${settings.newsFontSize}px"; +document.getElementsByClassName("news_box")[0].style.lineHeight="1.6em"; +'''); + } catch (e) { + Log.logPrint(e); + } + } +} diff --git a/lib/modules/news/detail/news_detail_page.dart b/lib/modules/news/detail/news_detail_page.dart new file mode 100644 index 0000000..6945b0d --- /dev/null +++ b/lib/modules/news/detail/news_detail_page.dart @@ -0,0 +1,189 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/news/detail/news_detail_controller.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class NewsDetailPage extends StatelessWidget { + final String newsUrl; + final int newsId; + final String title; + final NewsDetailController controller; + NewsDetailPage({ + required this.newsUrl, + this.title = "资讯详情", + required this.newsId, + Key? key, + }) : controller = Get.put( + NewsDetailController(id: newsId, newsUrl: newsUrl, title: title), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ), + super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx(() => Text(controller.newsTitle.value)), + actions: [ + IconButton( + onPressed: controller.share, + icon: const Icon(Icons.share), + ), + ], + ), + body: Stack( + children: [ + (Platform.isAndroid || Platform.isIOS) + ? Obx( + () => Offstage( + offstage: controller.pageLoadding.value, + child: WebViewWidget( + controller: controller.webViewController!, + ), + ), + ) + : buildHtml(), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: controller.refershContent, + ), + ), + ), + ], + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Obx( + () => TextButton.icon( + onPressed: controller.like, + icon: Icon( + controller.liked.value + ? Remix.thumb_up_fill + : Remix.thumb_up_line, + size: 20, + ), + label: Text(controller.moodAmount > 0 + ? "${controller.moodAmount}" + : "点赞"), + ), + ), + ), + Expanded( + child: Obx( + () => TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.comment, + icon: const Icon( + Remix.chat_2_line, + size: 20, + ), + label: Text(controller.commentAmount > 0 + ? "${controller.commentAmount}" + : "评论"), + ), + ), + ), + // Expanded( + // child: Obx( + // () => TextButton.icon( + // style: TextButton.styleFrom( + // textStyle: const TextStyle(fontSize: 14), + // ), + // onPressed: controller.collect, + // icon: Icon( + // controller.collected.value + // ? Remix.star_fill + // : Remix.star_line, + // size: 20, + // ), + // label: Text(controller.collected.value ? "已收藏" : "收藏"), + // ), + // ), + // ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.showSettings, + icon: const Icon( + Remix.settings_line, + size: 20, + ), + label: const Text("设置"), + ), + ), + ], + ), + ), + ), + ); + } + + Widget buildHtml() { + return Obx( + () => ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + Text( + controller.title, + style: Get.textTheme.titleLarge, + ), + AppStyle.vGap4, + Text( + "${controller.author.value} ${controller.src.value} ${controller.time.value}", + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + AppStyle.vGap12, + HtmlWidget( + controller.htmlContent.value, + textStyle: TextStyle( + fontSize: controller.settings.newsFontSize.value.toDouble(), + ), + customWidgetBuilder: (e) { + if (e.localName == "img") { + var imgSrc = e.attributes["src"]; + imgSrc ??= e.attributes["data-original"]; + return GestureDetector( + child: NetImage( + imgSrc!, + borderRadius: 4, + ), + onTap: () { + controller.showImageView(imgSrc ?? ""); + }, + ); + } + + return null; + }, + onTapUrl: controller.onTapUrl, + ), + ], + ), + ); + } +} diff --git a/lib/modules/news/home/news_home_controller.dart b/lib/modules/news/home/news_home_controller.dart new file mode 100644 index 0000000..da791b0 --- /dev/null +++ b/lib/modules/news/home/news_home_controller.dart @@ -0,0 +1,67 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/models/news/news_tag_model.dart'; +import 'package:flutter_dmzj/modules/news/home/news_list_controller.dart'; +import 'package:flutter_dmzj/requests/news_request.dart'; +import 'package:get/get.dart'; + +class NewsHomeController extends GetxController + with GetTickerProviderStateMixin { + NewsRequest request = NewsRequest(); + late TabController tabController; + var loadding = true; + List categores = []; + var error = false; + var errorMsg = ""; + + StreamSubscription? streamSubscription; + + @override + void onInit() { + streamSubscription = EventBus.instance.listen( + EventBus.kBottomNavigationBarClicked, + (index) { + if (index == 1) { + refreshOrScrollTop(); + } + }, + ); + loadCategores(); + super.onInit(); + } + + @override + void onClose() { + streamSubscription?.cancel(); + super.onClose(); + } + + void loadCategores() async { + try { + loadding = true; + error = false; + update(); + var category = await request.category(); + category.insert(0, NewsTagModel(id: 0, name: "最新")); + tabController = TabController(length: category.length, vsync: this); + + categores = category; + } catch (e) { + errorMsg = e.toString(); + error = true; + } finally { + loadding = false; + update(); + } + } + + void refreshOrScrollTop() { + var tabIndex = tabController.index; + BasePageController controller; + controller = Get.find(tag: "${categores[tabIndex].id}"); + controller.scrollToTopOrRefresh(); + } +} diff --git a/lib/modules/news/home/news_home_page.dart b/lib/modules/news/home/news_home_page.dart new file mode 100644 index 0000000..379e8b7 --- /dev/null +++ b/lib/modules/news/home/news_home_page.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/modules/news/home/news_home_controller.dart'; +import 'package:flutter_dmzj/modules/news/home/news_list_view.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:flutter_dmzj/widgets/tab_appbar.dart'; +import 'package:flutter_dmzj/widgets/windows_tab_page.dart'; +import 'package:get/get.dart'; + +class NewsHomePage extends GetView { + const NewsHomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetBuilder( + init: controller, + builder: (controller) { + if (controller.loadding) { + return const Scaffold( + body: AppLoaddingWidget(), + ); + } + if (!controller.loadding && controller.error) { + return Scaffold( + body: AppErrorWidget( + errorMsg: controller.errorMsg, + onRefresh: controller.loadCategores, + ), + ); + } + if (PlatformUtils.isWindows) { + return WindowsTabPage( + tabs: controller.categores + .map((e) => WindowsTabItem( + label: e.name, + body: NewsListView(tag: e), + )) + .toList(), + ); + } + return Scaffold( + appBar: TabAppBar( + tabs: controller.categores.map((e) => Tab(text: e.name)).toList(), + controller: controller.tabController, + ), + body: TabBarView( + controller: controller.tabController, + children: + controller.categores.map((e) => NewsListView(tag: e)).toList(), + ), + ); + }, + ); + } +} + diff --git a/lib/modules/news/home/news_list_controller.dart b/lib/modules/news/home/news_list_controller.dart new file mode 100644 index 0000000..7924d05 --- /dev/null +++ b/lib/modules/news/home/news_list_controller.dart @@ -0,0 +1,40 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/news/news_banner_model.dart'; +import 'package:flutter_dmzj/models/news/news_list_item_model.dart'; +import 'package:flutter_dmzj/models/news/news_tag_model.dart'; +import 'package:flutter_dmzj/requests/news_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class NewsListController extends BasePageController { + final NewsRequest request = NewsRequest(); + final NewsTagModel tag; + NewsListController(this.tag); + + RxList banners = RxList(); + + @override + Future> getData(int page, int pageSize) async { + if (tag.id == 0 && page == 1) { + loadBanner(); + } + return await request.getNewsList(tag.id, page); + } + + void loadBanner() async { + try { + banners.value = await request.banner(); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + void openBanner(NewsBannerModel item) { + AppNavigator.toNewsDetail( + url: item.objectUrl ?? "", + newsId: item.objectId ?? 0, + title: item.title, + ); + } +} diff --git a/lib/modules/news/home/news_list_view.dart b/lib/modules/news/home/news_list_view.dart new file mode 100644 index 0000000..0bd5ac9 --- /dev/null +++ b/lib/modules/news/home/news_list_view.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/news/news_tag_model.dart'; +import 'package:flutter_dmzj/modules/news/home/news_list_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:flutter_swiper_view/flutter_swiper_view.dart'; +import 'package:get/get.dart'; + +class NewsListView extends StatelessWidget { + final NewsTagModel tag; + final NewsListController controller; + NewsListView({Key? key, required this.tag}) + : controller = Get.put(NewsListController(tag), tag: tag.id.toString()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + header: tag.id == 0 ? buildBanner() : null, + itemBuilder: (context, i) { + var item = controller.list[i]; + return InkWell( + onTap: () { + AppNavigator.toNewsDetail( + newsId: item.articleId.toInt(), + title: item.title, + url: item.pageUrl ?? "", + ); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.rowPicUrl ?? "", + width: 100, + height: 62, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: SizedBox( + height: 62, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + item.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + Utils.formatTimestamp(item.createTime ?? 0), + style: const TextStyle( + color: Colors.grey, fontSize: 12), + ), + // Row( + // children: [ + // const Icon( + // Icons.thumb_up, + // size: 12.0, + // color: Colors.grey, + // ), + // AppStyle.hGap4, + // Text( + // item.moodAmount.toString(), + // style: const TextStyle( + // color: Colors.grey, + // fontSize: 12, + // ), + // ), + // AppStyle.hGap8, + // const Icon( + // Icons.chat, + // size: 12.0, + // color: Colors.grey, + // ), + // AppStyle.hGap4, + // Text( + // item.commentAmount.toString(), + // style: const TextStyle( + // color: Colors.grey, + // fontSize: 12, + // ), + // ) + // ], + // ) + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + + Widget buildBanner() { + return Padding( + padding: AppStyle.edgeInsetsH12.copyWith(bottom: 4), + child: Obx( + () => ClipRRect( + borderRadius: AppStyle.radius4, + child: AspectRatio( + aspectRatio: 75 / 40, + child: controller.banners.isEmpty + ? const SizedBox() + : Swiper( + itemWidth: 750, + itemHeight: 400, + autoplay: true, + itemCount: controller.banners.length, + onTap: (i) { + controller.openBanner(controller.banners[i]); + }, + itemBuilder: (_, i) => NetImage( + controller.banners[i].picUrl, + width: 750, + height: 400, + ), + pagination: SwiperCustomPagination( + builder: + (BuildContext context, SwiperPluginConfig config) { + return Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.only( + left: 8, + right: 12, + top: 4, + bottom: 4, + ), + //color: Colors.black12, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black38, + Colors.transparent, + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + controller + .banners[config.activeIndex].title, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, color: Colors.white), + ), + ), + AppStyle.hGap8, + PageIndicator( + controller: config.pageController!, + count: config.itemCount, + size: 10, + layout: PageIndicatorLayout.SCALE, + ), + ], + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/novel/category_detail/novel_category_detail_controller.dart b/lib/modules/novel/category_detail/novel_category_detail_controller.dart new file mode 100644 index 0000000..aad3dd1 --- /dev/null +++ b/lib/modules/novel/category_detail/novel_category_detail_controller.dart @@ -0,0 +1,83 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/novel/category_filter_model.dart'; +import 'package:flutter_dmzj/models/novel/category_novel_model.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class NovelCategoryDetailController + extends BasePageController { + final int id; + NovelCategoryDetailController(this.id); + final NovelRequest request = NovelRequest(); + RxList filters = RxList(); + + @override + void onInit() { + loadFilter(); + super.onInit(); + } + + String getTitle() { + var items = filters.where((x) => x.selectId.value != 0 && x.title != "排序"); + + if (items.isEmpty) { + return "全部小说"; + } else { + return items + .map((e) => + e.items.firstWhere((x) => x.tagId == e.selectId.value).tagName) + .join("-"); + } + } + + void loadFilter() async { + try { + filters.value = await request.categoryFilter(); + for (var item in filters) { + var tag = item.items.firstWhereOrNull((x) => x.tagId == id); + if (tag != null) { + item.selectId.value = tag.tagId; + } + } + filters.insert( + 0, + NovelCategoryFilterModel( + title: "排序", + items: [ + NovelCategoryFilterItemModel(tagId: 1, tagName: "更新排序"), + NovelCategoryFilterItemModel(tagId: 2, tagName: "热度排序"), + ], + )..selectId.value = 1, + ); + filters.insert( + 1, + NovelCategoryFilterModel( + title: "状态", + items: [ + NovelCategoryFilterItemModel(tagId: 0, tagName: "全部"), + NovelCategoryFilterItemModel(tagId: 1, tagName: "连载中"), + NovelCategoryFilterItemModel(tagId: 2, tagName: "已完结"), + ], + ), + ); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + @override + Future> getData(int page, int pageSize) async { + if (filters.isEmpty) { + return await request.categoryNovel(cateId: id, page: page - 1); + } else { + var sort = filters.first.selectId.value; + var status = filters[1].selectId.value; + var cateId = + filters.firstWhereOrNull((x) => x.title == "题材")?.selectId.value ?? 0; + + return await request.categoryNovel( + cateId: cateId, status: status, sort: sort, page: page - 1); + } + } +} diff --git a/lib/modules/novel/category_detail/novel_category_detail_page.dart b/lib/modules/novel/category_detail/novel_category_detail_page.dart new file mode 100644 index 0000000..c6514eb --- /dev/null +++ b/lib/modules/novel/category_detail/novel_category_detail_page.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/novel/category_detail/novel_category_detail_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_grid_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class NovelCategoryDetailPage extends StatelessWidget { + final int id; + final NovelCategoryDetailController controller; + NovelCategoryDetailPage(this.id, {super.key}) + : controller = Get.put( + NovelCategoryDetailController(id), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx( + () => Text( + controller.getTitle(), + ), + ), + actions: [ + Builder( + builder: (BuildContext context) => IconButton( + icon: const Icon(Remix.filter_line), + onPressed: () { + Scaffold.of(context).openEndDrawer(); + }, + ), + ) + ], + ), + endDrawer: Drawer( + child: Obx( + () => SafeArea( + child: ListView.builder( + padding: AppStyle.edgeInsetsA12.copyWith(top: 12), + itemCount: controller.filters.length, + itemBuilder: (context, i) { + var item = controller.filters[i]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: AppStyle.edgeInsetsV12, + child: Text( + item.title, + style: Get.textTheme.titleMedium, + ), + ), + Wrap( + spacing: 8, + runSpacing: 8, + children: item.items + .map( + (x) => OutlinedButton( + style: OutlinedButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + foregroundColor: x.tagId == item.selectId.value + ? Theme.of(context).colorScheme.primary + : Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: BorderSide( + color: x.tagId == item.selectId.value + ? Theme.of(context) + .colorScheme + .secondary + : Colors.transparent, + ), + ), + ), + child: Text( + x.tagName, + style: const TextStyle( + fontSize: 14, + ), + ), + onPressed: () async { + item.selectId.value = x.tagId; + + Navigator.pop(context); + controller.refreshData(); + }, + ), + ) + .toList(), + ), + ], + ); + }, + ), + ), + ), + ), + body: LayoutBuilder(builder: (context, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + return PageGridView( + pageController: controller, + firstRefresh: true, + crossAxisCount: count, + padding: AppStyle.edgeInsetsA12, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return ShadowCard( + onTap: () { + AppNavigator.toNovelDetail(item.id); + }, + radius: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover ?? "", + borderRadius: 4, + ), + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + item.authors ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.grey, + fontSize: 12.0, + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + ], + ), + ); + }, + ); + }), + ); + } +} diff --git a/lib/modules/novel/detail/novel_detail_controller.dart b/lib/modules/novel/detail/novel_detail_controller.dart new file mode 100644 index 0000000..646f877 --- /dev/null +++ b/lib/modules/novel/detail/novel_detail_controller.dart @@ -0,0 +1,243 @@ +import 'dart:async'; + +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/db/novel_history.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class NovelDetailControler extends BaseController { + final int novelId; + NovelDetailControler(this.novelId); + + final NovelRequest request = NovelRequest(); + final UserRequest userRequest = UserRequest(); + + Rx detail = Rx(NovelDetailInfo.empty()); + + var expandDescription = false.obs; + + /// 是否已订阅 + var subscribeStatus = false.obs; + + /// 阅读记录 + Rx history = Rx(null); + + /// 更新小说记录 + StreamSubscription? updateNovelSubscription; + + @override + void onInit() { + updateNovelSubscription = EventBus.instance.listen( + EventBus.kUpdatedNovelHistory, + (id) { + if (id == novelId) { + getHistory(); + } + }, + ); + // 从本地读取订阅状态 + subscribeStatus.value = + UserService.instance.subscribedNovelIds.contains(novelId); + getHistory(); + loadDetail(); + loadSubscribeStatus(); + //updateSubscribeRead(); + super.onInit(); + } + + void refreshDetail() { + getHistory(); + loadDetail(); + loadSubscribeStatus(); + } + + /// 更新订阅的阅读状态 + void updateSubscribeRead() { + try { + userRequest.subscribeRead(id: novelId, type: AppConstant.kTypeNovel); + } catch (e) { + Log.logPrint(e); + } + } + + @override + void onClose() { + updateNovelSubscription?.cancel(); + super.onClose(); + } + + void getHistory() { + var novelHistory = DBService.instance.getNovelHistory(novelId); + if (novelHistory != null) { + history.value = novelHistory; + history.update((val) {}); + } + } + + /// 加载信息 + void loadDetail() async { + try { + pageLoadding.value = true; + pageError.value = false; + var result = await request.novelDetail(novelId: novelId); + + detail.value = NovelDetailInfo.fromJson(result.data); + await loadChapter(); + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + } + + Future loadChapter() async { + try { + var result = await request.novelChapter(novelId: novelId); + detail.value.volume.value = + result.map((e) => NovelDetailVolume.fromJson(e)).toList(); + } catch (e) { + SmartDialog.showToast("无法读取小说章节:$e"); + } + } + + /// 检查订阅状态 + void loadSubscribeStatus() async { + try { + var result = await userRequest.checkSubscribeStatus( + objId: novelId, + type: AppConstant.kTypeNovel, + ); + subscribeStatus.value = result; + if (subscribeStatus.value) { + UserService.instance.subscribedNovelIds.add(novelId); + } else { + UserService.instance.subscribedNovelIds.remove(novelId); + } + } catch (e) { + Log.logPrint(e); + } + } + + /// 查看评论 + void comment() { + AppNavigator.toComment(objId: novelId, type: AppConstant.kTypeNovel); + } + + /// 分享 + void share() { + Utils.share( + "http://q.idmzj.com/$novelId/index.shtml", + content: detail.value.name, + ); + } + + /// 订阅 + void subscribe() async { + var result = await (subscribeStatus.value + ? UserService.instance + .cancelSubscribe([novelId], AppConstant.kTypeNovel) + : UserService.instance.addSubscribe([novelId], AppConstant.kTypeNovel)); + if (result) { + subscribeStatus.value = !subscribeStatus.value; + } + } + + /// 下载 + void download() { + AppNavigator.toNovelDownloadSelect(novelId); + } + + /// 开始/继续阅读 + void read() { + if (detail.value.volume.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + if (detail.value.volume.first.chapters.isEmpty) { + SmartDialog.showToast("没有可阅读的章节"); + return; + } + //查找记录 + if (history.value != null && history.value!.chapterId != 0) { + NovelDetailChapter? chapter; + for (var volumeItem in detail.value.volume) { + var chapterItem = volumeItem.chapters.firstWhereOrNull( + (x) => x.chapterId == history.value!.chapterId, + ); + if (chapterItem != null) { + chapter = chapterItem; + break; + } + } + if (chapter != null) { + List chapters = []; + for (var volume in detail.value.volume) { + chapters.addAll(volume.chapters); + } + + AppNavigator.toNovelReader( + novelId: novelId, + novelCover: detail.value.cover, + novelTitle: detail.value.name, + chapter: chapter, + chapters: chapters, + ); + } else { + SmartDialog.showToast("未找到历史记录对应章节,将从头开始阅读"); + readStart(); + } + } else { + readStart(); + } + } + + void readStart() { + //从头开始 + List chapters = []; + for (var volume in detail.value.volume) { + chapters.addAll(volume.chapters); + } + var chapter = chapters.first; + AppNavigator.toNovelReader( + novelId: novelId, + novelCover: detail.value.cover, + novelTitle: detail.value.name, + chapter: chapter, + chapters: chapters, + ); + } + + void readChapter(NovelDetailVolume volume, NovelDetailChapter item) { + List chapters = []; + for (var volume in detail.value.volume) { + chapters.addAll(volume.chapters); + } + + AppNavigator.toNovelReader( + novelId: novelId, + novelCover: detail.value.cover, + novelTitle: detail.value.name, + chapters: chapters, + chapter: item, + ); + } + + void toAuthorDetail(String e) { + AppNavigator.toNovelSearch(keyword: e); + } + + void toCategoryDetail(String e) { + AppNavigator.toNovelSearch(keyword: e); + } +} diff --git a/lib/modules/novel/detail/novel_detail_page.dart b/lib/modules/novel/detail/novel_detail_page.dart new file mode 100644 index 0000000..a0845ad --- /dev/null +++ b/lib/modules/novel/detail/novel_detail_page.dart @@ -0,0 +1,361 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/modules/novel/detail/novel_detail_controller.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class NovelDetailPage extends StatelessWidget { + final int id; + final NovelDetailControler controller; + NovelDetailPage(this.id, {super.key}) + : controller = Get.put( + NovelDetailControler(id), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Obx( + () => Text( + controller.detail.value.name.isEmpty + ? "小说详情" + : controller.detail.value.name, + ), + ), + actions: [ + IconButton( + onPressed: controller.share, + icon: const Icon(Icons.share), + ), + ], + ), + body: Stack( + children: [ + Obx( + () => Offstage( + offstage: controller.detail.value.novelId == 0, + child: EasyRefresh( + header: const MaterialHeader(), + onRefresh: controller.refreshDetail, + child: ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + _buildHeader(), + Obx( + () => Offstage( + offstage: controller.history.value == null, + child: Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + "上次看到:${controller.history.value?.volumeName ?? ""} ${controller.history.value?.chapterName ?? ""}", + style: Get.textTheme.titleSmall, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () { + controller.read(); + }, + ), + Divider( + color: Colors.grey.withOpacity(.2), + height: 1.0, + ), + ], + ), + ), + ), + _buildChapter(), + ], + ), + ), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadDetail(), + ), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + elevation: 2, + onPressed: controller.read, + child: const Icon(Icons.play_circle_outline_rounded), + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Obx( + () => TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.subscribe, + icon: Icon( + controller.subscribeStatus.value + ? Remix.heart_fill + : Remix.heart_line, + size: 20, + ), + label: Text(controller.subscribeStatus.value ? "取消" : "订阅"), + ), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.comment, + icon: const Icon( + Remix.chat_2_line, + size: 20, + ), + label: const Text("评论"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.download, + icon: const Icon( + Remix.download_line, + size: 20, + ), + label: const Text("下载"), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + //信息 + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + NetImage( + controller.detail.value.cover, + width: 120, + height: 160, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + controller.detail.value.name, + style: Get.textTheme.titleMedium, + ), + AppStyle.vGap8, + _buildInfoItems( + iconData: Remix.user_smile_line, + children: controller.detail.value.authors + .split("/") + .map( + (e) => GestureDetector( + onTap: () => controller.toAuthorDetail(e), + child: Text( + e, + style: TextStyle( + fontSize: 14, + height: 1.2, + decoration: TextDecoration.underline, + color: Get.isDarkMode + ? Colors.white + : AppColor.black333, + ), + ), + ), + ) + .toList(), + ), + _buildInfo( + title: + controller.detail.value.types.map((e) => e).join("/"), + iconData: Remix.hashtag, + ), + _buildInfo( + title: "人气 ${controller.detail.value.hotHits}", + iconData: Remix.fire_line, + ), + _buildInfo( + title: "订阅 ${controller.detail.value.subscribeNum}", + iconData: Remix.heart_line, + ), + _buildInfo( + title: + "${Utils.formatTimestampToDate(controller.detail.value.lastUpdateTime)} ${controller.detail.value.status}", + iconData: Icons.schedule, + ), + ], + ), + ), + ], + ), + AppStyle.vGap12, + GestureDetector( + onTap: () { + controller.expandDescription.value = + !controller.expandDescription.value; + }, + child: Text( + controller.detail.value.introduction, + style: const TextStyle( + color: Colors.grey, + fontSize: 14, + ), + maxLines: controller.expandDescription.value ? 999 : 2, + overflow: TextOverflow.ellipsis, + ), + ), + AppStyle.vGap12, + Divider( + color: Colors.grey.withOpacity(.2), + height: 1.0, + ), + ], + ); + } + + Widget _buildChapter() { + return Obx( + () => Column( + children: controller.detail.value.volume + .map( + (item) => ExpansionTile( + title: Text( + "${item.volumeName}(共${item.chapters.length}章)", + style: Get.textTheme.titleSmall, + ), + tilePadding: AppStyle.edgeInsetsH4, + children: [ + ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + physics: const NeverScrollableScrollPhysics(), + itemCount: item.chapters.length, + separatorBuilder: (_, i) => const Divider( + height: 1, + ), + itemBuilder: (context, i) { + var chapter = item.chapters[i]; + return ListTile( + title: Text( + chapter.chapterName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Get.textTheme.bodyMedium!.copyWith( + color: controller.history.value?.chapterId == + chapter.chapterId + ? Get.theme.colorScheme.primary + : null, + ), + ), + contentPadding: AppStyle.edgeInsetsA4, + visualDensity: const VisualDensity( + vertical: VisualDensity.minimumDensity), + onTap: () { + controller.readChapter(item, chapter); + }, + ); + }, + ), + ], + ), + ) + .toList(), + ), + ); + } + + Widget _buildInfo({ + required String title, + IconData iconData = Icons.tag, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + iconData, + color: Colors.grey, + size: 16, + ), + AppStyle.hGap8, + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14, + color: Get.isDarkMode ? Colors.white : AppColor.black333, + ), + ), + ), + ], + ), + ); + } + + Widget _buildInfoItems({ + required List children, + IconData iconData = Icons.tag, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + iconData, + color: Colors.grey, + size: 16, + ), + AppStyle.hGap8, + Expanded( + child: Wrap( + spacing: 8, + children: children, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/novel/home/category/novel_category_controller.dart b/lib/modules/novel/home/category/novel_category_controller.dart new file mode 100644 index 0000000..89c3877 --- /dev/null +++ b/lib/modules/novel/home/category/novel_category_controller.dart @@ -0,0 +1,22 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/novel/category_model.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +class NovelCategoryController extends BasePageController { + final NovelRequest request = NovelRequest(); + + @override + Future> getData(int page, int pageSize) async { + if (page > 1) { + return []; + } + var ls = await request.categores(); + + return ls; + } + + void toDetail(NovelCategoryModel item) { + AppNavigator.toNovelCategoryDetail(item.tagId); + } +} diff --git a/lib/modules/novel/home/category/novel_category_view.dart b/lib/modules/novel/home/category/novel_category_view.dart new file mode 100644 index 0000000..a5410f8 --- /dev/null +++ b/lib/modules/novel/home/category/novel_category_view.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/novel/home/category/novel_category_controller.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_grid_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; + +class NovelCategoryView extends StatelessWidget { + final NovelCategoryController controller; + NovelCategoryView({Key? key}) + : controller = Get.put(NovelCategoryController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + return KeepAliveWrapper( + child: PageGridView( + pageController: controller, + firstRefresh: true, + loadMore: false, + crossAxisCount: count, + padding: AppStyle.edgeInsetsH12.copyWith(bottom: 12), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return ShadowCard( + onTap: () { + controller.toDetail(item); + }, + child: Column( + children: [ + AspectRatio( + aspectRatio: 1.0, + child: NetImage( + item.cover, + borderRadius: 8, + ), + ), + Padding( + padding: AppStyle.edgeInsetsA8, + child: Text( + item.title, + textAlign: TextAlign.center, + style: const TextStyle(height: 1), + ), + ), + ], + ), + ); + }, + ), + ); + }); + } +} diff --git a/lib/modules/novel/home/latest/novel_latest_controller.dart b/lib/modules/novel/home/latest/novel_latest_controller.dart new file mode 100644 index 0000000..2e63963 --- /dev/null +++ b/lib/modules/novel/home/latest/novel_latest_controller.dart @@ -0,0 +1,14 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/novel/latest_model.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; + +class NovelLatestController extends BasePageController { + final NovelRequest request = NovelRequest(); + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.latest(page: page); + + return ls; + } +} diff --git a/lib/modules/novel/home/latest/novel_latest_view.dart b/lib/modules/novel/home/latest/novel_latest_view.dart new file mode 100644 index 0000000..34f2a24 --- /dev/null +++ b/lib/modules/novel/home/latest/novel_latest_view.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/novel/latest_model.dart'; +import 'package:flutter_dmzj/modules/novel/home/latest/novel_latest_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class NovelLatestView extends StatelessWidget { + final NovelLatestController controller; + NovelLatestView({Key? key}) + : controller = Get.put(NovelLatestController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + showPageLoadding: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ); + } + + Widget buildItem(NovelLatestModel item) { + return InkWell( + onTap: () { + AppNavigator.toNovelDetail(item.id); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover ?? "", + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text.rich( + TextSpan(children: [ + const WidgetSpan( + child: Icon( + Icons.account_circle, + color: Colors.grey, + size: 18, + )), + const TextSpan( + text: " ", + ), + TextSpan( + text: item.authors, + style: + const TextStyle(color: Colors.grey, fontSize: 14)) + ]), + ), + const SizedBox(height: 2), + Text(item.types ?? "", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 2), + Text(item.lastName ?? "", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + Center( + child: Obx( + () => UserService.instance.subscribedNovelIds.contains(item.id) + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + UserService.instance.cancelSubscribe( + [item.id], + AppConstant.kTypeNovel, + ); + }, + ) + : IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () { + UserService.instance.addSubscribe( + [item.id], + AppConstant.kTypeNovel, + ); + }, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/modules/novel/home/novel_home_controller.dart b/lib/modules/novel/home/novel_home_controller.dart new file mode 100644 index 0000000..7d01832 --- /dev/null +++ b/lib/modules/novel/home/novel_home_controller.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/modules/novel/home/category/novel_category_controller.dart'; +import 'package:flutter_dmzj/modules/novel/home/latest/novel_latest_controller.dart'; +import 'package:flutter_dmzj/modules/novel/home/rank/novel_rank_controller.dart'; +import 'package:flutter_dmzj/modules/novel/home/recommend/novel_recommend_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:get/get.dart'; + +class NovelHomeController extends GetxController + with GetTickerProviderStateMixin { + late TabController tabController; + + StreamSubscription? streamSubscription; + + @override + void onInit() { + streamSubscription = EventBus.instance.listen( + EventBus.kBottomNavigationBarClicked, + (index) { + if (index == 2) { + refreshOrScrollTop(); + } + }, + ); + tabController = TabController(length: 3, vsync: this); + + super.onInit(); + } + + void refreshOrScrollTop() { + var tabIndex = tabController.index; + BasePageController? controller; + if (tabIndex == 0) { + controller = Get.find(); + } else if (tabIndex == 1) { + controller = Get.find(); + } else if (tabIndex == 2) { + controller = Get.find(); + } else if (tabIndex == 3) { + controller = Get.find(); + } + controller?.scrollToTopOrRefresh(); + } + + void search() { + AppNavigator.toNovelSearch(); + } +} diff --git a/lib/modules/novel/home/novel_home_page.dart b/lib/modules/novel/home/novel_home_page.dart new file mode 100644 index 0000000..3dc2d9b --- /dev/null +++ b/lib/modules/novel/home/novel_home_page.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/modules/novel/home/category/novel_category_view.dart'; +import 'package:flutter_dmzj/modules/novel/home/latest/novel_latest_view.dart'; +import 'package:flutter_dmzj/modules/novel/home/novel_home_controller.dart'; +import 'package:flutter_dmzj/modules/novel/home/recommend/novel_recommend_view.dart'; +import 'package:flutter_dmzj/widgets/tab_appbar.dart'; +import 'package:flutter_dmzj/widgets/windows_tab_page.dart'; +import 'package:get/get.dart'; + +class NovelHomePage extends GetView { + const NovelHomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (PlatformUtils.isWindows) { + return WindowsTabPage( + tabs: [ + WindowsTabItem(label: '推荐', body: NovelRecommendView()), + WindowsTabItem(label: '更新', body: NovelLatestView()), + WindowsTabItem(label: '分类', body: NovelCategoryView()), + ], + headerAction: IconButton( + onPressed: controller.search, + icon: const Icon(Icons.search), + ), + ); + } + return Scaffold( + appBar: TabAppBar( + tabs: const [ + Tab(text: "推荐"), + Tab(text: "更新"), + Tab(text: "分类"), + //Tab(text: "排行"), + ], + controller: controller.tabController, + action: IconButton( + onPressed: controller.search, + icon: const Icon( + Icons.search, + ), + ), + ), + body: TabBarView( + controller: controller.tabController, + children: [ + NovelRecommendView(), + NovelLatestView(), + NovelCategoryView(), + //NovelRankView(), + ], + ), + ); + } +} + diff --git a/lib/modules/novel/home/rank/novel_rank_controller.dart b/lib/modules/novel/home/rank/novel_rank_controller.dart new file mode 100644 index 0000000..2832dca --- /dev/null +++ b/lib/modules/novel/home/rank/novel_rank_controller.dart @@ -0,0 +1,44 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/novel/rank_model.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class NovelRankController extends BasePageController { + final NovelRequest request = NovelRequest(); + RxMap tags = { + 0: "全部分类", + }.obs; + var tag = 0.obs; + + Map rankTypes = { + 0: "人气排行", + 1: "订阅排行", + }; + var rankType = 0.obs; + + @override + void onInit() { + loadFilter(); + super.onInit(); + } + + void loadFilter() async { + try { + tags.value = await request.rankFilter(); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.rank( + tagId: tag.value, + sort: rankType.value, + page: page - 1, + ); + + return ls; + } +} diff --git a/lib/modules/novel/home/rank/novel_rank_view.dart b/lib/modules/novel/home/rank/novel_rank_view.dart new file mode 100644 index 0000000..05d50f1 --- /dev/null +++ b/lib/modules/novel/home/rank/novel_rank_view.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/novel/rank_model.dart'; +import 'package:flutter_dmzj/modules/novel/home/rank/novel_rank_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class NovelRankView extends StatelessWidget { + final NovelRankController controller; + NovelRankView({Key? key}) + : controller = Get.put(NovelRankController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: Column( + children: [ + Obx( + () => Row( + children: [ + buildFilter( + // ignore: invalid_use_of_protected_member + types: controller.tags.value, + value: controller.tag.value, + onSelected: (e) { + controller.tag.value = e; + controller.refreshData(); + }, + ), + buildFilter( + types: controller.rankTypes, + value: controller.rankType.value, + onSelected: (e) { + controller.rankType.value = e; + controller.refreshData(); + }, + ), + ], + ), + ), + AppStyle.vGap12, + Expanded( + child: PageListView( + pageController: controller, + firstRefresh: true, + showPageLoadding: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ), + ], + ), + ); + } + + Widget buildFilter({ + required Map types, + required int value, + required Function(int) onSelected, + }) { + return Expanded( + child: PopupMenuButton( + onSelected: onSelected, + itemBuilder: (c) => types.keys + .map( + (k) => CheckedPopupMenuItem( + value: k, + checked: k == value, + child: Text(types[k] ?? ""), + ), + ) + .toList(), + child: SizedBox( + height: 36, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + types[value] ?? "", + ), + const Icon( + Icons.arrow_drop_down, + color: Colors.grey, + ) + ], + ), + ), + ), + ); + } + + Widget buildItem(NovelRankModel item) { + return InkWell( + onTap: () { + AppNavigator.toNovelDetail(item.id); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text.rich( + TextSpan(children: [ + const WidgetSpan( + child: Icon( + Icons.account_circle, + color: Colors.grey, + size: 18, + )), + const TextSpan( + text: " ", + ), + TextSpan( + text: item.authors, + style: + const TextStyle(color: Colors.grey, fontSize: 14)) + ]), + ), + const SizedBox(height: 2), + Text(item.types.join("/"), + style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 2), + Text(item.lastUpdateChapterName, + style: const TextStyle(color: Colors.grey, fontSize: 14)), + const SizedBox(height: 2), + Text("更新于${Utils.formatTimestamp(item.lastUpdateTime)}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + Center( + child: Obx( + () => UserService.instance.subscribedNovelIds.contains(item.id) + ? IconButton( + icon: const Icon(Icons.favorite), + onPressed: () { + UserService.instance.cancelSubscribe( + [item.id], + AppConstant.kTypeNovel, + ); + }, + ) + : IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () { + UserService.instance.addSubscribe( + [item.id], + AppConstant.kTypeNovel, + ); + }, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/modules/novel/home/recommend/novel_recommend_controller.dart b/lib/modules/novel/home/recommend/novel_recommend_controller.dart new file mode 100644 index 0000000..3485f42 --- /dev/null +++ b/lib/modules/novel/home/recommend/novel_recommend_controller.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/novel/recommend_model.dart'; +import 'package:flutter_dmzj/modules/novel/home/novel_home_controller.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class NovelRecommendController extends BasePageController { + final NovelRequest request = NovelRequest(); + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.recommend(); + + return ls; + } + + void openDetail(NovelRecommendItemModel item) { + //漫画=1 + if (item.type == null || item.type == 2) { + AppNavigator.toNovelDetail( + item.objId ?? item.id ?? 0, + ); + } else if (item.type == 1) { + //专题=5 + AppNavigator.toComicDetail( + item.objId ?? 0, + ); + } else if (item.type == 5) { + //专题=5 + AppNavigator.toSpecialDetail( + item.objId ?? 0, + ); + } else if (item.type == 6) { + //网页=6 + AppNavigator.toWebView(item.url ?? ""); + } else if (item.type == 7) { + //新闻=7 + AppNavigator.toNewsDetail( + url: item.url ?? "", + newsId: item.objId ?? 0, + title: item.title, + ); + } else if (item.type == 8) { + //作者=8 + AppNavigator.toComicAuthorDetail(item.objId ?? 0); + } else if (item.type == 13) { + //社区=13 + //直接跳转至网页 + launchUrlString( + "http://m.forum.idmzj.com/thread/detail?tid=${item.objId}"); + } else { + SmartDialog.showToast("未知类型,无法跳转"); + } + } + + void toLatest() { + var homeController = Get.find(); + homeController.tabController.animateTo(1); + } + + void toMySubscribe() {} +} diff --git a/lib/modules/novel/home/recommend/novel_recommend_view.dart b/lib/modules/novel/home/recommend/novel_recommend_view.dart new file mode 100644 index 0000000..d8aeaaf --- /dev/null +++ b/lib/modules/novel/home/recommend/novel_recommend_view.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/novel/recommend_model.dart'; +import 'package:flutter_dmzj/modules/novel/home/recommend/novel_recommend_controller.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:flutter_dmzj/widgets/refresh_until_widget.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_swiper_view/flutter_swiper_view.dart'; +import 'package:get/get.dart'; + +class NovelRecommendView extends StatelessWidget { + final NovelRecommendController controller; + NovelRecommendView({Key? key}) + : controller = Get.put(NovelRecommendController()), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + padding: AppStyle.edgeInsetsH12, + firstRefresh: true, + loadMore: false, + showPageLoadding: true, + itemBuilder: (context, i) { + var item = controller.list[i]; + if (item.categoryId == 57) { + return buildBanner(item); + } + + Widget? action; + if (item.categoryId == 58) { + action = buildShowMore(onTap: controller.toLatest); + } + return buildCard( + context, + child: buildTreeColumnGridView(item.data), + title: item.title.toString(), + action: action, + ); + }, + ), + ); + } + + Widget buildCard( + BuildContext context, { + required Widget child, + required String title, + Widget? action, + }) { + return Padding( + padding: AppStyle.edgeInsetsB8, + child: Container( + decoration: BoxDecoration( + borderRadius: AppStyle.radius8, + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 16, height: 1.0, fontWeight: FontWeight.bold), + ), + ), + SizedBox( + height: 48, + child: action, + ), + ], + ), + child, + ], + ), + ), + ); + } + + Widget buildShowMore({required Function() onTap}) { + return GestureDetector( + onTap: onTap, + child: const Row( + children: [ + Text( + "查看更多", + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + Icon(Icons.chevron_right, size: 18, color: Colors.grey), + ], + ), + ); + } + + Widget buildRefresh({required Future Function() onRefresh}) { + return RefreshUntilWidget(onRefresh: onRefresh, text: "换一批"); + } + + Widget buildBanner(NovelRecommendModel item) { + return Padding( + padding: AppStyle.edgeInsetsB12, + child: ClipRRect( + borderRadius: AppStyle.radius4, + child: AspectRatio( + aspectRatio: 7.5 / 4, + child: Swiper( + itemWidth: 750, + itemHeight: 400, + autoplay: true, + itemCount: item.data.length, + itemBuilder: (_, i) => NetImage( + item.data[i].cover, + width: 750, + height: 400, + ), + onTap: (i) { + controller.openDetail(item.data[i]); + }, + pagination: SwiperCustomPagination( + builder: (BuildContext context, SwiperPluginConfig config) { + return Align( + alignment: Alignment.bottomCenter, + child: Container( + padding: const EdgeInsets.only( + left: 8, + right: 12, + top: 4, + bottom: 4, + ), + //color: Colors.black12, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black38, + Colors.transparent, + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Text( + item.data[config.activeIndex].title, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 14, color: Colors.white), + ), + ), + AppStyle.hGap8, + PageIndicator( + controller: config.pageController!, + count: config.itemCount, + size: 10, + layout: PageIndicatorLayout.SCALE, + ), + ], + ), + ), + ); + }, + ), + ), + ), + ), + ); + } + + Widget buildTreeColumnGridView(List items) { + return MasonryGridView.count( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemCount: items.length, + itemBuilder: (_, i) { + var item = items[i]; + return InkWell( + onTap: () => controller.openDetail(item), + borderRadius: AppStyle.radius4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: AppStyle.radius4, + child: AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover, + width: 270, + height: 360, + ), + ), + ), + AppStyle.vGap8, + Text( + item.title, + maxLines: 1, + style: const TextStyle(height: 1.2), + overflow: TextOverflow.ellipsis, + ), + Text( + item.subTitle ?? item.status ?? '', + maxLines: 1, + style: const TextStyle( + height: 1.2, + fontSize: 12, + color: Colors.grey, + overflow: TextOverflow.ellipsis, + ), + ), + AppStyle.vGap8, + ], + ), + ); + }, + ); + } +} diff --git a/lib/modules/novel/reader/novel_horizontal_reader.dart b/lib/modules/novel/reader/novel_horizontal_reader.dart new file mode 100644 index 0000000..07e0290 --- /dev/null +++ b/lib/modules/novel/reader/novel_horizontal_reader.dart @@ -0,0 +1,405 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_dmzj/app/log.dart'; +import 'package:get/get.dart'; + +class NovelHorizontalReader extends StatefulWidget { + final String text; + final EdgeInsets? padding; + final TextStyle style; + final PageController? controller; + final bool reverse; + final Function(int index, int max)? onPageChanged; + const NovelHorizontalReader( + this.text, { + required this.style, + this.controller, + this.padding, + this.reverse = false, + this.onPageChanged, + Key? key, + }) : super(key: key); + + @override + State createState() => _NovelHorizontalReaderState(); +} + +class _NovelHorizontalReaderState extends State + with WidgetsBindingObserver { + List> textPages = []; + Size _lastSize = const Size(0, 0); + TextStyle textStyle = const TextStyle(); + double maxWidth = 500; + double maxHeight = 800; + String text = ""; + double fontHieght = 16.0; + EdgeInsets padding = EdgeInsets.zero; + + int index = 0; + + @override + void initState() { + super.initState(); + _lastSize = Get.size; + WidgetsBinding.instance.addObserver(this); + resetText(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + if (_lastSize != Get.size) { + _lastSize = Get.size; + resetText(); + } + } + + void resetText() { + text = widget.text; + textStyle = widget.style; + + padding = widget.padding ?? EdgeInsets.zero; + maxWidth = Get.width - padding.left - padding.right; + maxHeight = Get.height - + //AppStyle.statusBarHeight - + //AppStyle.bottomBarHeight - + padding.top - + padding.bottom; + if (text.isEmpty) { + setState(() { + textPages = []; + }); + return; + } + initText(); + } + + @override + void didUpdateWidget(covariant NovelHorizontalReader oldWidget) { + super.didUpdateWidget(oldWidget); + if ((widget.text != oldWidget.text) || + widget.style != oldWidget.style || + widget.padding != oldWidget.padding) { + if (widget.text != oldWidget.text) { + index = 0; + setState(() { + textPages = []; + }); + } + resetText(); + } + } + + /// 分割文本 + Future initText() async { + var startTime = DateTime.now().millisecondsSinceEpoch; + var fontSize = (textStyle.fontSize ?? 16).toDouble(); + var lineHeight = textStyle.height ?? 1.5; + // 计算出出各个类型的大小 + Size chineseCharSize = calcFontSize("中", + fontSize: fontSize.toDouble(), lineHeight: lineHeight); + fontHieght = chineseCharSize.height; + Size englishCharSize = calcFontSize("z", + fontSize: fontSize.toDouble(), lineHeight: lineHeight); + Size symbolCharSize = calcFontSize(",", + fontSize: fontSize.toDouble(), lineHeight: lineHeight); + Size spaceCharSize = calcFontSize(" ", + fontSize: fontSize.toDouble(), lineHeight: lineHeight); + // 计算可渲染的最大行数 + int maxLine = (maxHeight / chineseCharSize.height).floor(); + // 在新线程中进行分页 + + var pages = await compute( + splitText, + ComputeParameter( + content: text, + fontSize: fontSize.toDouble(), + width: maxWidth, + maxLine: maxLine, + lineHeight: lineHeight, + chineseWidth: chineseCharSize.width, + englishWidth: englishCharSize.width, + symbolWidth: symbolCharSize.width, + spaceWidth: spaceCharSize.width, + ), + ); + Log.d("耗时:${DateTime.now().millisecondsSinceEpoch - startTime}ms"); + Log.d("页数:${pages.length}"); + widget.onPageChanged?.call(index, pages.length); + setState(() { + textPages = pages; + }); + } + + /// 文本处理、分页 + /// 由于TextPainter.layout无法在isolate中使用,且计算极其耗时,所以手动写一个处理方法 + /// 处理一段12万字的文本,TextPainter.layout需要耗时16000ms左右;此方法则可以到1600ms,且能用isolate + /// 该方法还不是很完善,符号换行等还未实现,速度也可以再优化 + static List> splitText( + ComputeParameter parameter, + ) { + var str = parameter.content; + + Log.w("字数:${str.length}"); + + // 定义正则表达式(匹配中文字符、英文单词、符号、全角符号、数字串) + //RegExp reg = RegExp(r"([\u4e00-\u9fa5]|\b\w+\b|\x20| |\S|\p{Han}|\n)"); + RegExp reg = RegExp(r"([^\x00-\xff]|\b\w+\b|\p{P}|\x20|\S|\u3000|\n)"); + + // 使用正则表达式分割字符串 + List resultList = + reg.allMatches(str).map((match) => match.group(0) ?? "").toList(); + List chars = []; + final chineseExp = RegExp(r"[^\x00-\xff]"); + final wordExp = RegExp(r"\w+"); + + final symbolExp = RegExp(r"\p{P}"); + + final newLineExp = RegExp(r"\n"); + + for (var item in resultList) { + if (chineseExp.hasMatch(item)) { + chars.add( + CharInfo( + text: item, + width: parameter.chineseWidth, + type: CharType.chinese, + ), + ); + continue; + } + if (wordExp.hasMatch(item)) { + chars.add( + CharInfo( + text: item, + width: parameter.englishWidth * item.length, + type: CharType.word), + ); + continue; + } + if (newLineExp.hasMatch(item)) { + chars.add( + CharInfo(text: "", width: 0, type: CharType.newline), + ); + continue; + } + if (item == " ") { + chars.add( + CharInfo( + text: item, + width: parameter.spaceWidth, + type: CharType.symbol, + ), + ); + continue; + } + if (symbolExp.hasMatch(item)) { + chars.add( + CharInfo( + text: item, width: parameter.symbolWidth, type: CharType.symbol), + ); + continue; + } + + chars.add( + CharInfo( + text: item, + width: parameter.symbolWidth, + type: CharType.symbol, + ), + ); + } + + //开始分页 + List rows = []; + List> pages = []; + String rowStr = ""; + double rowWidth = 0; + for (var item in chars) { + //是否超出了最大行数 + if (rows.length >= parameter.maxLine) { + pages.add(rows); + rows = []; + } + //新行 + if (item.type == CharType.newline) { + rows.add(rowStr); + rowStr = ""; + rowWidth = 0; + //rowStr += item.text; + continue; + } + //是否超出了最大宽度 + if ((rowWidth + item.width) > parameter.width) { + rows.add(rowStr); + rowStr = ""; + rowWidth = 0; + } + rowStr += item.text; + rowWidth += item.width; + } + rows.add(rowStr); + pages.add(rows); + if (pages.length == 1 && + pages.first.length == 1 && + pages.first.first.isEmpty) { + return []; + } + return pages; + } + + /// 计算文字大小 + Size calcFontSize( + String text, { + required double fontSize, + required double lineHeight, + }) { + TextPainter textPainter = TextPainter( + text: TextSpan( + text: text, + style: TextStyle( + fontSize: fontSize, + height: lineHeight, + locale: PlatformDispatcher.instance.locale, + ), + ), + textDirection: TextDirection.ltr, + maxLines: 1, + ); + textPainter.layout(maxWidth: 200); + return textPainter.size; + } + + @override + Widget build(BuildContext context) { + return textPages.isEmpty + ? Center( + child: Text( + "加载中...", + style: widget.style, + ), + ) + : PageView.builder( + controller: widget.controller, + reverse: widget.reverse, + itemCount: textPages.length, + onPageChanged: (e) { + index = e; + widget.onPageChanged?.call(e, textPages.length); + }, + itemBuilder: (_, i) { + return Container( + padding: widget.padding ?? EdgeInsets.zero, + child: CustomPaint( + painter: NovelTextPainter( + textPages[i], + style: widget.style, + fontHieght: fontHieght, + ), + ), + ); + }, + ); + } +} + +class NovelTextPainter extends CustomPainter { + final TextStyle style; + final double fontHieght; + final List text; + NovelTextPainter( + this.text, { + required this.style, + required this.fontHieght, + }); + @override + void paint(Canvas canvas, Size size) { + var startTime = DateTime.now().millisecondsSinceEpoch; + + var i = 0; + for (var item in text) { + TextSpan textSpan = TextSpan( + text: item, + style: style, + ); + + final textPainter = TextPainter( + text: textSpan, + maxLines: 1, + textAlign: TextAlign.justify, + textDirection: TextDirection.ltr, + ); + textPainter.layout(maxWidth: size.width); + + final offset = Offset(0, i * fontHieght); + textPainter.paint(canvas, offset); + + i++; + } + Log.d("绘制单页耗时:${DateTime.now().millisecondsSinceEpoch - startTime}ms"); + } + + @override + bool shouldRepaint(covariant NovelTextPainter oldDelegate) { + return oldDelegate.style != style || + oldDelegate.text != text || + oldDelegate.fontHieght != fontHieght; + } +} + +enum CharType { + //中文及全角符号 + chinese, + //单词 + word, + //数字 + number, + //符号 + symbol, + //换行符 + newline +} + +class CharInfo { + CharType type; + String text; + double width; + CharInfo({ + required this.text, + required this.width, + required this.type, + }); + @override + String toString() { + return "($type,$width,$text)"; + } +} + +class ComputeParameter { + String content; + double width; + double fontSize; + double lineHeight; + int maxLine; + double chineseWidth; + double englishWidth; + double symbolWidth; + double spaceWidth; + ComputeParameter({ + required this.content, + required this.fontSize, + required this.width, + required this.maxLine, + required this.lineHeight, + required this.chineseWidth, + required this.englishWidth, + required this.symbolWidth, + required this.spaceWidth, + }); +} diff --git a/lib/modules/novel/reader/novel_reader_controller.dart b/lib/modules/novel/reader/novel_reader_controller.dart new file mode 100644 index 0000000..9153777 --- /dev/null +++ b/lib/modules/novel/reader/novel_reader_controller.dart @@ -0,0 +1,790 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:battery_plus/battery_plus.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/models/db/novel_download_info.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:html_unescape/html_unescape.dart'; +import 'package:remixicon/remixicon.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; + +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; + +class NovelReaderController extends BaseController { + final int novelId; + final String novelTitle; + final String novelCover; + final List chapters; + final FocusNode focusNode = FocusNode(); + NovelDetailChapter chapter; + NovelReaderController({ + required this.novelId, + required this.novelTitle, + required this.novelCover, + required this.chapters, + required this.chapter, + }) { + chapterIndex.value = chapters.indexOf(chapter); + } + + /// 当前章节索引 + var chapterIndex = 0.obs; + + /// 当前页面 + var currentIndex = 0.obs; + + /// 最大页面 + var maxPage = 0.obs; + + /// 阅读进度,百分比 + var progress = 0.0.obs; + + final AppSettingsService settings = AppSettingsService.instance; + final NovelRequest request = NovelRequest(); + + final PageController pageController = PageController(); + final ScrollController scrollController = ScrollController(); + + /// 连接信息监听 + StreamSubscription? connectivitySubscription; + + /// 电量信息监听 + StreamSubscription? batterySubscription; + + /// 连接类型 + Rx connectivityType = + Rx(ConnectivityResult.other); + + /// 电量信息 + Rx batteryLevel = 0.obs; + + /// 显示电量 + RxBool showBattery = true.obs; + + /// 文本内容 + var content = "".obs; + + /// 是否是图片 + var isPicture = false.obs; + + /// 是否为本地缓存 + var isLocal = false; + + /// 图片列表 + RxList pictures = RxList(); + + var contentLength = 0; + + /// 是否显示控制器 + var showControls = false.obs; + + /// 阅读方向 + var direction = 0.obs; + + /// 左手模式 + bool get leftHandMode => settings.novelReaderLeftHandMode.value; + + /// 翻页动画 + bool get pageAnimation => settings.novelReaderPageAnimation.value; + + @override + void onInit() { + initConnectivity(); + initBattery(); + direction.value = settings.novelReaderDirection.value; + + scrollController.addListener(listenVertical); + setFull(); + + loadContent(); + super.onInit(); + } + + /// 初始化电池信息 + void initBattery() async { + try { + //没有电池的Mac似乎会闪退,暂时屏蔽Mac + //https://github.com/xiaoyaocz/flutter_dmzj/discussions/146 + if (Platform.isMacOS) { + showBattery.value = false; + return; + } + var battery = Battery(); + batterySubscription = + battery.onBatteryStateChanged.listen((BatteryState state) async { + try { + var level = await battery.batteryLevel; + batteryLevel.value = level; + showBattery.value = true; + } catch (e) { + showBattery.value = false; + } + }); + batteryLevel.value = await battery.batteryLevel; + showBattery.value = true; + } catch (e) { + showBattery.value = false; + } + } + + /// 初始化连接状态 + void initConnectivity() async { + var connectivity = Connectivity(); + connectivitySubscription = + connectivity.onConnectivityChanged.listen((ConnectivityResult result) { + //提醒 + if (connectivityType.value != result && + result == ConnectivityResult.mobile) { + SmartDialog.showToast("您已切换至数据网络,请注意流量消耗"); + } + connectivityType.value = result; + }); + connectivityType.value = await connectivity.checkConnectivity(); + } + + /// 监听竖向模式时滚动百分比 + void listenVertical() { + if (scrollController.position.maxScrollExtent > 0) { + progress.value = scrollController.position.pixels / + scrollController.position.maxScrollExtent; + } + } + + @override + void onClose() { + scrollController.removeListener(listenVertical); + connectivitySubscription?.cancel(); + batterySubscription?.cancel(); + exitFull(); + uploadHistory(); + super.onClose(); + } + + /// 加载内容 + Future loadContent() async { + try { + pageLoadding.value = true; + pageError.value = false; + content.value = ""; + currentIndex.value = 0; + isLocal = false; + chapter = chapters[chapterIndex.value]; + + //查询本地是否存在 + var localInfo = NovelDownloadService.instance.box + .get("${novelId}_${chapter.volumeId}_${chapter.chapterId}"); + if (localInfo != null && localInfo.status == DownloadStatus.complete) { + return await loadFromLocal(localInfo); + } + + var text = await request.novelContent( + volumeId: chapter.volumeId, + chapterId: chapter.chapterId, + ); + + contentLength = text.length; + + var subStr = text.substring(0, text.length < 200 ? text.length : 200); + //检查是否是插画 + if (subStr.contains(RegExp(''))) { + List imgs = []; + for (var item + in RegExp(r'').allMatches(text)) { + var src = item.group(1); + if (src != null && src.isNotEmpty) { + imgs.add(src); + } + } + isPicture.value = true; + + pictures.value = imgs; + + content.value = text; + maxPage.value = pictures.length; + + SmartDialog.showToast("双击插画可放大、保存哦~"); + } else { + isPicture.value = false; + + text = HtmlUnescape().convert(text); + text = text + .replaceAll('\r\n', '\n') + .replaceAll("
", "\n") + .replaceAll('
', "\n") + .replaceAll('\n\n\n', "\n") + .replaceAll('\n\n', "\n") + .replaceAll('\n', "\n  ") + .replaceAll(RegExp(r"  \s+"), "  "); + + content.value = text; + } + if (scrollController.hasClients) { + scrollController.jumpTo(0); + progress.value = 0.0; + } + preloadContent(); + //TODO 阅读记录跳转 + //上传记录 + uploadHistory(); + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + + //SmartDialog.dismiss(status: SmartStatus.loading); + } + + Future loadFromLocal(NovelDownloadInfo local) async { + try { + isLocal = true; + var file = File(p.join(NovelDownloadService.instance.savePath, + local.taskId, local.fileName)); + + var text = await file.readAsString(); + + //检查是否是插画 + if (local.isImage) { + List imgs = local.imageFiles + .map((e) => + p.join(NovelDownloadService.instance.savePath, local.taskId, e)) + .toList(); + + isPicture.value = true; + + pictures.value = imgs; + + content.value = text; + maxPage.value = pictures.length; + + SmartDialog.showToast("双击插画可放大、保存哦~"); + } else { + isPicture.value = false; + + text = HtmlUnescape().convert(text); + text = text + .replaceAll('\r\n', '\n') + .replaceAll("
", "\n") + .replaceAll('
', "\n") + .replaceAll('\n\n\n', "\n") + .replaceAll('\n\n', "\n") + .replaceAll('\n', "\n  ") + .replaceAll(RegExp(r"  \s+"), "  "); + + content.value = text; + } + if (scrollController.hasClients) { + scrollController.jumpTo(0); + progress.value = 0.0; + } + preloadContent(); + //TODO 阅读记录跳转 + //上传记录 + uploadHistory(); + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + } + + /// 预加载下一话 + void preloadContent() async { + try { + if (chapterIndex.value == chapters.length - 1) { + return; + } + var nextChapter = chapters[chapterIndex.value + 1]; + await request.novelContent( + volumeId: nextChapter.volumeId, + chapterId: nextChapter.chapterId, + ); + } catch (e) { + Log.logPrint(e); + } + } + + /// 上传历史记录 + void uploadHistory() { + var chapter = chapters[chapterIndex.value]; + UserService.instance.updateNovelHistory( + novelId: novelId, + chapterId: chapter.chapterId, + //TODO 已读位置计算 + index: 0, + total: contentLength, + novelCover: novelCover, + novelName: novelTitle, + chapterName: chapter.chapterName, + volumeId: chapter.volumeId, + volumeName: chapter.volumeName, + ); + } + + /// 下一章 + void nextChapter() { + if (chapterIndex.value == chapters.length - 1) { + SmartDialog.showToast("后面没有了"); + return; + } + + chapterIndex.value += 1; + loadContent(); + } + + /// 上一章 + void forwardChapter() { + if (chapterIndex.value == 0) { + SmartDialog.showToast("前面没有了"); + return; + } + + chapterIndex.value -= 1; + loadContent(); + } + + /// 下一页 + void nextPage() { + if (direction.value == ReaderDirection.kUpToDown) { + return; + } + var value = currentIndex.value; + var max = maxPage.value; + if (value >= max - 1) { + nextChapter(); + } else { + jumpToPage(value + 1, anime: true); + } + } + + /// 上一页 + void forwardPage() { + if (direction.value == ReaderDirection.kUpToDown) { + return; + } + var value = currentIndex.value; + + if (value == 0) { + forwardChapter(); + } else { + jumpToPage(value - 1, anime: true); + } + } + + /// 跳转页数 + void jumpToPage(int page, {bool anime = false}) { + //竖向 + if (direction.value == ReaderDirection.kUpToDown) { + final viewportHeight = scrollController.position.viewportDimension; + scrollController.jumpTo(viewportHeight * page); + } else { + anime && pageAnimation + ? pageController.animateToPage(page, + duration: const Duration(milliseconds: 200), curve: Curves.linear) + : pageController.jumpToPage(page); + } + } + + /// 显示设置 + void showSettings() { + setShowControls(); + + showModalBottomSheet( + context: Get.context!, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + constraints: const BoxConstraints( + maxWidth: 500, + ), + builder: (context) => Column( + children: [ + ListTile( + title: const Text("设置"), + trailing: IconButton( + onPressed: Get.back, + icon: const Icon(Icons.close), + ), + contentPadding: AppStyle.edgeInsetsL12, + ), + Expanded( + child: Obx( + () => ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + buildBGItem( + context, + child: ListTile( + title: const Text("阅读方向"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + buildSelectedButton( + onTap: () { + setDirection(ReaderDirection.kLeftToRight); + }, + selected: settings.novelReaderDirection.value == + ReaderDirection.kLeftToRight, + child: const Icon(Remix.arrow_right_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + setDirection(ReaderDirection.kRightToLeft); + }, + selected: settings.novelReaderDirection.value == + ReaderDirection.kRightToLeft, + child: const Icon(Remix.arrow_left_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + setDirection(ReaderDirection.kUpToDown); + }, + selected: settings.novelReaderDirection.value == + ReaderDirection.kUpToDown, + child: const Icon(Remix.arrow_down_line), + ) + ], + ), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: ListTile( + title: const Text("阅读主题"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: AppColor.novelThemes.keys + .map( + (e) => GestureDetector( + onTap: () { + settings.setNovelReaderTheme(e); + }, + child: Container( + margin: AppStyle.edgeInsetsL8, + height: 36, + width: 36, + decoration: BoxDecoration( + color: AppColor.novelThemes[e]!.first, + borderRadius: AppStyle.radius24, + border: Border.all( + color: Colors.grey.withOpacity(.2), + ), + ), + child: Visibility( + visible: AppColor.novelThemes.keys + .toList() + .indexOf(e) == + settings.novelReaderTheme.value, + child: Icon( + Icons.check, + color: AppColor.novelThemes[e]!.last, + ), + ), + ), + ), + ) + .toList(), + ), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: SwitchListTile( + value: settings.novelReaderLeftHandMode.value, + onChanged: (e) { + settings.setNovelReaderLeftHandMode(e); + }, + title: const Text("操作反转"), + subtitle: const Text("点击左侧下一页,右侧上一页"), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: SwitchListTile( + value: settings.novelReaderShowStatus.value, + onChanged: (e) { + settings.setNovelReaderShowStatus(e); + }, + title: const Text("显示状态信息"), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: SwitchListTile( + value: settings.novelReaderPageAnimation.value, + onChanged: (e) { + settings.setNovelReaderPageAnimation(e); + }, + title: const Text("翻页动画"), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: ListTile( + title: const Text("字体大小"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + OutlinedButton( + onPressed: () { + settings.setNovelReaderFontSize( + settings.novelReaderFontSize.value + 1, + ); + }, + child: const Icon( + Icons.add, + // color: Colors.grey, + ), + ), + AppStyle.hGap12, + Text("${settings.novelReaderFontSize.value}"), + AppStyle.hGap12, + OutlinedButton( + onPressed: () { + settings.setNovelReaderFontSize( + settings.novelReaderFontSize.value - 1, + ); + }, + child: const Icon( + Icons.remove, + // color: Colors.grey, + ), + ), + ], + ), + ), + ), + AppStyle.vGap12, + buildBGItem( + context, + child: ListTile( + title: const Text("行距"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + OutlinedButton( + onPressed: () { + settings.setNovelReaderLineSpacing( + settings.novelReaderLineSpacing.value + 0.1, + ); + }, + child: const Icon( + Icons.add, + // color: Colors.grey, + ), + ), + AppStyle.hGap12, + Text((settings.novelReaderLineSpacing.value) + .toStringAsFixed(1)), + AppStyle.hGap12, + OutlinedButton( + onPressed: () { + settings.setNovelReaderLineSpacing( + settings.novelReaderLineSpacing.value - 0.1, + ); + }, + child: const Icon( + Icons.remove, + // color: Colors.grey, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + /// 设置阅读方向 + void setDirection(int value) { + settings.setNovelReaderDirection(value); + direction.value = value; + } + + Widget buildBGItem(BuildContext context, {required Widget child}) { + return Container( + decoration: BoxDecoration( + borderRadius: AppStyle.radius8, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + child: child, + ); + } + + Widget buildSelectedButton( + {required Widget child, bool selected = false, Function()? onTap}) { + return Builder(builder: (context) { + return OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: + selected ? Theme.of(context).colorScheme.primary : Colors.grey, + side: BorderSide( + color: + selected ? Theme.of(context).colorScheme.primary : Colors.grey, + ), + ), + onPressed: onTap, + child: child, + ); + }); + } + + /// 显示目录 + void showMenu() { + setShowControls(); + showModalBottomSheet( + context: Get.context!, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + constraints: const BoxConstraints( + maxWidth: 500, + ), + builder: (context) => Column( + children: [ + ListTile( + title: Text("目录(${chapters.length})"), + trailing: IconButton( + onPressed: Get.back, + icon: const Icon(Icons.close), + ), + contentPadding: AppStyle.edgeInsetsL12, + ), + Divider( + height: 1.0, + color: Theme.of(context).dividerColor.withOpacity(.2), + ), + Expanded( + child: ScrollablePositionedList.separated( + initialScrollIndex: chapterIndex.value, + itemCount: chapters.length, + separatorBuilder: (_, i) => Divider( + indent: 12, + endIndent: 12, + height: 1.0, + color: Theme.of(context).dividerColor.withOpacity(.2), + ), + itemBuilder: (_, i) { + var item = chapters[i]; + return ListTile( + selected: i == chapterIndex.value, + selectedTileColor: + Theme.of(context).colorScheme.secondaryContainer, + selectedColor: + Theme.of(context).colorScheme.onSecondaryContainer, + title: Text(item.chapterName), + subtitle: Text(item.volumeName), + onTap: () { + chapterIndex.value = i; + loadContent(); + Get.back(); + }, + ); + }, + ), + ), + ], + ), + routeSettings: const RouteSettings(name: "/modalBottomSheet"), + ); + } + + /// 设置显示/隐藏控制按钮 + void setShowControls() { + if (settings.novelReaderFullScreen.value) { + if (showControls.value) { + setFull(); + } else { + setFullEdge(); + } + } + Future.delayed(const Duration(milliseconds: 100), () { + showControls.value = !showControls.value; + }); + } + + /// 进入全屏 + void setFull() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: [], + ); + } + + /// 进入全屏edgeToEdge模式 + void setFullEdge() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + overlays: SystemUiOverlay.values, + ); + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: Brightness.light, + )); + } + + /// 退出全屏 + void exitFull() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + overlays: SystemUiOverlay.values, + ); + } + + void keyDown(LogicalKeyboardKey key) { + if (key == LogicalKeyboardKey.arrowLeft || + key == LogicalKeyboardKey.pageUp) { + if (leftHandMode) { + nextPage(); + } else { + forwardPage(); + } + } else if (key == LogicalKeyboardKey.arrowRight || + key == LogicalKeyboardKey.pageDown) { + if (leftHandMode) { + forwardPage(); + } else { + nextPage(); + } + } + } +} diff --git a/lib/modules/novel/reader/novel_reader_page.dart b/lib/modules/novel/reader/novel_reader_page.dart new file mode 100644 index 0000000..ea75df1 --- /dev/null +++ b/lib/modules/novel/reader/novel_reader_page.dart @@ -0,0 +1,670 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/modules/novel/reader/novel_horizontal_reader.dart'; + +import 'package:flutter_dmzj/modules/novel/reader/novel_reader_controller.dart'; +import 'package:flutter_dmzj/widgets/custom_header.dart'; +import 'package:flutter_dmzj/widgets/local_image.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class NovelReaderPage extends GetView { + const NovelReaderPage({Key? key}) : super(key: key); + + Color get color => + AppColor.novelThemes[controller.settings.novelReaderTheme.value]!.last; + + @override + Widget build(BuildContext context) { + return KeyboardListener( + onKeyEvent: (e) { + if (e.runtimeType == KeyUpEvent) { + controller.keyDown(e.logicalKey); + Log.d(e.toString()); + } + }, + focusNode: controller.focusNode, + autofocus: true, + child: Theme( + data: Theme.of(context), + child: Obx( + () => Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: AppColor + .novelThemes[controller.settings.novelReaderTheme.value]!.first, + body: Stack( + children: [ + Obx( + () => Offstage( + offstage: controller.content.value.isEmpty, + child: GestureDetector( + onTap: () { + controller.setShowControls(); + }, + child: controller.isPicture.value + ? buildPicture(context) + : (controller.direction.value == + ReaderDirection.kUpToDown + ? buildVertical(context) + : buildHorizontal(context)), + ), + ), + ), + Positioned.fill( + child: Row( + children: [ + Expanded( + flex: 1, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + controller.leftHandMode + ? controller.nextPage() + : controller.forwardPage(); + }, + child: Container( + width: double.infinity, + height: double.infinity, + color: Colors.transparent, + ), + ), + ), + Expanded( + flex: 8, + child: Container(), + ), + Expanded( + flex: 1, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + controller.leftHandMode + ? controller.forwardPage() + : controller.nextPage(); + }, + child: Container( + width: double.infinity, + height: double.infinity, + color: Colors.transparent, + ), + ), + ), + ], + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadContent(), + ), + ), + ), + buildBottomStatus(), + //顶部 + Obx( + () => AnimatedPositioned( + top: controller.showControls.value + ? 0 + : -(64 + AppStyle.statusBarHeight), + left: 0, + right: 0, + duration: const Duration(milliseconds: 100), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + height: 64 + AppStyle.statusBarHeight, + padding: EdgeInsets.only(top: AppStyle.statusBarHeight), + child: Row( + children: [ + IconButton( + onPressed: Get.back, + icon: const Icon(Icons.arrow_back), + ), + AppStyle.hGap12, + Expanded( + child: Text( + controller.chapters[controller.chapterIndex.value] + .chapterName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ], + ), + ), + ), + ), + //底部 + Obx( + () => AnimatedPositioned( + bottom: controller.showControls.value + ? 0 + : -(136 + AppStyle.bottomBarHeight), + left: 0, + right: 0, + duration: const Duration(milliseconds: 100), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + height: 136 + AppStyle.bottomBarHeight, + padding: + EdgeInsets.only(bottom: AppStyle.bottomBarHeight), + alignment: Alignment.center, + child: Container( + constraints: const BoxConstraints( + maxWidth: 600, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + buildSilderBar(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton.filledTonal( + onPressed: controller.forwardChapter, + icon: const Icon(Remix.skip_back_line), + tooltip: "上一话", + ), + IconButton.filledTonal( + onPressed: controller.showMenu, + icon: const Icon(Remix.file_list_line), + tooltip: "目录", + ), + IconButton.filledTonal( + onPressed: controller.showSettings, + icon: const Icon(Remix.settings_line), + tooltip: "设置", + ), + IconButton.filledTonal( + onPressed: controller.nextChapter, + icon: const Icon(Remix.skip_forward_line), + tooltip: "下一话", + ), + ], + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget buildHorizontal(BuildContext context) { + return EasyRefresh( + header: MaterialHeader2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_left, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + footer: MaterialFooter2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_right, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + refreshOnStart: false, + onRefresh: () async { + controller.forwardChapter(); + }, + onLoad: () async { + controller.nextChapter(); + }, + child: NovelHorizontalReader( + controller.content.value, + controller: controller.pageController, + reverse: controller.direction.value == ReaderDirection.kRightToLeft, + style: TextStyle( + fontSize: controller.settings.novelReaderFontSize.value.toDouble(), + height: controller.settings.novelReaderLineSpacing.value, + color: AppColor + .novelThemes[controller.settings.novelReaderTheme.value]!.last, + ), + padding: AppStyle.edgeInsetsA12.copyWith( + top: AppStyle.statusBarHeight + 12, + bottom: (controller.settings.novelReaderShowStatus.value ? 24 : 12), + ), + onPageChanged: (i, m) { + controller.currentIndex.value = i; + controller.maxPage.value = m; + }, + ), + ); + } + + Widget buildVertical(BuildContext context) { + return SizedBox( + height: double.infinity, + child: Padding( + padding: EdgeInsets.only( + top: AppStyle.statusBarHeight, + ), + child: Padding( + padding: AppStyle.edgeInsetsA12.copyWith( + bottom: + (controller.settings.novelReaderShowStatus.value ? 32 : 12), + ), + child: EasyRefresh( + header: MaterialHeader2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_up, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + footer: MaterialFooter2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + Icons.arrow_circle_down, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + refreshOnStart: false, + onRefresh: () async { + controller.forwardChapter(); + }, + onLoad: () async { + controller.nextChapter(); + }, + child: SingleChildScrollView( + controller: controller.scrollController, + child: Text( + controller.content.value, + textAlign: TextAlign.justify, + style: TextStyle( + fontSize: + controller.settings.novelReaderFontSize.value.toDouble(), + height: controller.settings.novelReaderLineSpacing.value, + color: AppColor + .novelThemes[controller.settings.novelReaderTheme.value]! + .last, + ), + ), + ), + ), + ), + ), + ); + } + + Widget buildPicture(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + top: AppStyle.statusBarHeight, + ), + child: EasyRefresh( + header: MaterialHeader2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + controller.direction.value != ReaderDirection.kUpToDown + ? Icons.arrow_circle_left + : Icons.arrow_circle_up, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + footer: MaterialFooter2( + triggerOffset: 80, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: AppStyle.radius24, + ), + padding: AppStyle.edgeInsetsA12, + child: Icon( + controller.direction.value != ReaderDirection.kUpToDown + ? Icons.arrow_circle_right + : Icons.arrow_circle_down, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + refreshOnStart: false, + onRefresh: () async { + controller.forwardChapter(); + }, + onLoad: () async { + controller.nextChapter(); + }, + child: controller.direction.value != ReaderDirection.kUpToDown + ? PageView.builder( + controller: controller.pageController, + itemCount: controller.pictures.length, + reverse: + controller.direction.value == ReaderDirection.kRightToLeft, + onPageChanged: (e) { + controller.currentIndex.value = e; + controller.maxPage.value = controller.pictures.length; + }, + itemBuilder: (_, i) { + return Padding( + padding: EdgeInsets.only( + bottom: (controller.settings.novelReaderShowStatus.value + ? 24 + : 12), + ), + child: GestureDetector( + onDoubleTap: () { + DialogUtils.showImageViewer( + i, controller.pictures.toList()); + }, + child: controller.isLocal + ? LocalImage( + controller.pictures[i], + fit: BoxFit.contain, + ) + : NetImage( + controller.pictures[i], + fit: BoxFit.contain, + progress: true, + ), + ), + ); + }) + : ListView.separated( + controller: controller.scrollController, + itemCount: controller.pictures.length, + padding: EdgeInsets.zero, + separatorBuilder: (_, i) => AppStyle.vGap4, + itemBuilder: (_, i) { + return GestureDetector( + onDoubleTap: () { + DialogUtils.showImageViewer( + i, controller.pictures.toList()); + }, + child: controller.isLocal + ? LocalImage( + controller.pictures[i], + fit: BoxFit.fitWidth, + ) + : NetImage( + controller.pictures[i], + fit: BoxFit.fitWidth, + progress: true, + ), + ); + }), + ), + ); + } + + Widget buildSilderBar() { + if (controller.direction.value == ReaderDirection.kUpToDown) { + return Obx( + () { + var value = controller.progress.value; + var max = 1.0; + if (value > max) { + return const SizedBox( + height: 48, + ); + } + return SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Slider( + value: value, + max: max, + onChanged: (e) { + controller.scrollController.jumpTo( + controller.scrollController.position.maxScrollExtent * + e, + ); + }, + ), + ), + ], + ), + ); + }, + ); + } + return Obx( + () { + var value = controller.currentIndex.value + 1.0; + var max = controller.maxPage.value; + if (value > max) { + return const SizedBox( + height: 48, + ); + } + return SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: Slider( + value: value, + max: max.toDouble(), + onChanged: (e) { + controller.jumpToPage((e - 1).toInt()); + }, + ), + ), + ], + ), + ); + }, + ); + } + + Widget buildBottomStatus() { + return Positioned( + right: 8, + left: 8, + bottom: 4, + child: Obx( + () => Offstage( + offstage: !controller.settings.novelReaderShowStatus.value, + child: Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + padding: AppStyle.edgeInsetsA12.copyWith(top: 4, bottom: 4), + child: Obx( + () => Row( + children: [ + buildConnectivity(), + buildBattery(), + const Expanded(child: SizedBox()), + controller.direction.value != ReaderDirection.kUpToDown + ? Text( + "${controller.currentIndex.value + 1} / ${controller.maxPage.value}", + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ) + : Text( + "${(controller.progress.value * 100).toStringAsFixed(0)}%", + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget buildConnectivity() { + var connectivityType = controller.connectivityType.value; + IconData icon = Remix.wifi_line; + var name = "WiFi"; + switch (connectivityType) { + case ConnectivityResult.bluetooth: + icon = Remix.wifi_line; + name = "蓝牙"; + break; + case ConnectivityResult.ethernet: + icon = Remix.computer_line; + name = "有线"; + break; + case ConnectivityResult.mobile: + icon = Remix.base_station_line; + name = "流量"; + break; + case ConnectivityResult.wifi: + icon = Remix.wifi_line; + name = "WiFi"; + break; + case ConnectivityResult.vpn: + icon = Remix.shield_keyhole_line; + name = "VPN"; + break; + case ConnectivityResult.none: + icon = Remix.wifi_off_line; + name = "无网络"; + break; + case ConnectivityResult.other: + icon = Remix.question_line; + name = "未知"; + break; + default: + } + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon, size: 12, color: Colors.white), + AppStyle.hGap4, + Text( + name, + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + AppStyle.hGap8, + ], + ); + } + + Widget buildBattery() { + var battery = controller.batteryLevel.value; + // IconData icon = Icons.battery_0_bar; + // if (battery >= 90) { + // icon = Icons.battery_full_rounded; + // } else if (battery < 90 && battery >= 80) { + // icon = Icons.battery_6_bar; + // } else if (battery < 80 && battery >= 70) { + // icon = Icons.battery_5_bar; + // } else if (battery < 70 && battery >= 50) { + // icon = Icons.battery_4_bar; + // } else if (battery < 50 && battery >= 30) { + // icon = Icons.battery_3_bar; + // } else if (battery < 30 && battery >= 20) { + // icon = Icons.battery_2_bar; + // } else if (battery < 20 && battery >= 10) { + // icon = Icons.battery_1_bar; + // } else { + // icon = Icons.battery_0_bar; + // } + return Visibility( + visible: controller.showBattery.value, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + //Icon(icon, size: 12, color: color.withOpacity(.6)), + Text( + "电量 $battery%", + style: const TextStyle( + fontSize: 12, + height: 1.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + AppStyle.hGap8, + ], + ), + ); + } +} diff --git a/lib/modules/novel/search/novel_search_controller.dart b/lib/modules/novel/search/novel_search_controller.dart new file mode 100644 index 0000000..15dc788 --- /dev/null +++ b/lib/modules/novel/search/novel_search_controller.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/novel/search_model.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:get/get.dart'; + +class NovelSearchController extends BasePageController { + final String keyword; + NovelSearchController(this.keyword) { + searchController = TextEditingController(text: keyword); + } + late TextEditingController searchController; + final NovelRequest request = NovelRequest(); + + String _keyword = ""; + RxMap hotWords = {}.obs; + var showHotWord = true.obs; + + @override + void onInit() { + // loadHotWord(); + if (keyword.isNotEmpty) { + submit(); + } + super.onInit(); + } + + void submit() { + if (searchController.text.isEmpty) { + list.clear(); + showHotWord.value = true; + return; + } + showHotWord.value = false; + _keyword = searchController.text; + refreshData(); + } + + @override + Future> getData(int page, int pageSize) async { + if (searchController.text.isEmpty) { + return []; + } + return await request.search(keyword: _keyword, page: page); + } + + void loadHotWord() async { + try { + hotWords.value = await request.searchHotWord(); + } catch (e) { + Log.logPrint(e); + } + } +} diff --git a/lib/modules/novel/search/novel_search_page.dart b/lib/modules/novel/search/novel_search_page.dart new file mode 100644 index 0000000..1e9ee0e --- /dev/null +++ b/lib/modules/novel/search/novel_search_page.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/novel/search_model.dart'; +import 'package:flutter_dmzj/modules/novel/search/novel_search_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class NovelSearchPage extends StatelessWidget { + final String keyword; + final NovelSearchController controller; + NovelSearchPage({this.keyword = "", super.key}) + : controller = Get.put(NovelSearchController(keyword)); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 8, + title: SizedBox( + height: 40, + child: TextField( + controller: controller.searchController, + autofocus: true, + decoration: InputDecoration( + hintText: "搜索轻小说", + contentPadding: AppStyle.edgeInsetsH12, + border: const OutlineInputBorder(), + prefixIcon: SizedBox( + width: 48, + child: IconButton( + onPressed: () { + AppNavigator.closePage(); + }, + icon: const Icon(Icons.arrow_back), + ), + ), + suffixIcon: SizedBox( + width: 48, + child: IconButton( + onPressed: controller.submit, + icon: const Icon(Icons.search), + ), + ), + ), + onSubmitted: (e) { + controller.submit(); + }, + ), + ), + ), + body: Stack( + children: [ + PageListView( + pageController: controller, + firstRefresh: false, + showPageLoadding: true, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + Positioned.fill( + child: Obx( + () => Offstage( + offstage: !controller.showHotWord.value, + child: SingleChildScrollView( + child: Column( + children: [ + const ListTile( + title: Text("热门搜索"), + ), + Padding( + padding: AppStyle.edgeInsetsH12.copyWith(bottom: 12), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: controller.hotWords.keys + .map( + (e) => OutlinedButton( + style: OutlinedButton.styleFrom( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + AppNavigator.toNovelDetail(e); + }, + child: Text(controller.hotWords[e] ?? ""), + ), + ) + .toList(), + ), + ) + ], + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildItem(NovelSearchModel item) { + return InkWell( + onTap: () { + AppNavigator.toNovelDetail(item.id); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover ?? "", + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text.rich( + TextSpan(children: [ + const WidgetSpan( + child: Icon( + Icons.account_circle, + color: Colors.grey, + size: 18, + )), + const TextSpan( + text: " ", + ), + TextSpan( + text: item.authors, + style: + const TextStyle(color: Colors.grey, fontSize: 14)) + ]), + ), + AppStyle.vGap4, + Text(item.types ?? "", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + AppStyle.vGap4, + Text(item.lastName ?? "", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/novel/select_chapter/novel_select_chapter_controller.dart b/lib/modules/novel/select_chapter/novel_select_chapter_controller.dart new file mode 100644 index 0000000..02081d5 --- /dev/null +++ b/lib/modules/novel/select_chapter/novel_select_chapter_controller.dart @@ -0,0 +1,126 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; + +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class NovelSelectChapterController extends BaseController { + final int novelId; + NovelSelectChapterController(this.novelId); + final NovelRequest request = NovelRequest(); + + RxList volumes = RxList(); + + String novelTitle = ""; + String novelCover = ""; + + RxMap> selectIds = RxMap>(); + + @override + void onInit() { + loadDetail(); + + super.onInit(); + } + + /// 加载信息 + void loadDetail() async { + try { + pageLoadding.value = true; + pageError.value = false; + var result = await request.novelDetail(novelId: novelId); + novelTitle = result.data.name; + novelCover = result.data.cover; + var chpaterResult = await request.novelChapter(novelId: novelId); + var ls = chpaterResult.map((e) => NovelDetailVolume.fromJson(e)).toList(); + selectIds.value = {}; + for (var item in ls) { + selectIds.addAll({ + item.volumeId: RxSet(), + }); + } + volumes.value = ls; + } catch (e) { + pageError.value = true; + errorMsg.value = e.toString(); + } finally { + pageLoadding.value = false; + } + } + + void selectItem(NovelDetailChapter item) { + var chapterIds = selectIds[item.volumeId]!; + if (chapterIds.contains(item.chapterId)) { + chapterIds.remove(item.chapterId); + } else { + chapterIds.add(item.chapterId); + } + } + + void selectAll() { + for (var volume in volumes) { + for (var chapter in volume.chapters) { + var chapterIds = selectIds[volume.volumeId]!; + var id = "${novelId}_${volume.volumeId}_${chapter.chapterId}"; + if (!NovelDownloadService.instance.downloadIds.contains(id)) { + chapterIds.add(chapter.chapterId); + } + } + } + } + + void cleanAll() { + for (var volume in selectIds.values) { + volume.clear(); + } + } + + void toDownloadManage() { + AppNavigator.toNovelDownloadManage(1); + } + + void startDownload() { + var chapterIds = []; + for (var item in selectIds.values) { + chapterIds.addAll(item); + } + if (chapterIds.isEmpty) { + SmartDialog.showToast("请选择需要下载的章节"); + return; + } + for (var id in chapterIds) { + //搜索章节 + NovelDetailVolume? volume; + NovelDetailChapter? chapter; + for (var item in volumes) { + var chapterItem = + item.chapters.firstWhereOrNull((y) => y.chapterId == id); + if (chapterItem != null) { + volume = item; + chapter = chapterItem; + break; + } + } + if (volume == null || chapter == null) { + continue; + } + NovelDownloadService.instance.addTask( + novelId: novelId, + chapterId: chapter.chapterId, + chapterSort: chapter.chapterOrder, + volumeName: volume.volumeName, + novelTitle: novelTitle, + novelCover: novelCover, + chapterName: chapter.chapterName, + isVip: false, + volumeId: volume.volumeId, + volumeOrder: volume.volumeOrder, + ); + } + cleanAll(); + SmartDialog.showToast("已添加到下载列表,下载过程中请保持APP在前台运行"); + } +} diff --git a/lib/modules/novel/select_chapter/novel_select_chapter_page.dart b/lib/modules/novel/select_chapter/novel_select_chapter_page.dart new file mode 100644 index 0000000..5caf564 --- /dev/null +++ b/lib/modules/novel/select_chapter/novel_select_chapter_page.dart @@ -0,0 +1,175 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; + +import 'package:flutter_dmzj/modules/novel/select_chapter/novel_select_chapter_controller.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class NovelSelectChapterPage extends StatelessWidget { + final int novelId; + final NovelSelectChapterController controller; + NovelSelectChapterPage(this.novelId, {super.key}) + : controller = Get.put( + NovelSelectChapterController(novelId), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("选择下载章节"), + actions: [ + TextButton( + onPressed: controller.toDownloadManage, + child: const Text("下载管理"), + ), + ], + ), + body: Stack( + children: [ + EasyRefresh( + header: const MaterialHeader(), + onRefresh: controller.loadDetail, + child: _buildVolumes(), + ), + Obx( + () => Offstage( + offstage: !controller.pageLoadding.value, + child: const AppLoaddingWidget(), + ), + ), + Obx( + () => Offstage( + offstage: !controller.pageError.value, + child: AppErrorWidget( + errorMsg: controller.errorMsg.value, + onRefresh: () => controller.loadDetail(), + ), + ), + ), + ], + ), + bottomNavigationBar: BottomAppBar( + child: SizedBox( + height: 48, + child: Row( + children: [ + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.selectAll, + icon: const Icon( + Remix.checkbox_line, + size: 20, + ), + label: const Text("全选"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.cleanAll, + icon: const Icon( + Remix.checkbox_blank_line, + size: 20, + ), + label: const Text("取消选中"), + ), + ), + Expanded( + child: TextButton.icon( + style: TextButton.styleFrom( + textStyle: const TextStyle(fontSize: 14), + ), + onPressed: controller.startDownload, + icon: const Icon( + Remix.download_line, + size: 20, + ), + label: const Text("下载选中"), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildVolumes() { + return Obx( + () => ListView.builder( + padding: EdgeInsets.zero, + itemCount: controller.volumes.length, + itemBuilder: (_, i) { + var item = controller.volumes[i]; + return _buildChapters(item); + }, + ), + ); + } + + Widget _buildChapters(NovelDetailVolume item) { + return Obx( + () { + var volume = controller.selectIds[item.volumeId]!; + return ExpansionTile( + title: Text("${item.volumeName}(共${item.chapters.length}话)"), + leading: SizedBox( + width: 40, + child: Checkbox( + value: volume.length == item.chapters.length, + onChanged: (e) { + if (e!) { + volume.addAll( + item.chapters + .where((x) => !NovelDownloadService.instance.downloadIds + .contains( + "${novelId}_${x.volumeId}_${x.chapterId}")) + .map((e) => e.chapterId), + ); + } else { + volume.clear(); + } + }, + ), + ), + children: item.chapters + .map( + (chapter) => CheckboxListTile( + value: volume.contains(chapter.chapterId), + controlAffinity: ListTileControlAffinity.leading, + title: Text( + chapter.chapterName, + style: Get.textTheme.titleSmall, + ), + enabled: !NovelDownloadService.instance.downloadIds.contains( + "${novelId}_${chapter.volumeId}_${chapter.chapterId}"), + subtitle: NovelDownloadService.instance.downloadIds.contains( + "${novelId}_${chapter.volumeId}_${chapter.chapterId}") + ? const Text("已下载") + : null, + onChanged: (e) { + if (e!) { + volume.add(chapter.chapterId); + } else { + volume.remove(chapter.chapterId); + } + }, + ), + ) + .toList(), + ); + }, + ); + } +} diff --git a/lib/modules/user/comment/user_comment_controller.dart b/lib/modules/user/comment/user_comment_controller.dart new file mode 100644 index 0000000..9be3315 --- /dev/null +++ b/lib/modules/user/comment/user_comment_controller.dart @@ -0,0 +1,19 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/comment/user_comment_item.dart'; +import 'package:flutter_dmzj/requests/comment_request.dart'; + +class UserCommentController extends BasePageController { + final int type; + final int userId; + UserCommentController({required this.type, required this.userId}); + final CommentRequest request = CommentRequest(); + + @override + Future> getData(int page, int pageSize) async { + return await request.getUserComment( + type: type, + uid: userId, + page: page - 1, + ); + } +} diff --git a/lib/modules/user/comment/user_comment_page.dart b/lib/modules/user/comment/user_comment_page.dart new file mode 100644 index 0000000..040824a --- /dev/null +++ b/lib/modules/user/comment/user_comment_page.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/user/comment/user_comment_view.dart'; +import 'package:get/get.dart'; + +class UserCommentPage extends StatelessWidget { + final int userId; + const UserCommentPage(this.userId, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 3, + child: Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 56), + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + labelPadding: AppStyle.edgeInsetsH24, + indicatorColor: Theme.of(context).colorScheme.primary, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: const [ + Tab(text: "漫画"), + Tab(text: "小说"), + Tab(text: "新闻"), + ], + ), + ), + ), + body: TabBarView( + children: [ + UserCommentView(type: 0, userId: userId), + UserCommentView(type: 1, userId: userId), + UserCommentView(type: 2, userId: userId), + ], + ), + ), + ); + } +} diff --git a/lib/modules/user/comment/user_comment_view.dart b/lib/modules/user/comment/user_comment_view.dart new file mode 100644 index 0000000..89c8633 --- /dev/null +++ b/lib/modules/user/comment/user_comment_view.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/comment/user_comment_item.dart'; +import 'package:flutter_dmzj/modules/user/comment/user_comment_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class UserCommentView extends StatelessWidget { + final int type; + final int userId; + final UserCommentController controller; + UserCommentView({ + required this.type, + required this.userId, + Key? key, + }) : controller = Get.put( + UserCommentController( + type: type, + userId: userId, + ), + tag: "${userId}_$type", + ), + super(key: key); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + //TODO 跳转评论详情 + return Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + toDetail(item); + }, + child: NetImage( + item.objCover, + width: 60, + borderRadius: 4, + ), + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + item.objName, + ), + AppStyle.vGap8, + Container( + padding: AppStyle.edgeInsetsA8, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.1), + borderRadius: AppStyle.radius4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(item.content), + Visibility( + visible: item.mastercomment != null, + child: Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.1), + borderRadius: AppStyle.radius4, + ), + padding: AppStyle.edgeInsetsA4, + margin: AppStyle.edgeInsetsV4, + child: Text( + "${item.mastercomment?.nickname}:${item.mastercomment?.content}", + style: const TextStyle( + fontSize: 14, + ), + ), + ), + ), + AppStyle.vGap4, + Row( + children: [ + const Icon( + Remix.thumb_up_line, + color: Colors.grey, + size: 14, + ), + AppStyle.hGap4, + Text( + "${item.likeAmount}", + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + AppStyle.hGap12, + const Icon( + Remix.message_2_line, + color: Colors.grey, + size: 14, + ), + AppStyle.hGap4, + Text( + "${item.likeAmount}", + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + const Expanded(child: SizedBox()), + Text( + Utils.formatTimestamp(item.createTime), + style: const TextStyle( + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } + + void toDetail(UserCommentItem item) { + //漫画 + if (type == 0) { + AppNavigator.toComicDetail(item.objId); + } else if (type == 1) { + AppNavigator.toNovelDetail(item.objId); + } else if (type == 2) { + AppNavigator.toNewsDetail(url: item.pageUrl ?? ""); + } + } +} diff --git a/lib/modules/user/history/comic/comic_history_controller.dart b/lib/modules/user/history/comic/comic_history_controller.dart new file mode 100644 index 0000000..11864a7 --- /dev/null +++ b/lib/modules/user/history/comic/comic_history_controller.dart @@ -0,0 +1,15 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/user/comic_history_model.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; + +class ComicHistoryController extends BasePageController { + final UserRequest request = UserRequest(); + + @override + Future> getData(int page, int pageSize) async { + if (page > 1) { + return []; + } + return await request.comicHistory(); + } +} diff --git a/lib/modules/user/history/comic/comic_history_view.dart b/lib/modules/user/history/comic/comic_history_view.dart new file mode 100644 index 0000000..39940e7 --- /dev/null +++ b/lib/modules/user/history/comic/comic_history_view.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/user/comic_history_model.dart'; +import 'package:flutter_dmzj/modules/user/history/comic/comic_history_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class ComicHistoryView extends StatelessWidget { + final ComicHistoryController controller; + ComicHistoryView({super.key}) + : controller = Get.put(ComicHistoryController()); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + loadMore: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ); + } + + Widget buildItem(UserComicHistoryModel item) { + return InkWell( + onTap: () { + AppNavigator.toComicDetail(item.comicId); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.comicName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text("看到${item.chapterName} ${item.record}页", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + AppStyle.vGap4, + Text("观看于${Utils.formatTimestamp(item.viewingTime ?? 0)}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/user/history/novel/novel_history_controller.dart b/lib/modules/user/history/novel/novel_history_controller.dart new file mode 100644 index 0000000..c469302 --- /dev/null +++ b/lib/modules/user/history/novel/novel_history_controller.dart @@ -0,0 +1,15 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/user/novel_history_model.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; + +class NovelHistoryController extends BasePageController { + final UserRequest request = UserRequest(); + + @override + Future> getData(int page, int pageSize) async { + if (page > 1) { + return []; + } + return await request.novelHistory(); + } +} diff --git a/lib/modules/user/history/novel/novel_history_view.dart b/lib/modules/user/history/novel/novel_history_view.dart new file mode 100644 index 0000000..34b76c8 --- /dev/null +++ b/lib/modules/user/history/novel/novel_history_view.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/user/novel_history_model.dart'; +import 'package:flutter_dmzj/modules/user/history/novel/novel_history_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class NovelHistoryView extends StatelessWidget { + final NovelHistoryController controller; + NovelHistoryView({super.key}) + : controller = Get.put(NovelHistoryController()); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + loadMore: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ); + } + + Widget buildItem(UserNovelHistoryModel item) { + return InkWell( + onTap: () { + AppNavigator.toNovelDetail(item.lnovelId); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.cover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.novelName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text( + "看到${item.volumeName} ${item.chapterName} ${item.record}页", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + AppStyle.vGap4, + Text("观看于${Utils.formatTimestamp(item.viewingTime ?? 0)}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/user/history/user_history_controller.dart b/lib/modules/user/history/user_history_controller.dart new file mode 100644 index 0000000..03bfe0a --- /dev/null +++ b/lib/modules/user/history/user_history_controller.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +import 'package:get/get.dart'; + +class UserHistoryController extends GetxController + with GetSingleTickerProviderStateMixin { + final int type; + UserHistoryController(this.type); + late TabController tabController; + + @override + void onInit() { + tabController = TabController(length: 2, vsync: this, initialIndex: type); + + super.onInit(); + } +} diff --git a/lib/modules/user/history/user_history_page.dart b/lib/modules/user/history/user_history_page.dart new file mode 100644 index 0000000..4bb2244 --- /dev/null +++ b/lib/modules/user/history/user_history_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/user/history/comic/comic_history_view.dart'; +import 'package:flutter_dmzj/modules/user/history/novel/novel_history_view.dart'; +import 'package:flutter_dmzj/modules/user/history/user_history_controller.dart'; +import 'package:get/get.dart'; + +class UserHistoryPage extends StatelessWidget { + final UserHistoryController controller; + final int type; + UserHistoryPage({this.type = 0, super.key}) + : controller = Get.put( + UserHistoryController(type), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 56), + child: TabBar( + controller: controller.tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + labelPadding: AppStyle.edgeInsetsH24, + indicatorColor: Theme.of(context).colorScheme.primary, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: const [ + Tab(text: "漫画记录"), + Tab(text: "小说记录"), + ], + ), + ), + ), + body: TabBarView( + controller: controller.tabController, + children: [ + ComicHistoryView(), + NovelHistoryView(), + ], + ), + ); + } +} diff --git a/lib/modules/user/local_favorite/local_favorite_controller.dart b/lib/modules/user/local_favorite/local_favorite_controller.dart new file mode 100644 index 0000000..23c410c --- /dev/null +++ b/lib/modules/user/local_favorite/local_favorite_controller.dart @@ -0,0 +1,39 @@ +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/db/local_favorite.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:get/get.dart'; + +class LocalFavoriteController extends BasePageController { + var editMode = false.obs; + @override + Future> getData(int page, int pageSize) async { + if (page > 1) { + return []; + } + return DBService.instance.localFavoriteBox.values + .where((x) => x.type == AppConstant.kTypeComic) + .toList(); + } + + void cancelEdit() { + for (var item in list) { + item.isChecked.value = false; + } + editMode.value = false; + } + + void cancelFavorite() async { + var items = list.where((x) => x.isChecked.value).toList(); + if (items.isEmpty) { + cancelEdit(); + return; + } + cancelEdit(); + for (var item in items) { + DBService.instance.removeComicFavorite(comicId: item.objId); + } + + refreshData(); + } +} diff --git a/lib/modules/user/local_favorite/local_favorite_page.dart b/lib/modules/user/local_favorite/local_favorite_page.dart new file mode 100644 index 0000000..0e87c9c --- /dev/null +++ b/lib/modules/user/local_favorite/local_favorite_page.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/db/local_favorite.dart'; +import 'package:flutter_dmzj/modules/user/local_favorite/local_favorite_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_grid_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; + +class LocalFavoritePage extends StatelessWidget { + final LocalFavoriteController controller; + LocalFavoritePage({super.key}) + : controller = Get.put(LocalFavoriteController()); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("本机收藏"), + ), + body: LayoutBuilder(builder: (context, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + return PageGridView( + pageController: controller, + firstRefresh: true, + crossAxisCount: count, + padding: AppStyle.edgeInsetsA12, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ); + }), + bottomNavigationBar: Obx( + () => Offstage( + offstage: !controller.editMode.value, + child: SizedBox( + height: 48, + child: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: controller.cancelFavorite, + icon: const Icon(Icons.favorite_border), + label: const Text("取消收藏"), + ), + AppStyle.hGap8, + TextButton.icon( + onPressed: controller.cancelEdit, + icon: const Icon(Icons.cancel_outlined), + label: const Text("取消"), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget buildItem(LocalFavorite item) { + return ShadowCard( + onTap: () { + if (controller.editMode.value) { + item.isChecked.value = !item.isChecked.value; + return; + } + + AppNavigator.toComicDetail(item.objId); + }, + onLongPress: () { + if (controller.editMode.value) { + return; + } + + item.isChecked.value = true; + controller.editMode.value = true; + }, + radius: 4, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover, + borderRadius: 4, + ), + ), + Padding( + padding: AppStyle.edgeInsetsA8, + child: Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + height: 1.2, + ), + ), + ), + ], + ), + Obx( + () => Positioned( + right: 0, + top: 0, + child: Offstage( + offstage: !controller.editMode.value, + child: Checkbox( + value: item.isChecked.value, + onChanged: (e) { + item.isChecked.value = e!; + }, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/user/local_history/comic/comic_history_controller.dart b/lib/modules/user/local_history/comic/comic_history_controller.dart new file mode 100644 index 0000000..629c68c --- /dev/null +++ b/lib/modules/user/local_history/comic/comic_history_controller.dart @@ -0,0 +1,17 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/db/comic_history.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; + +class LocalComicHistoryController extends BasePageController { + final UserRequest request = UserRequest(); + + @override + Future> getData(int page, int pageSize) async { + if (page > 1) { + return []; + } + + return DBService.instance.getComicHistoryList(); + } +} diff --git a/lib/modules/user/local_history/comic/comic_history_view.dart b/lib/modules/user/local_history/comic/comic_history_view.dart new file mode 100644 index 0000000..6e7ce79 --- /dev/null +++ b/lib/modules/user/local_history/comic/comic_history_view.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/db/comic_history.dart'; +import 'package:flutter_dmzj/modules/user/local_history/comic/comic_history_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class LocalComicHistoryView extends StatelessWidget { + final LocalComicHistoryController controller; + LocalComicHistoryView({super.key}) + : controller = Get.put(LocalComicHistoryController()); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + loadMore: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ); + } + + Widget buildItem(ComicHistory item) { + return InkWell( + onTap: () { + AppNavigator.toComicDetail(item.comicId); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.comicCover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.comicName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text("看到${item.chapterName} ${item.page}页", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + AppStyle.vGap4, + Text( + "观看于${Utils.formatTimestampMS(item.updateTime.millisecondsSinceEpoch)}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/user/local_history/local_history_controller.dart b/lib/modules/user/local_history/local_history_controller.dart new file mode 100644 index 0000000..04e6d52 --- /dev/null +++ b/lib/modules/user/local_history/local_history_controller.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +import 'package:get/get.dart'; + +class LocalHistoryController extends GetxController + with GetSingleTickerProviderStateMixin { + final int type; + LocalHistoryController(this.type); + late TabController tabController; + + @override + void onInit() { + tabController = TabController(length: 2, vsync: this, initialIndex: type); + + super.onInit(); + } +} diff --git a/lib/modules/user/local_history/local_history_page.dart b/lib/modules/user/local_history/local_history_page.dart new file mode 100644 index 0000000..bdf0d15 --- /dev/null +++ b/lib/modules/user/local_history/local_history_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/user/local_history/comic/comic_history_view.dart'; + +import 'package:flutter_dmzj/modules/user/local_history/local_history_controller.dart'; +import 'package:flutter_dmzj/modules/user/local_history/novel/novel_history_view.dart'; +import 'package:get/get.dart'; + +class LocalHistoryPage extends StatelessWidget { + final LocalHistoryController controller; + final int type; + LocalHistoryPage({this.type = 0, super.key}) + : controller = Get.put( + LocalHistoryController(type), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 56), + child: TabBar( + controller: controller.tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + labelPadding: AppStyle.edgeInsetsH24, + indicatorColor: Theme.of(context).colorScheme.primary, + indicatorSize: TabBarIndicatorSize.label, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: const [ + Tab(text: "漫画记录"), + Tab(text: "小说记录"), + ], + ), + ), + ), + body: TabBarView( + controller: controller.tabController, + children: [ + LocalComicHistoryView(), + LocalNovelHistoryView(), + ], + ), + ); + } +} diff --git a/lib/modules/user/local_history/novel/novel_history_controller.dart b/lib/modules/user/local_history/novel/novel_history_controller.dart new file mode 100644 index 0000000..e654c8a --- /dev/null +++ b/lib/modules/user/local_history/novel/novel_history_controller.dart @@ -0,0 +1,17 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/db/novel_history.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; + +class LocalNovelHistoryController extends BasePageController { + final UserRequest request = UserRequest(); + + @override + Future> getData(int page, int pageSize) async { + if (page > 1) { + return []; + } + + return DBService.instance.getNovelHistoryList(); + } +} diff --git a/lib/modules/user/local_history/novel/novel_history_view.dart b/lib/modules/user/local_history/novel/novel_history_view.dart new file mode 100644 index 0000000..fc077b7 --- /dev/null +++ b/lib/modules/user/local_history/novel/novel_history_view.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/db/novel_history.dart'; +import 'package:flutter_dmzj/modules/user/local_history/novel/novel_history_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class LocalNovelHistoryView extends StatelessWidget { + final LocalNovelHistoryController controller; + LocalNovelHistoryView({super.key}) + : controller = Get.put(LocalNovelHistoryController()); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + loadMore: false, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ), + ); + } + + Widget buildItem(NovelHistory item) { + return InkWell( + onTap: () { + AppNavigator.toNovelDetail(item.novelId); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.novelCover, + width: 80, + height: 110, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.novelName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + AppStyle.vGap4, + Text("看到${item.volumeName} ${item.chapterName}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + AppStyle.vGap4, + Text( + "观看于${Utils.formatTimestampMS(item.updateTime.millisecondsSinceEpoch)}", + style: const TextStyle(color: Colors.grey, fontSize: 14)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/user/login/user_login_controller.dart b/lib/modules/user/login/user_login_controller.dart new file mode 100644 index 0000000..282b59d --- /dev/null +++ b/lib/modules/user/login/user_login_controller.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class UserLoginController extends GetxController { + final TextEditingController userNameController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final UserRequest userRequest = UserRequest(); + var loadding = false.obs; + void login() async { + if (userNameController.text.isEmpty) { + SmartDialog.showToast("请输入用户名"); + return; + } + if (passwordController.text.isEmpty) { + SmartDialog.showToast("请输入密码"); + return; + } + try { + loadding.value = true; + var data = await userRequest.login( + nickname: userNameController.text, + password: passwordController.text, + ); + UserService.instance.setAuthInfo(data); + + loadding.value = false; + Get.back(result: true); + } catch (e) { + SmartDialog.showToast(e.toString()); + Log.logPrint(e); + } finally { + loadding.value = false; + } + } +} diff --git a/lib/modules/user/login/user_login_dialog.dart b/lib/modules/user/login/user_login_dialog.dart new file mode 100644 index 0000000..d198282 --- /dev/null +++ b/lib/modules/user/login/user_login_dialog.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/user/login/user_login_controller.dart'; +import 'package:get/get.dart'; + +class UserLoginDialog extends StatelessWidget { + final UserLoginController controller = Get.put(UserLoginController()); + UserLoginDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: AppStyle.radius12, + ), + child: Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + contentPadding: AppStyle.edgeInsetsL12, + title: const Text("登录"), + trailing: IconButton( + onPressed: Get.back, + icon: const Icon(Icons.close), + ), + ), + AppStyle.vGap12, + Padding( + padding: AppStyle.edgeInsetsH24, + child: TextField( + controller: controller.userNameController, + autofocus: true, + textInputAction: TextInputAction.next, + decoration: const InputDecoration( + hintText: "请输入用户名/手机号", + labelText: "用户名/手机号", + floatingLabelBehavior: FloatingLabelBehavior.always, + contentPadding: AppStyle.edgeInsetsH8, + border: OutlineInputBorder(), + ), + ), + ), + AppStyle.vGap24, + Padding( + padding: AppStyle.edgeInsetsH24, + child: TextField( + controller: controller.passwordController, + obscureText: true, + textInputAction: TextInputAction.done, + decoration: const InputDecoration( + hintText: "请输入密码", + labelText: "密码", + floatingLabelBehavior: FloatingLabelBehavior.always, + contentPadding: AppStyle.edgeInsetsH8, + border: OutlineInputBorder(), + ), + onSubmitted: (e) { + controller.login(); + }, + ), + ), + AppStyle.vGap12, + Container( + width: double.infinity, + padding: AppStyle.edgeInsetsA12.copyWith(left: 24, right: 24), + child: SizedBox( + height: 40, + child: Obx( + () => ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: AppStyle.radius24, + ), + ), + onPressed: + controller.loadding.value ? null : controller.login, + child: controller.loadding.value + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : const Text("登录"), + ), + ), + ), + ), + AppStyle.vGap12, + ], + ), + ), + ); + } +} diff --git a/lib/modules/user/settings/settings_controller.dart b/lib/modules/user/settings/settings_controller.dart new file mode 100644 index 0000000..ee2edd1 --- /dev/null +++ b/lib/modules/user/settings/settings_controller.dart @@ -0,0 +1,97 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/services/local_storage_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class SettingsController extends GetxController { + final settings = AppSettingsService.instance; + var imageCacheSize = "正在计算缓存...".obs; + var novelCacheSize = "正在计算缓存...".obs; + + @override + void onInit() { + super.onInit(); + getImageCachedSize(); + getNovelCachedSize(); + } + + void getImageCachedSize() async { + try { + imageCacheSize.value = "正在计算缓存..."; + var bytes = await getCachedSizeBytes(); + imageCacheSize.value = "${(bytes / 1024 / 1024).toStringAsFixed(1)}MB"; + } catch (e) { + imageCacheSize.value = "缓存计算失败"; + } + } + + void getNovelCachedSize() async { + try { + novelCacheSize.value = "正在计算缓存..."; + var bytes = await LocalStorageService.instance.getNovelCacheSize(); + novelCacheSize.value = "${(bytes / 1024 / 1024).toStringAsFixed(1)}MB"; + } catch (e) { + novelCacheSize.value = "缓存计算失败"; + } + } + + void cleanImageCache() async { + var result = await clearDiskCachedImages(); + if (!result) { + SmartDialog.showToast("清除失败"); + } + getImageCachedSize(); + } + + void cleanNovelCache() async { + var result = await LocalStorageService.instance.cleanNovelCacheSize(); + if (!result) { + SmartDialog.showToast("清除失败"); + } + getNovelCachedSize(); + } + + void setDownloadComicTask() { + Get.dialog( + SimpleDialog( + title: const Text("漫画最大任务数"), + children: [0, 1, 2, 3, 4, 5] + .map( + (e) => RadioListTile( + title: Text(e == 0 ? "无限制" : "$e个"), + value: e, + groupValue: settings.downloadComicTaskCount.value, + onChanged: (e) { + Get.back(); + settings.setDownloadComicTaskCount(e ?? 0); + }, + ), + ) + .toList(), + ), + ); + } + + void setDownloadNovelTask() { + Get.dialog( + SimpleDialog( + title: const Text("小说最大任务数"), + children: [0, 1, 2, 3, 4, 5] + .map( + (e) => RadioListTile( + title: Text(e == 0 ? "无限制" : "$e个"), + value: e, + groupValue: settings.downloadNovelTaskCount.value, + onChanged: (e) { + Get.back(); + settings.setDownloadNovelTaskCount(e ?? 0); + }, + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/modules/user/settings/settings_page.dart b/lib/modules/user/settings/settings_page.dart new file mode 100644 index 0000000..274f0f1 --- /dev/null +++ b/lib/modules/user/settings/settings_page.dart @@ -0,0 +1,529 @@ +import 'dart:io'; + +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/modules/user/settings/settings_controller.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; + +class SettingsPage extends StatelessWidget { + final int index; + SettingsPage({required this.index, super.key}); + final controller = Get.put(SettingsController()); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 4, + initialIndex: index, + child: Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 56), + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).colorScheme.primary, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: const [ + Tab(text: "常规"), + Tab(text: "漫画"), + Tab(text: "小说"), + Tab(text: "下载"), + ], + ), + ), + ), + body: TabBarView( + children: [ + buildGeneralSettings(), + buildComicSettings(), + buildNovelSettings(), + buildDownloadSettings(), + ], + ), + ), + ); + } + + Widget buildGeneralSettings() { + return Obx( + () => ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + buildToggle( + value: controller.settings.useDynamicColor.value, + onChanged: (e) { + controller.settings.setUseDynamicColor(e); + }, + title: "使用MD动态取色", + subtitle: "关闭后使用固定主题色 #4196f9", + ), + ListTile( + title: const Text("清除图片缓存"), + subtitle: Text(controller.imageCacheSize.value), + trailing: OutlinedButton( + onPressed: () { + controller.cleanImageCache(); + }, + child: const Text("清除"), + ), + ), + ListTile( + title: const Text("清除小说缓存"), + subtitle: Text(controller.novelCacheSize.value), + trailing: OutlinedButton( + onPressed: () {}, + child: const Text("清除"), + ), + ), + // SwitchListTile( + // value: controller.settings.comicSearchUseWebApi.value, + // onChanged: (e) { + // controller.settings.setComicSearchUseWebApi(e); + // }, + // title: const Text("使用Web接口搜索漫画"), + // subtitle: const Text("开启后可以搜索到更多漫画"), + // ), + buildToggle( + value: controller.settings.useSystemFontSize.value, + onChanged: (e) { + controller.settings.setUseSystemFontSize(e); + }, + title: "字体大小跟随系统", + subtitle: "开启可能会有布局错乱", + ), + buildToggle( + value: controller.settings.collectHideComic.value, + onChanged: (e) { + controller.settings.setCollectHideComic(e); + }, + title: "自动收藏神隐漫画", + subtitle: "浏览神隐漫画时自动添加到本机收藏", + ), + ListTile( + title: const Text("代理地址"), + subtitle: TextField( + controller: TextEditingController(text: controller.settings.proxyAddress.value), + decoration: const InputDecoration( + hintText: "仅支持http协议,重启生效 eg:127.0.0.1:7890", + ), + onSubmitted: (e){ + controller.settings.setProxyAddress(e); + }, + ), + + ) + ], + ), + ); + } + + Widget buildComicSettings() { + return Obx( + () => ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + buildToggle( + value: controller.settings.comicReaderHD.value, + onChanged: (e) { + controller.settings.setComicReaderHD(e); + }, + title: "优先加载高清图", + subtitle: "部分单行本可能未分页", + ), + ListTile( + title: const Text("阅读方向"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + buildSelectedButton( + onTap: () { + controller.settings.setComicReaderDirection(0); + }, + selected: controller.settings.comicReaderDirection.value == 0, + child: const Icon(Remix.arrow_right_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + controller.settings.setComicReaderDirection(2); + }, + selected: controller.settings.comicReaderDirection.value == 2, + child: const Icon(Remix.arrow_left_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + controller.settings.setComicReaderDirection(1); + }, + selected: controller.settings.comicReaderDirection.value == 1, + child: const Icon(Remix.arrow_down_line), + ) + ], + ), + ), + buildToggle( + value: controller.settings.comicReaderLeftHandMode.value, + onChanged: (e) { + controller.settings.setComicReaderLeftHandMode(e); + }, + title: "操作反转", + subtitle: "点击左侧下一页,右侧上一页", + ), + buildToggle( + value: controller.settings.comicReaderFullScreen.value, + onChanged: (e) { + controller.settings.setComicReaderFullScreen(e); + }, + title: "全屏阅读", + ), + buildToggle( + value: controller.settings.comicReaderShowStatus.value, + onChanged: (e) { + controller.settings.setComicReaderShowStatus(e); + }, + title: "显示状态信息", + ), + buildToggle( + value: controller.settings.comicReaderShowViewPoint.value, + onChanged: (e) { + controller.settings.setComicReaderShowViewPoint(e); + }, + title: "显示吐槽", + ), + buildToggle( + value: controller.settings.comicReaderOldViewPoint.value, + onChanged: (e) { + controller.settings.setComicReaderOldViewPoint(e); + }, + title: "旧版吐槽", + ), + buildToggle( + value: controller.settings.comicReaderPageAnimation.value, + onChanged: (e) { + controller.settings.setComicReaderPageAnimation(e); + }, + title: "翻页动画", + ), + ], + ), + ); + } + + Widget buildNovelSettings() { + return Obx( + () => ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + ListTile( + title: const Text("阅读方向"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + buildSelectedButton( + onTap: () { + controller.settings.setNovelReaderDirection(0); + }, + selected: controller.settings.novelReaderDirection.value == 0, + child: const Icon(Remix.arrow_right_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + controller.settings.setNovelReaderDirection(2); + }, + selected: controller.settings.novelReaderDirection.value == 2, + child: const Icon(Remix.arrow_left_line), + ), + AppStyle.hGap8, + buildSelectedButton( + onTap: () { + controller.settings.setNovelReaderDirection(1); + }, + selected: controller.settings.novelReaderDirection.value == 1, + child: const Icon(Remix.arrow_down_line), + ) + ], + ), + ), + buildToggle( + value: controller.settings.novelReaderLeftHandMode.value, + onChanged: (e) { + controller.settings.setNovelReaderLeftHandMode(e); + }, + title: "操作反转", + subtitle: "点击左侧下一页,右侧上一页", + ), + // SwitchListTile( + // value: settings.novelReaderFullScreen.value, + // onChanged: (e) { + // settings.setNovelReaderFullScreen(e); + // }, + // title: const Text("全屏阅读"), + // ), + buildToggle( + value: controller.settings.novelReaderShowStatus.value, + onChanged: (e) { + controller.settings.setNovelReaderShowStatus(e); + }, + title: "显示状态信息", + ), + buildToggle( + value: controller.settings.novelReaderPageAnimation.value, + onChanged: (e) { + controller.settings.setNovelReaderPageAnimation(e); + }, + title: "翻页动画", + ), + ListTile( + title: const Text("字体大小"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + OutlinedButton( + onPressed: () { + controller.settings.setNovelReaderFontSize( + controller.settings.novelReaderFontSize.value + 1, + ); + }, + child: const Icon( + Icons.add, + ), + ), + AppStyle.hGap12, + Text("${controller.settings.novelReaderFontSize.value}"), + AppStyle.hGap12, + OutlinedButton( + onPressed: () { + controller.settings.setNovelReaderFontSize( + controller.settings.novelReaderFontSize.value - 1, + ); + }, + child: const Icon( + Icons.remove, + ), + ), + ], + ), + ), + ListTile( + title: const Text("行距"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + OutlinedButton( + onPressed: () { + controller.settings.setNovelReaderLineSpacing( + controller.settings.novelReaderLineSpacing.value + 0.1, + ); + }, + child: const Icon( + Icons.add, + ), + ), + AppStyle.hGap12, + Text((controller.settings.novelReaderLineSpacing.value) + .toStringAsFixed(1)), + AppStyle.hGap12, + OutlinedButton( + onPressed: () { + controller.settings.setNovelReaderLineSpacing( + controller.settings.novelReaderLineSpacing.value - 0.1, + ); + }, + child: const Icon( + Icons.remove, + ), + ), + ], + ), + ), + ListTile( + title: const Text("阅读主题"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: AppColor.novelThemes.keys + .map( + (e) => GestureDetector( + onTap: () { + controller.settings.setNovelReaderTheme(e); + }, + child: Container( + margin: AppStyle.edgeInsetsL8, + height: 36, + width: 36, + decoration: BoxDecoration( + color: AppColor.novelThemes[e]!.first, + borderRadius: AppStyle.radius24, + ), + child: Visibility( + visible: + AppColor.novelThemes.keys.toList().indexOf(e) == + controller.settings.novelReaderTheme.value, + child: Icon( + Icons.check, + color: AppColor.novelThemes[e]!.last, + ), + ), + ), + ), + ) + .toList(), + ), + ), + Container( + margin: AppStyle.edgeInsetsV12, + padding: AppStyle.edgeInsetsA8, + decoration: BoxDecoration( + borderRadius: AppStyle.radius4, + color: AppColor + .novelThemes[controller.settings.novelReaderTheme]!.first, + ), + child: Text( + """这是一段测试文字,可以预览上面的设置效果。 + +  晋太元中,武陵人捕鱼为业。缘溪行,忘路之远近。忽逢桃花林,夹岸数百步,中无杂树,芳草鲜美,落英缤纷。渔人甚异之,复前行,欲穷其林。 +  林尽水源,便得一山,山有小口,仿佛若有光。便舍船,从口入。初极狭,才通人。复行数十步,豁然开朗。土地平旷,屋舍俨然,有良田、美池、桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐……""", + //不需要跟随系统 + textScaler: const TextScaler.linear(1.0), + style: TextStyle( + fontSize: + controller.settings.novelReaderFontSize.value.toDouble(), + height: controller.settings.novelReaderLineSpacing.value, + color: AppColor + .novelThemes[controller.settings.novelReaderTheme]!.last, + ), + ), + ), + ], + ), + ); + } + + Widget buildDownloadSettings() { + return Obx( + () => ListView( + padding: AppStyle.edgeInsetsA12, + children: [ + buildToggle( + value: controller.settings.downloadAllowCellular.value, + onChanged: (e) { + controller.settings.setDownloadAllowCellular(e); + }, + title: "允许使用流量下载", + ), + ListTile( + title: const Text("漫画最大任务数"), + onTap: () { + controller.setDownloadComicTask(); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.settings.downloadComicTaskCount.value == 0 + ? "无限制" + : controller.settings.downloadComicTaskCount.toString(), + ), + AppStyle.hGap4, + const Icon( + Icons.chevron_right, + color: Colors.grey, + ), + ], + ), + ), + ListTile( + title: const Text("小说最大任务数"), + onTap: () { + controller.setDownloadNovelTask(); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.settings.downloadNovelTaskCount.value == 0 + ? "无限制" + : controller.settings.downloadNovelTaskCount.toString(), + ), + AppStyle.hGap4, + const Icon( + Icons.chevron_right, + color: Colors.grey, + ), + ], + ), + ), + ], + ), + ); + } + + Widget buildSelectedButton( + {required Widget child, bool selected = false, Function()? onTap}) { + final primary = Get.theme.colorScheme.primary; + return OutlinedButton( + style: OutlinedButton.styleFrom( + foregroundColor: selected ? primary : Colors.grey, + side: BorderSide( + color: selected ? primary : Colors.grey, + ), + ), + onPressed: onTap, + child: child, + ); + } + + /// 平台自适应开关控件 + /// Windows使用Fluent ToggleSwitch,其他平台使用Material SwitchListTile + Widget buildToggle({ + required String title, + required bool value, + required ValueChanged onChanged, + String? subtitle, + }) { + if (PlatformUtils.isWindows) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, + style: Get.textTheme.bodyMedium), + if (subtitle != null) + Text(subtitle, + style: Get.textTheme.bodySmall + ?.copyWith(color: Colors.grey)), + ], + ), + ), + fluent.FluentTheme( + data: PlatformUtils.getFluentTheme(Get.context!), + child: fluent.ToggleSwitch( + checked: value, + onChanged: onChanged, + ), + ), + ], + ), + ); + } + return SwitchListTile( + value: value, + onChanged: onChanged, + title: Text(title), + subtitle: subtitle != null ? Text(subtitle) : null, + ); + } +} diff --git a/lib/modules/user/subscribe/comic/comic_subscribe_controller.dart b/lib/modules/user/subscribe/comic/comic_subscribe_controller.dart new file mode 100644 index 0000000..93a7828 --- /dev/null +++ b/lib/modules/user/subscribe/comic/comic_subscribe_controller.dart @@ -0,0 +1,78 @@ +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/user/subscribe_comic_model.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class ComicSubscribeController + extends BasePageController { + ComicSubscribeController() { + for (var item in List.generate( + 26, (index) => String.fromCharCode(index + 65).toLowerCase())) { + letters.addAll({item: "${item.toUpperCase()}开头"}); + } + } + final UserRequest request = UserRequest(); + + var letter = "".obs; + + Map letters = { + "": "全部", + "number": "数字开头", + }; + + Map types = { + 1: "全部订阅", + 2: "未读", + 3: "已读", + 4: "完结", + }; + var type = 1.obs; + + var editMode = false.obs; + + @override + Future> getData( + int page, int pageSize) async { + var ls = await request.comicSubscribes( + subType: type.value, + letter: letter.value, + page: page, + ); + UserService.instance.subscribedComicIds.addAll(ls.map((e) => e.id)); + return ls; + } + + void cancelEdit() { + for (var item in list) { + item.isChecked.value = false; + } + editMode.value = false; + } + + void cancelSub() async { + var ids = list.where((x) => x.isChecked.value).map((e) => e.id).toList(); + if (ids.isEmpty) { + cancelEdit(); + return; + } + cancelEdit(); + await UserService.instance.cancelSubscribe(ids, AppConstant.kTypeComic); + easyRefreshController.callRefresh(); + } + + void addFavorite() async { + for (var item in list.where((x) => x.isChecked.value)) { + DBService.instance.putComicFavorite( + title: item.title, + cover: item.cover, + comicId: item.id, + ); + } + cancelEdit(); + SmartDialog.showToast("已添加至本机收藏"); + } +} diff --git a/lib/modules/user/subscribe/comic/comic_subscribe_view.dart b/lib/modules/user/subscribe/comic/comic_subscribe_view.dart new file mode 100644 index 0000000..ca8b8ce --- /dev/null +++ b/lib/modules/user/subscribe/comic/comic_subscribe_view.dart @@ -0,0 +1,273 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/user/subscribe_comic_model.dart'; +import 'package:flutter_dmzj/modules/user/subscribe/comic/comic_subscribe_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_grid_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; + +class ComicSubscribeView extends StatelessWidget { + final ComicSubscribeController controller; + ComicSubscribeView({super.key}) + : controller = Get.put(ComicSubscribeController()); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: Column( + children: [ + Obx( + () => Row( + children: [ + buildFilter( + // ignore: invalid_use_of_protected_member + types: controller.letters, + value: controller.letter.value, + onSelected: (e) { + controller.letter.value = e; + controller.refreshData(); + }, + ), + buildFilter( + types: controller.types, + value: controller.type.value, + onSelected: (e) { + controller.type.value = e; + controller.refreshData(); + }, + ), + ], + ), + ), + Divider( + color: Colors.grey.withOpacity(.2), + height: 1.0, + ), + Expanded( + child: LayoutBuilder(builder: (context, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + return PageGridView( + pageController: controller, + firstRefresh: true, + crossAxisCount: count, + padding: AppStyle.edgeInsetsA12, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ); + }), + ), + Obx( + () => Offstage( + offstage: !controller.editMode.value, + child: SizedBox( + height: 48, + child: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: controller.addFavorite, + icon: const Icon(Icons.star_border), + label: const Text("添加收藏"), + ), + AppStyle.hGap8, + TextButton.icon( + onPressed: controller.cancelSub, + icon: const Icon(Icons.favorite_border), + label: const Text("取消订阅"), + ), + AppStyle.hGap8, + TextButton.icon( + onPressed: controller.cancelEdit, + icon: const Icon(Icons.cancel_outlined), + label: const Text("取消"), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildItem(UserSubscribeComicItemModel item) { + return ShadowCard( + onTap: () { + if (controller.editMode.value) { + item.isChecked.value = !item.isChecked.value; + return; + } + item.hasNew.value = false; + AppNavigator.toComicDetail(item.id); + }, + onLongPress: () { + if (controller.editMode.value) { + return; + } + + item.isChecked.value = true; + controller.editMode.value = true; + }, + radius: 4, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover, + borderRadius: 4, + ), + ), + Positioned( + left: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + color: + item.status == "连载中" ? Get.theme.colorScheme.primary : Colors.orange, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + padding: + AppStyle.edgeInsetsH8.copyWith(top: 2, bottom: 2), + child: Text( + item.status, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ), + Positioned( + right: 0, + top: 0, + child: Obx( + () => Visibility( + visible: item.hasNew.value, + child: Container( + decoration: const BoxDecoration( + color: Colors.deepOrange, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + ), + padding: + AppStyle.edgeInsetsH8.copyWith(top: 2, bottom: 2), + child: const Text( + "新", + style: TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + "更新 ${item.lastUpdateChapterName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.grey, + fontSize: 12.0, + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + ], + ), + Obx( + () => Positioned( + right: 0, + top: 0, + child: Offstage( + offstage: !controller.editMode.value, + child: Checkbox( + value: item.isChecked.value, + onChanged: (e) { + item.isChecked.value = e!; + }, + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildFilter({ + required Map types, + required dynamic value, + required Function(dynamic) onSelected, + }) { + return Expanded( + child: PopupMenuButton( + onSelected: onSelected, + itemBuilder: (c) => types.keys + .map( + (k) => CheckedPopupMenuItem( + value: k, + checked: k == value, + child: Text(types[k] ?? ""), + ), + ) + .toList(), + child: SizedBox( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + types[value] ?? "", + ), + const Icon( + Icons.arrow_drop_down, + color: Colors.grey, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/user/subscribe/news/news_subscribe_controller.dart b/lib/modules/user/subscribe/news/news_subscribe_controller.dart new file mode 100644 index 0000000..eb36926 --- /dev/null +++ b/lib/modules/user/subscribe/news/news_subscribe_controller.dart @@ -0,0 +1,15 @@ +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/user/subscribe_news_model.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; + +class NewsSubscribeController + extends BasePageController { + final UserRequest request = UserRequest(); + + @override + Future> getData(int page, int pageSize) async { + return await request.newsSubscribes( + page: page, + ); + } +} diff --git a/lib/modules/user/subscribe/news/news_subscribe_view.dart b/lib/modules/user/subscribe/news/news_subscribe_view.dart new file mode 100644 index 0000000..6e9f9af --- /dev/null +++ b/lib/modules/user/subscribe/news/news_subscribe_view.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/modules/user/subscribe/news/news_subscribe_controller.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; + +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_list_view.dart'; +import 'package:get/get.dart'; + +class NewsSubscribeView extends StatelessWidget { + final NewsSubscribeController controller; + NewsSubscribeView({super.key}) + : controller = Get.put(NewsSubscribeController()); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: PageListView( + pageController: controller, + firstRefresh: true, + separatorBuilder: (context, i) => Divider( + endIndent: 12, + indent: 12, + color: Colors.grey.withOpacity(.2), + height: 1, + ), + itemBuilder: (context, i) { + var item = controller.list[i]; + return InkWell( + onTap: () { + AppNavigator.toNewsDetail( + newsId: item.subId.toInt(), + title: item.title, + url: item.pageUrl, + ); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetImage( + item.rowPicUrl, + width: 100, + height: 62, + borderRadius: 4, + ), + AppStyle.hGap12, + Expanded( + child: SizedBox( + height: 62, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + item.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "收藏于${Utils.formatTimestamp(item.subTime.toInt())}", + style: const TextStyle( + color: Colors.grey, fontSize: 12), + ), + Row( + children: [ + const Icon( + Icons.thumb_up, + size: 12.0, + color: Colors.grey, + ), + AppStyle.hGap4, + Text( + item.moodAmount.toString(), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + AppStyle.hGap8, + const Icon( + Icons.chat, + size: 12.0, + color: Colors.grey, + ), + AppStyle.hGap4, + Text( + item.commentAmount.toString(), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ) + ], + ) + ], + ) + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/modules/user/subscribe/novel/novel_subscribe_controller.dart b/lib/modules/user/subscribe/novel/novel_subscribe_controller.dart new file mode 100644 index 0000000..ee89b95 --- /dev/null +++ b/lib/modules/user/subscribe/novel/novel_subscribe_controller.dart @@ -0,0 +1,62 @@ +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/models/user/subscribe_novel_model.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:get/get.dart'; + +class NovelSubscribeController + extends BasePageController { + NovelSubscribeController() { + for (var item in List.generate( + 26, (index) => String.fromCharCode(index + 65).toLowerCase())) { + letters.addAll({item: "${item.toUpperCase()}开头"}); + } + } + final UserRequest request = UserRequest(); + + var letter = "".obs; + + Map letters = { + "": "全部", + "number": "数字开头", + }; + + Map types = { + 1: "全部订阅", + 2: "未读", + 3: "已读", + 4: "完结", + }; + var type = 1.obs; + + @override + Future> getData(int page, int pageSize) async { + var ls = await request.novelSubscribes( + subType: type.value, + letter: letter.value, + page: page - 1, + ); + UserService.instance.subscribedNovelIds.addAll(ls.map((e) => e.id)); + return ls; + } + + var editMode = false.obs; + void cancelEdit() { + for (var item in list) { + item.isChecked.value = false; + } + editMode.value = false; + } + + void cancelSub() async { + var ids = list.where((x) => x.isChecked.value).map((e) => e.id).toList(); + if (ids.isEmpty) { + cancelEdit(); + return; + } + cancelEdit(); + await UserService.instance.cancelSubscribe(ids, AppConstant.kTypeNovel); + easyRefreshController.callRefresh(); + } +} diff --git a/lib/modules/user/subscribe/novel/novel_subscribe_view.dart b/lib/modules/user/subscribe/novel/novel_subscribe_view.dart new file mode 100644 index 0000000..e5da01a --- /dev/null +++ b/lib/modules/user/subscribe/novel/novel_subscribe_view.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/models/user/subscribe_novel_model.dart'; +import 'package:flutter_dmzj/modules/user/subscribe/novel/novel_subscribe_controller.dart'; + +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/keep_alive_wrapper.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/page_grid_view.dart'; +import 'package:flutter_dmzj/widgets/shadow_card.dart'; +import 'package:get/get.dart'; + +class NovelSubscribeView extends StatelessWidget { + final NovelSubscribeController controller; + NovelSubscribeView({super.key}) + : controller = Get.put(NovelSubscribeController()); + + @override + Widget build(BuildContext context) { + return KeepAliveWrapper( + child: Column( + children: [ + Obx( + () => Row( + children: [ + buildFilter( + // ignore: invalid_use_of_protected_member + types: controller.letters, + value: controller.letter.value, + onSelected: (e) { + controller.letter.value = e; + controller.refreshData(); + }, + ), + buildFilter( + types: controller.types, + value: controller.type.value, + onSelected: (e) { + controller.type.value = e; + controller.refreshData(); + }, + ), + ], + ), + ), + Divider( + color: Colors.grey.withOpacity(.2), + height: 1.0, + ), + Expanded( + child: LayoutBuilder(builder: (context, constraints) { + var count = constraints.maxWidth ~/ 160; + if (count < 3) count = 3; + return PageGridView( + pageController: controller, + firstRefresh: true, + crossAxisCount: count, + padding: AppStyle.edgeInsetsA12, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + itemBuilder: (context, i) { + var item = controller.list[i]; + return buildItem(item); + }, + ); + }), + ), + Obx( + () => Offstage( + offstage: !controller.editMode.value, + child: SizedBox( + height: 48, + child: BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: controller.cancelSub, + icon: const Icon(Icons.favorite_border), + label: const Text("取消订阅"), + ), + TextButton.icon( + onPressed: controller.cancelEdit, + icon: const Icon(Icons.cancel_outlined), + label: const Text("取消"), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildItem(UserSubscribeNovelModel item) { + return ShadowCard( + onTap: () { + if (controller.editMode.value) { + item.isChecked.value = !item.isChecked.value; + return; + } + item.hasNew.value = false; + AppNavigator.toNovelDetail(item.id); + }, + onLongPress: () { + if (controller.editMode.value) { + return; + } + + item.isChecked.value = true; + controller.editMode.value = true; + }, + radius: 4, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + AspectRatio( + aspectRatio: 27 / 36, + child: NetImage( + item.cover ?? "", + borderRadius: 4, + ), + ), + Positioned( + left: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + color: + item.status == "连载中" ? Get.theme.colorScheme.primary : Colors.orange, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + padding: + AppStyle.edgeInsetsH8.copyWith(top: 2, bottom: 2), + child: Text( + item.status ?? "-", + style: const TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ), + Positioned( + right: 0, + top: 0, + child: Obx( + () => Visibility( + visible: item.hasNew.value, + child: Container( + decoration: const BoxDecoration( + color: Colors.deepOrange, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + ), + padding: + AppStyle.edgeInsetsH8.copyWith(top: 2, bottom: 2), + child: const Text( + "新", + style: TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ), + ), + ), + ], + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + Padding( + padding: AppStyle.edgeInsetsH4, + child: Text( + "更新 ${item.lastUpdateChapterName}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.grey, + fontSize: 12.0, + height: 1.2, + ), + ), + ), + AppStyle.vGap4, + ], + ), + Obx( + () => Positioned( + right: 0, + top: 0, + child: Offstage( + offstage: !controller.editMode.value, + child: Checkbox( + value: item.isChecked.value, + onChanged: (e) { + item.isChecked.value = e!; + }, + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildFilter({ + required Map types, + required dynamic value, + required Function(dynamic) onSelected, + }) { + return Expanded( + child: PopupMenuButton( + onSelected: onSelected, + itemBuilder: (c) => types.keys + .map( + (k) => CheckedPopupMenuItem( + value: k, + checked: k == value, + child: Text(types[k] ?? ""), + ), + ) + .toList(), + child: SizedBox( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + types[value] ?? "", + ), + const Icon( + Icons.arrow_drop_down, + color: Colors.grey, + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/user/subscribe/user_subscribe_controller.dart b/lib/modules/user/subscribe/user_subscribe_controller.dart new file mode 100644 index 0000000..4da10d3 --- /dev/null +++ b/lib/modules/user/subscribe/user_subscribe_controller.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class UserSubscribeController extends GetxController + with GetSingleTickerProviderStateMixin { + final int type; + UserSubscribeController(this.type); + late TabController tabController; + + @override + void onInit() { + tabController = TabController(length: 2, vsync: this, initialIndex: type); + + super.onInit(); + } +} diff --git a/lib/modules/user/subscribe/user_subscribe_pgae.dart b/lib/modules/user/subscribe/user_subscribe_pgae.dart new file mode 100644 index 0000000..5c6bd3d --- /dev/null +++ b/lib/modules/user/subscribe/user_subscribe_pgae.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/modules/user/subscribe/comic/comic_subscribe_view.dart'; +import 'package:flutter_dmzj/modules/user/subscribe/novel/novel_subscribe_view.dart'; +import 'package:flutter_dmzj/modules/user/subscribe/user_subscribe_controller.dart'; +import 'package:get/get.dart'; + +class UserSubscribePage extends StatelessWidget { + final UserSubscribeController controller; + final int type; + UserSubscribePage({this.type = 0, super.key}) + : controller = Get.put( + UserSubscribeController(type), + tag: DateTime.now().millisecondsSinceEpoch.toString(), + ); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(right: 56), + child: TabBar( + controller: controller.tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, + labelPadding: AppStyle.edgeInsetsH24, + indicatorSize: TabBarIndicatorSize.label, + indicatorColor: Theme.of(context).colorScheme.primary, + labelColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + tabs: const [ + Tab(text: "漫画"), + Tab(text: "小说"), + // Tab(text: "新闻"), + ], + ), + ), + ), + body: TabBarView( + controller: controller.tabController, + children: [ + ComicSubscribeView(), + NovelSubscribeView(), + // NewsSubscribeView(), + ], + ), + ); + } +} diff --git a/lib/modules/user/user_home_controller.dart b/lib/modules/user/user_home_controller.dart new file mode 100644 index 0000000..09e694c --- /dev/null +++ b/lib/modules/user/user_home_controller.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; + +import 'package:get/get.dart'; + +class UserHomeController extends GetxController { + final AppSettingsService settings = AppSettingsService.instance; + + @override + void onInit() { + UserService.instance.refreshProfile(); + super.onInit(); + } + + /// 登录 + void login() { + UserService.instance.login(); + } + + /// 退出登录 + void logout() async { + var result = await DialogUtils.showAlertDialog( + "确定要退出登录吗?", + title: "退出登录", + ); + if (result) { + UserService.instance.logout(); + } + } + + /// 主题设置 + void setTheme() { + settings.changeTheme(); + } + + /// 关于我们 + void about() { + Get.dialog(AboutDialog( + applicationIcon: Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.withOpacity(.2), + ), + borderRadius: AppStyle.radius12, + ), + child: ClipRRect( + borderRadius: AppStyle.radius12, + child: Image.asset( + 'assets/images/logo.png', + width: 48, + height: 48, + ), + ), + ), + applicationName: "动漫之家", + applicationVersion: "Ver ${Utils.packageInfo.version}", + applicationLegalese: "由akasei二次修改并分发", + )); + } + + /// 检查更新 + void checkUpdate() { + Utils.checkUpdate(showMsg: true); + } + + /// 订阅 + void toUserSubscribe() async { + if (!await UserService.instance.login()) { + return; + } + AppNavigator.toUserSubscribe(); + } + + /// 历史 + void toUserHistory() async { + if (!await UserService.instance.login()) { + return; + } + AppNavigator.toUserHistory(); + } + + /// 本机历史 + void toLocalHistory() async { + AppNavigator.toLocalHistory(); + } + + void toSettings() async { + AppNavigator.toSettings(); + } + + void comicDownload() { + AppNavigator.toComicDownloadManage(0); + } + + void novelDownload() { + AppNavigator.toNovelDownloadManage(0); + } + + void userComment() { + AppNavigator.toUserComment(int.tryParse(UserService.instance.userId) ?? 0); + } + + void toFavorite() { + AppNavigator.tolocalFavorite(); + } +} diff --git a/lib/modules/user/user_home_page.dart b/lib/modules/user/user_home_page.dart new file mode 100644 index 0000000..1a5b7ab --- /dev/null +++ b/lib/modules/user/user_home_page.dart @@ -0,0 +1,371 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_color.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/platform_utils.dart'; +import 'package:flutter_dmzj/modules/user/user_home_controller.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:flutter_dmzj/widgets/user_photo.dart'; +import 'package:get/get.dart'; +import 'package:remixicon/remixicon.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class UserHomePage extends GetView { + const UserHomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (PlatformUtils.isWindows) { + return _buildWindowsLayout(context); + } + return _buildMobileLayout(context); + } + + Widget _buildWindowsLayout(BuildContext context) { + final fluentTheme = fluent.FluentTheme.of(context); + return ColoredBox( + color: fluentTheme.micaBackgroundColor, + child: EasyRefresh( + header: const MaterialHeader(), + onRefresh: UserService.instance.refreshProfile, + child: _buildListContent(context), + ), + ); + } + + Widget _buildMobileLayout(BuildContext context) { + return Scaffold( + body: AnnotatedRegion( + value: Get.isDarkMode + ? SystemUiOverlayStyle.light.copyWith( + systemNavigationBarColor: Colors.transparent, + ) + : SystemUiOverlayStyle.dark.copyWith( + systemNavigationBarColor: Colors.transparent, + ), + child: SafeArea( + child: EasyRefresh( + header: const MaterialHeader(), + onRefresh: UserService.instance.refreshProfile, + child: _buildListContent(context), + ), + ), + ), + ); + } + + Widget _buildListContent(BuildContext context) { + return ListView( + padding: AppStyle.edgeInsetsA4, + children: [ + AppStyle.vGap12, + // 用户名、头像 + Obx( + () => Visibility( + visible: UserService.instance.logined.value, + child: ListTile( + leading: UserPhoto( + url: UserService.instance.userProfile.value?.cover, + size: 48, + ), + title: Text.rich( + TextSpan( + text: UserService + .instance.userProfile.value?.nickname ?? + UserService.instance.nickname, + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Visibility( + visible: (UserService.instance.userProfile.value + ?.userfeeinfo?.isVip ?? + false), + child: Padding( + padding: AppStyle.edgeInsetsL4, + child: Image.asset( + "assets/images/vip.png", + height: 16, + ), + ), + ), + ), + ], + ), + ), + subtitle: Text( + UserService.instance.isVip + ? UserService.instance.vipInfo + : UserService.instance.sign, + style: Get.textTheme.bodySmall, + ), + trailing: IconButton( + onPressed: controller.logout, + icon: const Icon(Remix.logout_box_r_line), + ), + ), + ), + ), + Obx( + () => Visibility( + visible: !UserService.instance.logined.value, + child: ListTile( + leading: const UserPhoto( + url: "", + size: 48, + ), + title: const Text( + "未登录", + style: TextStyle(height: 1.0), + ), + subtitle: const Text( + "点击前往登录", + ), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: controller.login, + ), + ), + ), + Obx( + () => _buildCard( + context, + children: [ + Visibility( + visible: UserService.instance.logined.value, + child: ListTile( + leading: const Icon(Remix.heart_line), + title: const Text("我的订阅"), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: controller.toUserSubscribe, + ), + ), + // Visibility( + // visible: UserService.instance.logined.value, + // child: ListTile( + // leading: const Icon(Remix.history_line), + // title: const Text("浏览记录"), + // trailing: const Icon( + // Icons.chevron_right, + // color: Colors.grey, + // ), + // onTap: controller.toUserHistory, + // ), + // ), + // Visibility( + // visible: UserService.instance.logined.value, + // child: ListTile( + // leading: const Icon(Remix.chat_smile_2_line), + // title: const Text("我的评论"), + // trailing: const Icon( + // Icons.chevron_right, + // color: Colors.grey, + // ), + // onTap: controller.userComment, + // ), + // ), + ], + ), + ), + _buildCard( + context, + children: [ + ListTile( + leading: const Icon(Remix.file_history_line), + title: const Text("本机记录"), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: controller.toLocalHistory, + ), + ListTile( + leading: const Icon(Remix.star_line), + title: const Text("本机收藏"), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: controller.toFavorite, + ), + ListTile( + leading: const Icon(Remix.download_line), + title: const Text("漫画下载"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () => Visibility( + visible: ComicDownloadService + .instance.taskQueues.isNotEmpty, + child: Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: AppStyle.radius24, + ), + width: 20, + height: 20, + child: Center( + child: Text( + "${ComicDownloadService.instance.taskQueues.length}", + style: const TextStyle( + fontSize: 10, + color: Colors.white, + ), + ), + ), + ), + ), + ), + Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + ], + ), + onTap: controller.comicDownload, + ), + ListTile( + leading: const Icon(Remix.download_line), + title: const Text("小说下载"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Obx( + () => Visibility( + visible: NovelDownloadService + .instance.taskQueues.isNotEmpty, + child: Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: AppStyle.radius24, + ), + width: 20, + height: 20, + child: Center( + child: Text( + "${NovelDownloadService.instance.taskQueues.length}", + style: const TextStyle( + fontSize: 10, + color: Colors.white, + ), + ), + ), + ), + ), + ), + Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + ], + ), + onTap: controller.novelDownload, + ), + ], + ), + _buildCard( + context, + children: [ + ListTile( + leading: Icon( + Get.isDarkMode ? Remix.moon_line : Remix.sun_line), + title: const Text("显示主题"), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: controller.setTheme, + ), + ListTile( + leading: const Icon(Remix.settings_line), + title: const Text("更多设置"), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: controller.toSettings, + ), + ], + ), + _buildCard( + context, + children: [ + ListTile( + leading: const Icon(Remix.error_warning_line), + title: const Text("免责声明"), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: DialogUtils.showStatement, + ), + // ListTile( + // leading: const Icon(Remix.github_fill), + // title: const Text("开源主页"), + // trailing: const Icon( + // Icons.chevron_right, + // color: Colors.grey, + // ), + // onTap: () { + // launchUrlString( + // "https://github.com/xiaoyaocz/flutter_dmzj", + // mode: LaunchMode.externalApplication, + // ); + // }, + // ), + // ListTile( + // leading: const Icon(Remix.upload_2_line), + // title: const Text("检查更新"), + // trailing: const Icon( + // Icons.chevron_right, + // color: Colors.grey, + // ), + // onTap: controller.checkUpdate, + // ), + ListTile( + leading: const Icon(Remix.information_line), + title: const Text("关于APP"), + trailing: Icon( + Icons.chevron_right, + color: Theme.of(context).iconTheme.color?.withOpacity(0.5), + ), + onTap: controller.about, + ), + ], + ), + ], + ); + } + + Widget _buildCard(BuildContext context, {required List children}) { + return Container( + margin: AppStyle.edgeInsetsH12.copyWith(top: 12), + child: Material( + color: Theme.of(context).cardColor, + borderRadius: AppStyle.radius8, + child: Theme( + data: Theme.of(context).copyWith( + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder(borderRadius: AppStyle.radius8), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ), + ), + ), + ); + } +} diff --git a/lib/requests/comic_request.dart b/lib/requests/comic_request.dart new file mode 100644 index 0000000..21e14ba --- /dev/null +++ b/lib/requests/comic_request.dart @@ -0,0 +1,462 @@ +import 'dart:convert'; + +import 'package:flutter_dmzj/app/app_error.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/comic/author_model.dart'; +import 'package:flutter_dmzj/models/comic/category_comic_model.dart'; +import 'package:flutter_dmzj/models/comic/category_filter_model.dart'; +import 'package:flutter_dmzj/models/comic/category_item_model.dart'; +import 'package:flutter_dmzj/models/comic/chapter_detail_model.dart'; +import 'package:flutter_dmzj/models/comic/chapter_detail_web_model.dart'; +import 'package:flutter_dmzj/models/comic/chapter_info.dart'; +import 'package:flutter_dmzj/models/comic/comic_related_model.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/models/comic/detail_model.dart'; +import 'package:flutter_dmzj/models/comic/detail_v1_model.dart'; +import 'package:flutter_dmzj/models/comic/rank_item_model.dart'; +import 'package:flutter_dmzj/models/comic/recommend_model.dart'; +import 'package:flutter_dmzj/models/comic/search_item.dart'; +import 'package:flutter_dmzj/models/comic/search_model.dart'; +import 'package:flutter_dmzj/models/comic/special_model.dart'; +import 'package:flutter_dmzj/models/comic/update_item_model.dart'; +import 'package:flutter_dmzj/models/comic/view_point_model.dart'; +import 'package:flutter_dmzj/models/comic/web_search_model.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/requests/common/http_client.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; + +import '../models/comic/special_detail_model.dart'; + +class ComicRequest { + /// 漫画-推荐 + Future> recommend() async { + var list = []; + var result = await HttpClient.instance.getJson('/comic/recommend/index'); + + for (var item in result) { + list.add(ComicRecommendModel.fromJson(item)); + } + return list; + } + + /// 猜你喜欢 + Future> refreshRecommend(int categoryId, + {int page = 1, int size = 3}) async { + var result = await HttpClient.instance.getJson( + '/comic/recommend/more', + queryParameters: {"cateId": categoryId, "size": size, "page": page}, + ); + List list = []; + + for (var item in result["data"]["recommendList"]) { + list.add(ComicRecommendItemModel.fromJson(item)); + } + return list; + } + + /// 首页-我的订阅 + Future recommendSubscribe() async { + var result = await HttpClient.instance.getJson( + '/comic/sub/list', + needLogin: true, + checkCode: true, + queryParameters: {"status": 0, "firstLetter": "", "page": 1, "size": 3}, + ); + + var list = []; + for (var item in result["subList"]) { + list.add(ComicRecommendItemModel.fromJson(item)); + } + return ComicRecommendModel( + categoryId: 49, + title: "我的订阅", + sort: 0, + data: list, + ); + } + + /// 最近更新 + Future> latest( + {required int type, int page = 1}) async { + var result = await HttpClient.instance.getJson( + '/comic/update/list/$type/$page', + needLogin: true, + ); + var list = []; + for (var item in result["data"]) { + list.add(ComicUpdateItemModel.fromJson(item)); + } + return list; + } + + /// 分类 + Future> categores() async { + var list = []; + var result = await HttpClient.instance.getJson( + '/comic/filter/category', + queryParameters: {"source": 1}, + checkCode: true, + ); + for (var item in result["cateList"]) { + list.add(ComicCategoryItemModel.fromJson(item)); + } + // 百合赛高 + list.add(ComicCategoryItemModel(tagId: 3243, title: "ゆり", cover: "")); + return list; + } + + /// 分类-筛选 + Future> categoryFilter() async { + var result = await HttpClient.instance.getJson( + '/comic/filter/category', + queryParameters: {"source": 1}, + checkCode: true, + ); + // for (var item in result["cateList"]) { + // list.add(ComicCategoryFilterModel.fromJson(item)); + // } + var list = []; + for (var item in result["cateList"]) { + list.add(ComicCategoryFilterItemModel.fromJson(item)); + } + return [ + ComicCategoryFilterModel(title: "全部分类", items: list), + ]; + } + + /// 分类下漫画 + /// - [ids] 标签 + /// - [sort] 排序,0=人气,1=更新 + /// - [page] 页数,从0开始 + Future> categoryComic({ + required int id, + int sort = 1, + int page = 1, + int status = 0, + }) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/comic/filter/list', + queryParameters: { + "theme": id, + "status": 0, + "sortType": sort, + "page": page, + "size": 20, + }, + checkCode: true, + needLogin: true // 登录可以更多内容 + ); + for (var item in result["comicList"]) { + list.add(ComicCategoryComicModel.fromJson(item)); + } + return list; + } + + /// 排行榜 + Future> rank({ + required int tagId, + required byTime, + required rankType, + int page = 1, + }) async { + var result = await HttpClient.instance.getJson( + '/comic/rank/list', + queryParameters: { + 'tag_id': tagId, + 'by_time': byTime, + 'rank_type': rankType, + 'page': page + }, + ); + var list = []; + for (var item in result["data"]) { + list.add(ComicRankListItemModel.fromJson(item)); + } + return list; + } + + /// 排行榜-分类 + Future> rankFilter() async { + var result = await HttpClient.instance.getJson( + '/comic/filter/category', + queryParameters: {"source": 1}, + checkCode: true, + ); + Map map = { + 0: "全部分类", + 3243: "ゆり" + }; + for (var item in result["cateList"]) { + map.addAll({ + item["tagId"]: item["title"], + }); + } + return map; + } + + /// 专题 + Future> special({int page = 1}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/subject/0/$page.json', + checkCode: true, + ); + for (var item in result) { + list.add(ComicSpecialModel.fromJson(item)); + } + return list; + } + + /// 专题 + Future specialDetail({required int id}) async { + var result = await HttpClient.instance.getJson( + '/subject/$id.json', + checkCode: true, + ); + + return ComicSpecialDetailModel.fromJson(result); + } + + /// 作者详情 + Future authorDetail({required int id}) async { + var result = await HttpClient.instance.getJson( + '/UCenter/author/$id.json', + ); + + return ComicAuthorModel.fromJson(result); + } + + /// 作品相关 + Future related({required int id}) async { + var result = await HttpClient.instance.getJson( + '/v3/comic/related/$id.json', + ); + + return ComicRelatedModel.fromJson(result); + } + + Future comicDetail( + {required int comicId, bool priorityV1 = false}) async { + ComicDetailInfo info; + var errorMsg = ""; + try { + if (priorityV1) { + info = ComicDetailInfo.fromV1(await comicDetailV1(comicId: comicId), + isHide: true); + } else { + info = ComicDetailInfo.fromV4(await comicDetailV4(comicId: comicId)); + } + } catch (e) { + errorMsg += "${priorityV1 ? "V1" : "V4"}:$e"; + try { + if (priorityV1) { + info = ComicDetailInfo.fromV4(await comicDetailV4(comicId: comicId)); + } else { + info = ComicDetailInfo.fromV1(await comicDetailV1(comicId: comicId), + isHide: e.toString() == "漫画不存在"); + } + } catch (e) { + errorMsg += "\n${priorityV1 ? "V4" : "V1"}:$e"; + throw AppError("ComicID:$comicId\n无法读取漫画信息,可能需要登录或有等级限制\n$errorMsg"); + } + } + return info; + } + + /// 漫画详情 + Future comicDetailV4({ + required int comicId, + }) async { + var result = await HttpClient.instance.getJson( + '/comic/detail/$comicId', + needLogin: true, + checkCode: true, + ); + + return ComicDetailModel.fromJson(result); + } + + /// 漫画详情 + Future comicDetailV1({ + required int comicId, + }) async { + var result = await HttpClient.instance.getJson( + '/dynamic/comicinfo/$comicId.json', + baseUrl: "https://api.dmzj.com", + needLogin: true, + ); + var data = json.decode(result); + if (data["result"] != 1) { + throw AppError(data["msg"]); + } + if (data["data"]?["info"]?["id"] == null) { + throw AppError("无法读取漫画信息"); + } + return ComicDetailV1Model.fromJson(data["data"]); + } + + /// 漫画搜索 + /// - [page] 页数从0开始 + /// - [keyword] 关键字 + Future> search( + {required String keyword, int page = 1}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/search/index', + queryParameters: { + "keyword": keyword, + "page": page, + "size": 20, + }, + checkCode: true, + ); + for (var item in result["list"]) { + list.add(ComicSearchModel.fromJson(item)); + } + return list.map((e) => SearchComicItem.fromApi(e)).toList(); + } + + /// 漫画搜索热词 + Future> searchHotWord() async { + var result = await HttpClient.instance.getJson( + '/search/hot/0.json', + ); + Map map = {}; + for (var item in result) { + map.addAll({ + item["id"]: item["name"], + }); + } + return map; + } + + /// 章节详情 + Future chapterDetail( + {required int comicId, + required int chapterId, + required bool useHD}) async { + ComicChapterDetail info; + + try { + //查询本地是否存在 + var localInfo = + ComicDownloadService.instance.box.get("${comicId}_$chapterId"); + if (localInfo != null && localInfo.status == DownloadStatus.complete) { + return ComicChapterDetail.fromDownload(localInfo); + } + + var v4 = await chapterDetailV4(comicId: comicId, chapterId: chapterId); + info = ComicChapterDetail.fromV4(v4, useHD); + } catch (e) { + Log.logPrint(e); + try { + var v1 = await chapterDetailWeb(comicId: comicId, chapterId: chapterId); + info = ComicChapterDetail.fromWebApi(v1); + } catch (e) { + Log.logPrint(e); + + throw AppError("ComicID:$comicId ChapterID:$chapterId\n无法读取章节信息"); + } + } + return info; + } + + /// 章节详情-V4 + Future chapterDetailV4( + {required int comicId, required int chapterId}) async { + var result = await HttpClient.instance.getJson( + '/comic/chapter/$comicId/$chapterId', + needLogin: true, + checkCode: true, + ); + + var data = ComicChapterDetailModel.fromJson(result["data"]); + + return data; + } + + /// 章节详情-WebAPI + Future chapterDetailWeb( + {required int comicId, required int chapterId}) async { + var result = await HttpClient.instance.getJson( + '/chapinfo/$comicId/$chapterId.html', + baseUrl: "https://m.idmzj.com", + needLogin: true, + ); + if (result.toString().startsWith("{")) { + var data = json.decode(result); + return ComicChapterDetailWebModel.fromJson(data); + } else { + throw AppError(result); + } + } + + /// 观点、吐槽 + Future> viewPoints( + {required int comicId, required int chapterId}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/viewPoint/0/$comicId/$chapterId.json', + ); + for (var item in result) { + list.add(ComicViewPointModel.fromJson(item)); + } + return list; + } + + /// 点赞观点、吐槽 + Future likeViewPoint({required int comicId, required int id}) async { + await HttpClient.instance.postJson( + '/viewPoint/praise', + checkCode: true, + data: { + "sub_type": comicId, + "uid": UserService.instance.userId, + "vote_id": id, + }, + ); + return true; + } + + /// 点赞观点、吐槽 + Future sendViewPoint( + {required int comicId, + required int chapterId, + required String content, + required int page}) async { + await HttpClient.instance.postJson( + '/viewPoint/addv2', + checkCode: true, + data: { + "sub_type": comicId, + "uid": UserService.instance.userId, + "dmzj_token": UserService.instance.dmzjToken, + "page": page, + "type": 0, + "third_type": chapterId, + "content": content, + }, + ); + return true; + } + + /// 漫画搜索-Web接口 + /// - [keyword] 关键字 + Future> searchWeb({required String keyword}) async { + var list = []; + var result = await HttpClient.instance.getText( + 'http://sacg.idmzj.com/comicsum/search.php', + baseUrl: "", + queryParameters: { + "s": keyword, + }, + ); + var data = jsonDecode(result.substring(20, result.lastIndexOf(';'))); + for (var item in data) { + list.add(ComicWebSearchModel.fromJson(item)); + } + return list.map((e) => SearchComicItem.fromWeb(e)).toList(); + } +} diff --git a/lib/requests/comment_request.dart b/lib/requests/comment_request.dart new file mode 100644 index 0000000..01bc2ed --- /dev/null +++ b/lib/requests/comment_request.dart @@ -0,0 +1,169 @@ +import 'package:flutter_dmzj/app/app_error.dart'; +import 'package:flutter_dmzj/models/comment/comment_item.dart'; +import 'package:flutter_dmzj/models/comment/user_comment_item.dart'; +import 'package:flutter_dmzj/requests/common/api.dart'; +import 'package:flutter_dmzj/requests/common/http_client.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; +import 'package:get/get.dart'; +import 'package:html_unescape/html_unescape.dart'; + +class CommentRequest { + var unescape = HtmlUnescape(); + + /// 读取最新的评论 + /// - [type] 类型 + /// - [objId] ID + /// - [page] 页数 + /// - [pageSize] 每页数量 + Future> getComment({ + required int type, + required int objId, + int sort = 1, + int page = 1, + int pageSize = 30, + }) async { + List ls = []; + Map result = await HttpClient.instance.getJson( + '/comment/list', + baseUrl: Api.BASE_URL, + queryParameters: { + "type": type, + "objId": objId, + "sort": sort, + "page": page - 1, + "size": pageSize, + }, + ); + if (result["errno"] != 0) { + throw AppError(result["errmsg"].toString()); + } + if (result["data"]["commentIdList"] == null) { + return []; + } + var ids = result["data"]["commentIdList"]; + var comments = result["data"]["commentList"]; + for (String id in ids) { + var idSplit = id.split(","); + var item = _parseLatestCommentItem(comments, idSplit.first, type); + if (idSplit.length > 1) { + item.parents = []; + for (var id2 in idSplit.skip(1)) { + item.parents.insert(0, _parseLatestCommentItem(comments, id2, type)); + } + } + if (item.id != 0) { + ls.add(item); + } + } + return ls; + } + + CommentItem _parseLatestCommentItem(Map comments, String id, int type) { + if (!comments.containsKey(id)) { + return CommentItem.createEmpty(); + } + var item = comments[id]; + //返回的类型非常随机,有时候是int,有时候是string,所以使用int.tryParse + return CommentItem( + type: type, + id: int.tryParse(item["id"].toString()) ?? 0, + objId: int.tryParse(item["obj_id"].toString()) ?? 0, + content: unescape.convert(item["content"].toString()), + photo: item["photo"].toString(), + createTime: int.tryParse(item["create_time"].toString()) ?? 0, + images: item["upload_images"] + .toString() + .split(",") + .where((x) => x.isNotEmpty) + .toList(), + likeAmount: (int.tryParse(item["like_amount"].toString()) ?? 0).obs, + nickname: item["nickname"].toString(), + replyAmount: int.tryParse(item["reply_amount"].toString()) ?? 0, + gender: int.tryParse(item["sex"].toString()) ?? 0, + userId: int.tryParse(item["sender_uid"].toString()) ?? 0, + originId: int.tryParse(item["origin_comment_id"].toString()) ?? 0, + ); + } + + /// 发表评论 + /// - [objId] ID + /// - [type] 类型 ,见AppConstant + /// - [content] 内容 + /// - [toCommentId] 回复评论ID + /// - [originCommentId] 原始评论ID + /// - [toUid] 回复用户 + Future sendComment({ + required int objId, + required int type, + required String content, + String toCommentId = "0", + String originCommentId = "0", + String toUid = "0", + }) async { + var result = await HttpClient.instance.postJson( + "/v1/$type/new/add/app", + baseUrl: Api.BASE_URL, + data: { + "obj_id": objId, + "to_comment_id": toCommentId, + "origin_comment_id": originCommentId, + "to_uid": toUid, + "sender_terminal": 1, + "content": content, + "dmzj_token": UserService.instance.dmzjToken, + "_debug": 0 + }, + ); + if (result["code"] != 0) { + throw AppError(result["msg"].toString()); + } + return true; + } + + /// 评论点赞 + Future likeComment({ + required int commentId, + required int objId, + required int type, + }) async { + await HttpClient.instance.getJson( + "/v1/$type/like/$commentId", + baseUrl: Api.BASE_URL, + queryParameters: { + "comment_id": commentId, + "obj_id": objId, + "type": type, + }, + needLogin: true, + withDefaultParameter: true, + checkCode: true, + ); + + return true; + } + + /// 读取用户的评论 + /// - [type] 类型 0=漫画,1=轻小说,2=新闻 + /// - [uid] 用户ID + /// - [page] 页数,从0开始 + Future> getUserComment({ + required int type, + required int uid, + int page = 0, + }) async { + List ls = []; + var result = await HttpClient.instance.getJson( + type == 1 + ? '/comment/owner/1/$uid/$page.json' + : '/v3/old/comment/owner/$type/$uid/$page.json', + baseUrl: Api.BASE_URL, + withDefaultParameter: true, + needLogin: true, + ); + for (var item in result) { + ls.add(UserCommentItem.fromJson(item)); + } + + return ls; + } +} diff --git a/lib/requests/common/api.dart b/lib/requests/common/api.dart new file mode 100644 index 0000000..8eeb121 --- /dev/null +++ b/lib/requests/common/api.dart @@ -0,0 +1,81 @@ +// ignore_for_file: constant_identifier_names + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:crypton/crypton.dart'; +import 'package:flutter_dmzj/app/app_error.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; + +class Api { + static const String DMZJ_DOMAIN_NAME = "dmzj.com"; + static const String IDMZJ_DOMAIN_NAME = "idmzj.com"; + static const String MUWAI_DOMAIN_NAME = "muwai.com"; + static const String DOMAIN_NAME = "zaimanhua.com"; + + /// V3接口,无加密 + static const String BASE_URL = "https://v4api.zaimanhua.com/app/v1"; + + /// 用户 + static const String BASE_URL_USER = "https://account-api.zaimanhua.com/v1"; + + /// Interface + static const String BASE_URL_INTERFACE = + "http://nninterface.$IDMZJ_DOMAIN_NAME"; + + /// V4 API的密钥 + static const V4_PRIVATE_KEY = + "MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAK8nNR1lTnIfIes6oRWJNj3mB6OssDGx0uGMpgpbVCpf6+VwnuI2stmhZNoQcM417Iz7WqlPzbUmu9R4dEKmLGEEqOhOdVaeh9Xk2IPPjqIu5TbkLZRxkY3dJM1htbz57d/roesJLkZXqssfG5EJauNc+RcABTfLb4IiFjSMlTsnAgMBAAECgYEAiz/pi2hKOJKlvcTL4jpHJGjn8+lL3wZX+LeAHkXDoTjHa47g0knYYQteCbv+YwMeAGupBWiLy5RyyhXFoGNKbbnvftMYK56hH+iqxjtDLnjSDKWnhcB7089sNKaEM9Ilil6uxWMrMMBH9v2PLdYsqMBHqPutKu/SigeGPeiB7VECQQDizVlNv67go99QAIv2n/ga4e0wLizVuaNBXE88AdOnaZ0LOTeniVEqvPtgUk63zbjl0P/pzQzyjitwe6HoCAIpAkEAxbOtnCm1uKEp5HsNaXEJTwE7WQf7PrLD4+BpGtNKkgja6f6F4ld4QZ2TQ6qvsCizSGJrjOpNdjVGJ7bgYMcczwJBALvJWPLmDi7ToFfGTB0EsNHZVKE66kZ/8Stx+ezueke4S556XplqOflQBjbnj2PigwBN/0afT+QZUOBOjWzoDJkCQClzo+oDQMvGVs9GEajS/32mJ3hiWQZrWvEzgzYRqSf3XVcEe7PaXSd8z3y3lACeeACsShqQoc8wGlaHXIJOHTcCQQCZw5127ZGs8ZDTSrogrH73Kw/HvX55wGAeirKYcv28eauveCG7iyFR0PFB/P/EDZnyb+ifvyEFlucPUI0+Y87F"; + static Uint8List decryptV4(String text) { + try { + RSAKeypair rsaKeypair = + RSAKeypair(RSAPrivateKey.fromString(V4_PRIVATE_KEY)); + var decrypted = rsaKeypair.privateKey.decryptData(base64.decode(text)); + return decrypted; + } catch (e) { + throw AppError('返回数据解密失败'); + } + } + + /// 签名 + static String sign(String content, String mode) { + var utf8Content = utf8.encode(mode + content); + + return md5.convert(utf8Content).toString(); + } + + static const String VERSION = "3.8.2"; + static String get timeStamp => + (DateTime.now().millisecondsSinceEpoch / 1000).toStringAsFixed(0); + + /// 默认的参数 + static Map getDefaultParameter({bool withUid = false}) { + var map = { + "channel": "android", + //"version": VERSION, + "timestamp": timeStamp + }; + if (withUid && UserService.instance.logined.value) { + map.addAll({"uid": UserService.instance.userId}); + } + return map; + } + + /// 小说正文链接 + static String getNovelContentUrl( + {required int volumeId, required int chapterId}) { + // var path = "/lnovel/${volumeId}_$chapterId.txt"; + // var ts = (DateTime.now().millisecondsSinceEpoch / 1000).toStringAsFixed(0); + // var key = + // "IBAAKCAQEAsUAdKtXNt8cdrcTXLsaFKj9bSK1nEOAROGn2KJXlEVekcPssKUxSN8dsfba51kmHM"; + // key += path; + // key += ts; + // key = md5.convert(utf8.encode(key)).toString().toLowerCase(); + + // return "http://jurisdiction.idmzj.com$path?t=$ts&k=$key"; + + //https://v4api.zaimanhua.com/app/v1/novel/download/chapter?volumeId=12458&chapterId=127221 + return "$BASE_URL/novel/download/chapter?volumeId=$volumeId&chapterId=$chapterId"; + } +} diff --git a/lib/requests/common/custom_interceptor.dart b/lib/requests/common/custom_interceptor.dart new file mode 100644 index 0000000..0fbd612 --- /dev/null +++ b/lib/requests/common/custom_interceptor.dart @@ -0,0 +1,53 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dmzj/app/log.dart'; + +class CustomInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + options.extra["ts"] = DateTime.now().millisecondsSinceEpoch; + super.onRequest(options, handler); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + var time = + DateTime.now().millisecondsSinceEpoch - err.requestOptions.extra["ts"]; + Log.e('''【HTTP请求错误】 耗时:${time}ms +Request Method:${err.requestOptions.method} +Response Code:${err.response?.statusCode} +Request URL:${err.requestOptions.uri} +Request Query:${err.requestOptions.queryParameters} +Request Data:${err.requestOptions.data} +Request Headers:${err.requestOptions.headers} +Response Headers:${err.response?.headers.map} +Response Data:${err.response?.data}''', err.stackTrace); + super.onError(err, handler); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + var time = DateTime.now().millisecondsSinceEpoch - + response.requestOptions.extra["ts"]; + if (response.requestOptions.uri.toString().contains(".txt")) { + Log.i( + '''【HTTP请求响应】 耗时:${time}ms +Request Method:${response.requestOptions.method} +Request Code:${response.statusCode} +Request URL:${response.requestOptions.uri}''', + ); + return super.onResponse(response, handler); + } + Log.i( + '''【HTTP请求响应】 耗时:${time}ms +Request Method:${response.requestOptions.method} +Request Code:${response.statusCode} +Request URL:${response.requestOptions.uri} +Request Query:${response.requestOptions.queryParameters} +Request Data:${response.requestOptions.data} +Request Headers:${response.requestOptions.headers} +Response Headers:${response.headers.map} +Response Data:${response.data}''', + ); + super.onResponse(response, handler); + } +} diff --git a/lib/requests/common/http_client.dart b/lib/requests/common/http_client.dart new file mode 100644 index 0000000..e5abce0 --- /dev/null +++ b/lib/requests/common/http_client.dart @@ -0,0 +1,260 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:flutter_dmzj/app/app_error.dart'; +import 'package:flutter_dmzj/requests/common/api.dart'; +import 'package:flutter_dmzj/requests/common/custom_interceptor.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; + +class HttpClient { + static HttpClient? _httpUtil; + + static HttpClient get instance { + _httpUtil ??= HttpClient(); + return _httpUtil!; + } + + late Dio dio; + HttpClient() { + dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 20), + receiveTimeout: const Duration(seconds: 20), + sendTimeout: const Duration(seconds: 20), + ), + ); + dio.interceptors.add(CustomInterceptor()); + } + + /// Get请求 + /// * [path] 请求链接 + /// * [queryParameters] 请求参数 + /// * [cancel] 任务取消Token + /// * [needLogin] 是否需要登录 + /// * [withDefaultParameter] 是否需要带上一些默认参数 + /// * [responseType] 返回的类型 + Future get( + String path, { + Map? queryParameters, + String baseUrl = Api.BASE_URL, + CancelToken? cancel, + bool withDefaultParameter = true, + bool needLogin = false, + ResponseType responseType = ResponseType.json, + bool checkCode = false, + }) async { + Map header = {}; + queryParameters ??= {}; + var query = Api.getDefaultParameter(withUid: needLogin); + if (withDefaultParameter) { + queryParameters.addAll(query); + } + if (needLogin) { + if (UserService.instance.logined.value) { + header['Authorization'] = 'Bearer ${UserService.instance.dmzjToken}'; + } + } + + try { + var result = await dio.get( + baseUrl + path, + queryParameters: queryParameters, + options: Options( + responseType: responseType, + headers: header, + ), + cancelToken: cancel, + ); + if (checkCode && result.data is Map) { + var data = result.data as Map; + if (data['errno'] == 0) { + return result.data['data']; + } else { + throw AppError( + result.data['errmsg'].toString(), + code: int.tryParse(result.data['errno'].toString()) ?? 0, + ); + } + } + return result.data; + } on DioException catch (e) { + if (e.type == DioExceptionType.cancel) { + rethrow; + } + if (e.type == DioExceptionType.badResponse) { + return throw AppError("请求失败:${e.response?.statusCode ?? -1}"); + } + throw AppError("请求失败,请检查网络"); + } + } + + /// Get 请求,返回JSON + /// * [path] 请求链接 + /// * [queryParameters] 请求参数 + /// * [cancel] 任务取消Token + /// * [needLogin] 是否需要登录 + /// * [withDefaultParameter] 是否需要带上一些默认参数 + Future getJson( + String path, { + Map? queryParameters, + String baseUrl = Api.BASE_URL, + CancelToken? cancel, + bool withDefaultParameter = true, + bool needLogin = false, + bool checkCode = false, + }) async { + var result = await get( + path, + queryParameters: queryParameters, + baseUrl: baseUrl, + cancel: cancel, + withDefaultParameter: withDefaultParameter, + needLogin: needLogin, + responseType: ResponseType.json, + checkCode: checkCode, + ); + if (result is Map || result is List) { + return result; + } else if (result is String) { + return jsonDecode(result); + } + return result; + } + + /// Get 请求,返回Text + /// * [path] 请求链接 + /// * [queryParameters] 请求参数 + /// * [cancel] 任务取消Token + /// * [needLogin] 是否需要登录 + /// * [withDefaultParameter] 是否需要带上一些默认参数 + Future getText( + String path, { + Map? queryParameters, + String baseUrl = Api.BASE_URL, + CancelToken? cancel, + bool withDefaultParameter = true, + bool needLogin = false, + }) async { + return await get( + path, + queryParameters: queryParameters, + baseUrl: baseUrl, + cancel: cancel, + withDefaultParameter: withDefaultParameter, + needLogin: needLogin, + responseType: ResponseType.plain, + ); + } + + /// Get 请求,返回解密后Bytes + /// * [path] 请求链接 + /// * [queryParameters] 请求参数 + /// * [cancel] 任务取消Token + /// * [needLogin] 是否需要登录 + /// * [withDefaultParameter] 是否需要带上一些默认参数 + Future getEncryptV4( + String path, { + Map? queryParameters, + String baseUrl = Api.BASE_URL, + CancelToken? cancel, + bool withDefaultParameter = true, + bool needLogin = false, + }) async { + var result = await get( + path, + queryParameters: queryParameters, + baseUrl: baseUrl, + cancel: cancel, + withDefaultParameter: withDefaultParameter, + needLogin: needLogin, + responseType: ResponseType.plain, + ); + var resultBytes = Api.decryptV4(result); + return resultBytes; + } + + /// Get 请求,返回byte + /// * [path] 请求链接 + /// * [queryParameters] 请求参数 + /// * [cancel] 任务取消Token + /// * [needLogin] 是否需要登录 + /// * [withDefaultParameter] 是否需要带上一些默认参数 + Future getBytes( + String path, { + Map? queryParameters, + String baseUrl = Api.BASE_URL, + CancelToken? cancel, + bool withDefaultParameter = true, + bool needLogin = false, + }) async { + return await get( + path, + queryParameters: queryParameters, + baseUrl: baseUrl, + cancel: cancel, + withDefaultParameter: withDefaultParameter, + needLogin: needLogin, + responseType: ResponseType.bytes, + ); + } + + /// Post请求,返回Map + /// * [path] 请求链接 + /// * [data] 发送数据 + /// * [queryParameters] 请求参数 + /// * [cancel] 任务取消Token + Future postJson( + String path, { + Map? queryParameters, + Map? data, + String baseUrl = Api.BASE_URL, + CancelToken? cancel, + bool formUrlEncoded = false, + bool checkCode = false, + bool needLogin = false, + }) async { + Map header = {}; + queryParameters ??= {}; + if (needLogin) { + if (UserService.instance.logined.value) { + header['Authorization'] = 'Bearer ${UserService.instance.dmzjToken}'; + } + } + try { + var result = await dio.post( + baseUrl + path, + queryParameters: queryParameters, + data: data, + options: Options( + responseType: ResponseType.json, + headers: header, + contentType: + formUrlEncoded ? Headers.formUrlEncodedContentType : null, + ), + cancelToken: cancel, + ); + var jsonMap = result.data; + if (jsonMap is String) { + jsonMap = jsonDecode(jsonMap); + } + if (checkCode) { + var data = result.data as Map; + if (data['errno'] == 0) { + return result.data['data']; + } else { + throw AppError( + result.data['errmsg'].toString(), + code: int.tryParse(result.data['errno'].toString()) ?? 0, + ); + } + } + return result.data; + } on DioException catch (e) { + if (e.type == DioExceptionType.badResponse) { + return throw AppError("请求失败:状态码:${e.response?.statusCode ?? -1}"); + } + throw AppError("请求失败,请检查网络"); + } + } +} diff --git a/lib/requests/common_request.dart b/lib/requests/common_request.dart new file mode 100644 index 0000000..6c65562 --- /dev/null +++ b/lib/requests/common_request.dart @@ -0,0 +1,41 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dmzj/models/version_model.dart'; + +/// 通用的请求 +class CommonRequest { + Future checkUpdate() async { + try { + return await checkUpdateGitMirror(); + } catch (e) { + return await checkUpdateJsDelivr(); + } + } + + /// 检查更新 + Future checkUpdateGitMirror() async { + var result = await Dio().get( + "https://raw.gitmirror.com/xiaoyaocz/flutter_dmzj/zaimanhua/document/app_version.json", + queryParameters: { + "ts": DateTime.now().millisecondsSinceEpoch, + }, + options: Options( + responseType: ResponseType.json, + ), + ); + return VersionModel.fromJson(result.data); + } + + /// 检查更新 + Future checkUpdateJsDelivr() async { + var result = await Dio().get( + "https://cdn.jsdelivr.net/gh/xiaoyaocz/flutter_dmzj@zaimanhua/document/app_version.json", + queryParameters: { + "ts": DateTime.now().millisecondsSinceEpoch, + }, + options: Options( + responseType: ResponseType.json, + ), + ); + return VersionModel.fromJson(result.data); + } +} diff --git a/lib/requests/news_request.dart b/lib/requests/news_request.dart new file mode 100644 index 0000000..f052fd1 --- /dev/null +++ b/lib/requests/news_request.dart @@ -0,0 +1,134 @@ +import 'dart:convert'; + +import 'package:flutter_dmzj/app/app_error.dart'; +import 'package:flutter_dmzj/models/news/news_banner_model.dart'; +import 'package:flutter_dmzj/models/news/news_list_item_model.dart'; +import 'package:flutter_dmzj/models/news/news_stat_model.dart'; +import 'package:flutter_dmzj/models/news/news_tag_model.dart'; +import 'package:flutter_dmzj/requests/common/api.dart'; +import 'package:flutter_dmzj/requests/common/http_client.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; + +class NewsRequest { + /// 新闻分类 + Future> category() async { + var list = []; + var result = await HttpClient.instance.getJson( + '/news/category', + ); + for (var item in result["data"]["cateList"]) { + list.add(NewsTagModel.fromJson(item)); + } + return list; + } + + /// 新闻Banner + Future> banner() async { + var list = []; + var result = await HttpClient.instance.getJson('/news/recommend'); + for (var item in result["data"]["recommendList"]) { + list.add(NewsBannerModel.fromJson(item)); + } + return list; + } + + /// 读取新闻列表 + /// - [id] 新闻分类ID + /// - [page] 页数,从1开始 + Future> getNewsList(int id, int page) async { + var result = await HttpClient.instance.getJson( + '/news/list/$id/${id == 0 ? 2 : 3}/$page', + ); + + if (result["errno"] != 0) { + throw AppError(result["errmsg"]); + } + var list = []; + for (var item in result["data"]["newsList"]) { + list.add(NewsListItemModel.fromJson(item)); + } + return list; + } + + /// 新闻数据 + /// - [newsId] 新闻ID + Future stat(int newsId) async { + var result = await HttpClient.instance.getJson( + '/v3/article/total/$newsId.json', + checkCode: true, + ); + + return NewsStatModel.fromJson(result); + } + + /// 新闻点赞 + /// - [newsId] 新闻ID + Future like(int newsId) async { + await HttpClient.instance.getJson( + '/article/mood/$newsId', + checkCode: true, + ); + + return true; + } + + /// 新闻检查收藏 + /// - [newsId] 新闻ID + Future checkCollect(int newsId) async { + var uid = UserService.instance.userId; + var par = {"uid": int.parse(uid), "sub_id": newsId}; + var parJson = jsonEncode(par); + var sign = Api.sign(parJson, 'app_news_sub'); + + var result = await HttpClient.instance.postJson( + '/api/news/subscribe/check', + baseUrl: Api.BASE_URL_INTERFACE, + data: { + "parm": parJson, + "sign": sign, + }, + ); + + return json.decode(result)["result"] == 809; + } + + /// 新闻收藏 + /// - [newsId] 新闻ID + Future collect(int newsId) async { + var uid = UserService.instance.userId; + var par = {"uid": int.parse(uid), "sub_id": newsId}; + var parJson = jsonEncode(par); + var sign = Api.sign(parJson, 'app_news_sub'); + + var result = await HttpClient.instance.postJson( + '/api/news/subscribe/add', + baseUrl: Api.BASE_URL_INTERFACE, + data: { + "parm": parJson, + "sign": sign, + }, + ); + + return json.decode(result)["result"] == 1000; + } + + /// 移除收藏 + /// - [newsId] 新闻ID + Future delCollect(int newsId) async { + var uid = UserService.instance.userId; + var par = {"uid": int.parse(uid), "sub_id": newsId}; + var parJson = jsonEncode(par); + var sign = Api.sign(parJson, 'app_news_sub'); + + var result = await HttpClient.instance.postJson( + '/api/news/subscribe/del', + baseUrl: Api.BASE_URL_INTERFACE, + data: { + "parm": parJson, + "sign": sign, + }, + ); + + return json.decode(result)["result"] == 1000; + } +} diff --git a/lib/requests/novel_request.dart b/lib/requests/novel_request.dart new file mode 100644 index 0000000..dc99e7f --- /dev/null +++ b/lib/requests/novel_request.dart @@ -0,0 +1,240 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dmzj/models/novel/category_filter_model.dart'; +import 'package:flutter_dmzj/models/novel/category_model.dart'; +import 'package:flutter_dmzj/models/novel/category_novel_model.dart'; +import 'package:flutter_dmzj/models/novel/detail_model.dart'; +import 'package:flutter_dmzj/models/novel/latest_model.dart'; +import 'package:flutter_dmzj/models/novel/rank_model.dart'; +import 'package:flutter_dmzj/models/novel/recommend_model.dart'; +import 'package:flutter_dmzj/models/novel/search_model.dart'; +import 'package:flutter_dmzj/models/novel/volume_detail_model.dart'; +import 'package:flutter_dmzj/requests/common/api.dart'; +import 'package:flutter_dmzj/requests/common/http_client.dart'; +import 'package:flutter_dmzj/services/local_storage_service.dart'; + +class NovelRequest { + /// 轻小说-推荐 + Future> recommend() async { + var list = []; + var result = + await HttpClient.instance.getJson('/novel/recommend', checkCode: true); + for (var item in result["recommendList"]) { + list.add(NovelRecommendModel.fromJson(item)); + } + return list; + } + + /// 轻小说-更新 + /// - [page] 页数从0开始 + Future> latest({int page = 1}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/novel/filter/list', + queryParameters: { + //status=0&sortType=1&page=1&size=20&tagId=0 + "status": 0, + "sortType": 1, + "page": page, + "size": 20, + }, + checkCode: true, + ); + for (var item in result["novelList"]) { + list.add(NovelLatestModel.fromJson(item)); + } + return list; + } + + /// 轻小说-分类 + Future> categores() async { + var list = []; + var result = await HttpClient.instance.getJson( + '/comic/filter/category', + queryParameters: { + "source": 2, + }, + checkCode: true, + ); + for (var item in result["cateList"]) { + list.add(NovelCategoryModel.fromJson(item)); + } + return list; + } + + /// 分类-筛选 + Future> categoryFilter() async { + var result = await HttpClient.instance.getJson( + '/comic/filter/category', + queryParameters: {"source": 2}, + checkCode: true, + ); + var list = []; + for (var item in result["cateList"]) { + list.add(NovelCategoryFilterItemModel.fromJson(item)); + } + return [ + NovelCategoryFilterModel(title: "题材", items: list), + ]; + } + + /// 分类下漫画 + /// - [cateId] 分类 + /// - [sort] 排序,0=人气,1=更新 + /// - [page] 页数,从0开始 + Future> categoryNovel({ + int cateId = 0, + int status = 0, + int sort = 0, + int page = 1, + }) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/novel/filter/list', + queryParameters: { + "tagId": cateId, + "status": 0, + "sortType": sort, + "page": page, + "size": 20, + }, + checkCode: true, + ); + for (var item in result["novelList"]) { + list.add(NovelCategoryNovelModel.fromJson(item)); + } + return list; + } + + /// 排行榜 + Future> rank({ + required int tagId, + required sort, + int page = 0, + }) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/novel/rank/$sort/$tagId/$page.json', + ); + for (var item in result) { + list.add(NovelRankModel.fromJson(item)); + } + return list; + } + + /// 排行榜-分类 + Future> rankFilter() async { + var result = await HttpClient.instance.getJson( + '/comic/filter/category', + queryParameters: { + "source": 2, + }, + checkCode: true, + ); + Map map = {}; + for (var item in result["cateList"]) { + map.addAll({ + item["tagId"]: item["title"], + }); + } + return map; + } + + /// 轻小说搜索 + /// - [page] 页数从0开始 + /// - [keyword] 关键字 + Future> search( + {required String keyword, int page = 1}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/search/index', + queryParameters: { + "keyword": keyword, + "page": page, + "size": 20, + "source": 1, + }, + checkCode: true, + ); + for (var item in result["list"]) { + list.add(NovelSearchModel.fromJson(item)); + } + return list; + } + + /// 小说搜索热词 + Future> searchHotWord() async { + var result = await HttpClient.instance.getJson( + '/search/hot/1.json', + ); + Map map = {}; + for (var item in result) { + map.addAll({ + item["id"]: item["name"], + }); + } + return map; + } + + /// 小说详情 + Future novelDetail({ + required int novelId, + }) async { + var result = await HttpClient.instance.getJson( + '/novel/detail/$novelId', + needLogin: true, + checkCode: true, + ); + var data = NovelDetailModel.fromJson(result); + + return data; + } + + /// 小说章节 + Future> novelChapter({ + required int novelId, + }) async { + var result = await HttpClient.instance.getJson( + '/novel/chapter/$novelId', + needLogin: true, + checkCode: true, + ); + var list = []; + for (var item in result["data"]) { + list.add(NovelVolumeDetailModel.fromJson(item)); + } + return list; + } + + /// 小说正文内容 + /// - [volumeId] 卷ID + /// - [chapterId] 章节ID + /// - [cancel] 取消Token + /// - [cache] 是否缓存 + Future novelContent({ + required int volumeId, + required int chapterId, + CancelToken? cancel, + bool cache = true, + }) async { + var localContent = await LocalStorageService.instance + .getNovelContent(volumeId: volumeId, chapterId: chapterId); + if (localContent != null) { + return localContent; + } + var result = await HttpClient.instance.getText( + Api.getNovelContentUrl(volumeId: volumeId, chapterId: chapterId), + baseUrl: "", + withDefaultParameter: false, + cancel: cancel, + ); + if (cache) { + await LocalStorageService.instance.saveNovelContent( + volumeId: volumeId, + chapterId: chapterId, + content: result, + ); + } + + return result; + } +} diff --git a/lib/requests/user_request.dart b/lib/requests/user_request.dart new file mode 100644 index 0000000..5d2f58a --- /dev/null +++ b/lib/requests/user_request.dart @@ -0,0 +1,349 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/app_error.dart'; +import 'package:flutter_dmzj/models/user/comic_history_model.dart'; +import 'package:flutter_dmzj/models/user/bind_status_model.dart'; +import 'package:flutter_dmzj/models/user/login_result_model.dart'; +import 'package:flutter_dmzj/models/user/novel_history_model.dart'; +import 'package:flutter_dmzj/models/user/subscribe_comic_model.dart'; +import 'package:flutter_dmzj/models/user/subscribe_news_model.dart'; +import 'package:flutter_dmzj/models/user/subscribe_novel_model.dart'; +import 'package:flutter_dmzj/models/user/user_profile_model.dart'; +import 'package:flutter_dmzj/requests/common/api.dart'; +import 'package:flutter_dmzj/requests/common/http_client.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_dmzj/services/user_service.dart'; + +class UserRequest { + /// 登录 + /// - [nickname] 用户名 + /// - [password] 密码 + Future login( + {required String nickname, required String password}) async { + var pwd = md5.convert(utf8.encode(password)).toString().toLowerCase(); + + Map data = { + "username": nickname, + "passwd": pwd, + }; + + var result = await HttpClient.instance.postJson( + "/login/passwd", + baseUrl: Api.BASE_URL_USER, + data: data, + formUrlEncoded: true, + checkCode: true, + ); + + return LoginResultModel.fromJson(result["user"]); + } + + /// 用户资料 + Future userProfile() async { + var result = await HttpClient.instance.getJson( + "/UCenter/comicsv2/${UserService.instance.userId}.json", + baseUrl: Api.BASE_URL, + queryParameters: { + "dmzj_token": UserService.instance.dmzjToken, + }, + withDefaultParameter: true, + ); + + return UserProfileModel.fromJson(result); + } + + /// 获取绑定手机、设置密码状态 + Future isBindTelPwd() async { + var result = await HttpClient.instance.getJson( + "/account/isbindtelpwd", + baseUrl: Api.BASE_URL, + queryParameters: { + "dmzj_token": UserService.instance.dmzjToken, + }, + withDefaultParameter: true, + checkCode: true, + ); + + return UserBindStatusModel.fromJson(result); + } + + /// 我的漫画订阅 + /// - [page] 页数从0开始 + /// - [subType] 全部=1,未读=2,已读=3,完结=4 + /// - [letter] all=全部 + Future> comicSubscribes( + {required int subType, int page = 1, String letter = ""}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/comic/sub/list', + queryParameters: { + //uid=$uid&sub_type=$subType&letter=$letter&dmzj_token=$token&page=$page&type=$type + "status": subType, + "firstLetter": letter, + "page": page, + "size": 20 + }, + needLogin: true, + checkCode: true, + ); + for (var item in result["subList"]) { + list.add(UserSubscribeComicItemModel.fromJson(item)); + } + return list; + } + + /// 我的小说订阅 + /// - [page] 页数从0开始 + /// - [subType] 全部=1,未读=2,已读=3,完结=4 + /// - [letter] all=全部 + Future> novelSubscribes( + {required int subType, int page = 0, String letter = "all"}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/novel/sub/list', + queryParameters: { + //uid=$uid&sub_type=$subType&letter=$letter&dmzj_token=$token&page=$page&type=$type + "status": subType, + "firstLetter": letter, + "page": page, + "size": 20 + }, + needLogin: true, + checkCode: true, + ); + for (var item in result["subList"]) { + list.add(UserSubscribeNovelModel.fromJson(item)); + } + return list; + } + + /// 我的新闻收藏 + /// - [page] 页数从0开始 + Future> newsSubscribes({int page = 1}) async { + var uid = UserService.instance.userId; + var par = {"uid": int.parse(uid), "page": page}; + var parJson = jsonEncode(par); + var sign = Api.sign(parJson, 'app_news_sub'); + + var result = await HttpClient.instance.postJson( + '/api/news/getSubscribe', + baseUrl: Api.BASE_URL_INTERFACE, + data: { + "parm": parJson, + "sign": sign, + }, + ); + var data = json.decode(result); + if (data["result"] != 1000) { + throw AppError(data["msg"]); + } + var list = []; + for (var item in data["data"]) { + list.add(UserSubscribeNewsModel.fromJson(item)); + } + return list; + } + + /// 添加订阅 + /// - [type] 类型,对应AppConstant + Future addSubscribe({required List ids, required int type}) async { + var requestUrl = "/comic/sub/add"; + var requestQuery = {}; + if (type == AppConstant.kTypeComic) { + requestUrl = "/comic/sub/add"; + requestQuery = { + "comic_id": ids.join(","), + }; + } else if (type == AppConstant.kTypeNovel) { + requestUrl = "/novel/sub/add"; + requestQuery = { + "novel_id": ids.join(","), + }; + } + + await HttpClient.instance.getJson( + requestUrl, + queryParameters: requestQuery, + needLogin: true, + checkCode: true, + ); + return true; + } + + /// 更新订阅的阅读状态 + /// - [type] 类型,对应AppConstant + Future subscribeRead({required int id, required int type}) async { + var typeStr = "mh"; + if (type == AppConstant.kTypeComic) { + typeStr = "mh"; + } else if (type == AppConstant.kTypeNovel) { + typeStr = "xs"; + } + + await HttpClient.instance.getJson( + '/subscribe/read', + queryParameters: { + "obj_id": id, + "type": typeStr, + }, + withDefaultParameter: true, + needLogin: true, + ); + return true; + } + + /// 取消订阅 + /// - [type] 类型,对应AppConstant + Future removeSubscribe( + {required List ids, required int type}) async { + var requestUrl = "/comic/sub/del"; + var requestQuery = {}; + if (type == AppConstant.kTypeComic) { + requestUrl = "/comic/sub/del"; + requestQuery = { + "comic_id": ids.join(","), + }; + } else if (type == AppConstant.kTypeNovel) { + requestUrl = "/novel/sub/del"; + requestQuery = { + "novel_id": ids.join(","), + }; + } + + await HttpClient.instance.getJson( + requestUrl, + queryParameters: requestQuery, + needLogin: true, + checkCode: true, + ); + return true; + } + + /// 查询订阅状态 + /// - [objId] 漫画ID或小说ID + /// - [type] 类型,对应AppConstant + Future checkSubscribeStatus( + {required int objId, required int type}) async { + var typeId = 1; + if (type == AppConstant.kTypeComic) { + typeId = 1; + } else if (type == AppConstant.kTypeNovel) { + typeId = 2; + } + + var result = await HttpClient.instance.getJson( + '/comic/sub/checkIsSub', + checkCode: true, + queryParameters: { + "objId": objId, + "source": typeId, + }, + needLogin: true, + ); + return result["isSub"]; + } + + /// 漫画阅读记录 + /// - [page] 页数从0开始,接口并没有分页 + Future> comicHistory({int page = 0}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/api/getReInfo/comic/${UserService.instance.userId}/$page', + queryParameters: {}, + baseUrl: Api.BASE_URL_INTERFACE, + ); + var data = json.decode(result); + for (var item in data) { + list.add(UserComicHistoryModel.fromJson(item)); + } + //远程与本地同步 + DBService.instance.syncRemoteComicHistory(list); + return list; + } + + /// 小说阅读记录 + /// - [page] 页数从0开始,接口并没有分页 + Future> novelHistory({int page = 0}) async { + var list = []; + var result = await HttpClient.instance.getJson( + '/api/getReInfo/novel/${UserService.instance.userId}/$page', + queryParameters: {}, + baseUrl: Api.BASE_URL_INTERFACE, + ); + var data = json.decode(result); + for (var item in data) { + list.add(UserNovelHistoryModel.fromJson(item)); + } + //远程与本地同步 + DBService.instance.syncRemoteNovelHistory(list); + return list; + } + + /// 上传漫画记录 + Future uploadComicHistory({ + required int comicId, + required int chapterId, + required int page, + required DateTime time, + }) async { + var data = { + comicId.toString(): chapterId.toString(), + "comicId": comicId.toString(), + "chapterId": chapterId.toString(), + "page": page, + "time": (time.millisecondsSinceEpoch ~/ 1000).toString() + }; + await HttpClient.instance.getJson( + "/api/record/getRe", + baseUrl: Api.BASE_URL_INTERFACE, + queryParameters: { + "st": "comic", + "uid": UserService.instance.userId, + "callback": "record_jsonpCallback", + "type": 3, + "json": "[${json.encode(data)}]", + }, + withDefaultParameter: true, + checkCode: true, + ); + + return true; + } + + /// 上传小说记录 + Future uploadNovelHistory({ + required int novelId, + required int chapterId, + required int volumeId, + required int page, + required int total, + required DateTime time, + }) async { + var data = { + novelId.toString(): chapterId.toString(), + "lnovel_id": novelId.toString(), + "volume_id": volumeId.toString(), + "chapterId": chapterId.toString(), + "total_num": total, + "page": page, + "time": (time.millisecondsSinceEpoch ~/ 1000).toString() + }; + await HttpClient.instance.getJson( + "/api/record/getRe", + baseUrl: Api.BASE_URL_INTERFACE, + queryParameters: { + "st": "novel", + "uid": UserService.instance.userId, + "callback": "record_jsonpCallback", + "type": 3, + "json": "[${json.encode(data)}]", + }, + withDefaultParameter: true, + checkCode: true, + ); + + return true; + } +} diff --git a/lib/routes/app_navigator.dart b/lib/routes/app_navigator.dart new file mode 100644 index 0000000..502bfac --- /dev/null +++ b/lib/routes/app_navigator.dart @@ -0,0 +1,282 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/models/comment/comment_item.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; +import 'package:flutter_dmzj/routes/route_path.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class AppNavigator { + /// 当前内容路由的名称 + static String currentContentRouteName = "/"; + + /// 子路由ID + static const int kSubNavigatorID = 1; + + /// 子路由Key + static final GlobalKey? subNavigatorKey = + Get.nestedKey(kSubNavigatorID); + + /// 子路由的Context + static BuildContext get subNavigatorContext => + subNavigatorKey!.currentContext!; + + static void toPage(String name, {dynamic arg}) { + Get.toNamed(name, arguments: arg); + } + + /// 跳转子路由页面 + static void toContentPage(String name, {dynamic arg, bool replace = false}) { + if (currentContentRouteName == name && replace) { + Get.offAndToNamed(name, arguments: arg, id: kSubNavigatorID); + } else { + Get.toNamed(name, arguments: arg, id: kSubNavigatorID); + } + } + + /// 关闭页面 + /// 优先关闭主路由的页面 + static void closePage() { + if (Navigator.canPop(Get.context!)) { + Get.back(); + } else { + Get.back(id: 1); + } + } + + /// 打开新闻详情 + static void toNewsDetail({ + required String url, + String title = "资讯详情", + int newsId = 0, + }) { + if (!url.startsWith("http:") && !url.startsWith("https:")) { + SmartDialog.showToast("无法打开此此链接:$url"); + return; + } + //https://news.dmzj.com/article/77288.html + if (url.contains("article/")) { + toContentPage(RoutePath.kNewsDetail, arg: { + "title": title, + "newsUrl": url, + "newsId": newsId, + }); + } else { + toWebView(url); + } + } + + /// 打开漫画详情 + static void toComicDetail(int id) { + toContentPage(RoutePath.kComicDetail, arg: id); + } + + /// 打开小说详情 + static void toNovelDetail(int id) { + Log.w("打开小说:$id"); + toContentPage(RoutePath.kNovelDetail, arg: id); + } + + /// 打开评论 + static void toComment({ + required int objId, + required int type, + }) { + toContentPage(RoutePath.kComment, arg: { + "objId": objId, + "type": type, + }); + } + + /// 打开WebView + static void toWebView(String url) { + url = url.trimRight().trimLeft(); + if (Platform.isAndroid || Platform.isIOS) { + toContentPage(RoutePath.kWebView, arg: url); + } else { + launchUrlString(url); + } + } + + /// 打开漫画分类详情 + static void toComicCategoryDetail(int id) { + toContentPage(RoutePath.kComicCategoryDetail, arg: id); + } + + /// 打开漫画作者详情 + static void toComicAuthorDetail(int id) { + toContentPage(RoutePath.kComicAuthorDetail, arg: id); + } + + /// 打开专题详情 + static void toSpecialDetail(int id) { + toContentPage(RoutePath.kSpecialDetail, arg: id); + } + + /// 打开漫画搜索 + static void toComicSearch({String keyword = ""}) { + toContentPage(RoutePath.kComicSearch, arg: keyword); + } + + /// 打开轻小说搜索 + static void toNovelSearch({String keyword = ""}) { + toContentPage(RoutePath.kNovelSearch, arg: keyword); + } + + /// 打开漫画分类详情 + static void toNovelCategoryDetail(int id) { + toContentPage(RoutePath.kNovelCategoryDetail, arg: id); + } + + /// 打开用户订阅 + /// - [type] 0=漫画,1=小说,2=新闻 + static void toUserSubscribe({int type = 0}) { + toContentPage(RoutePath.kUserSubscribe, arg: type); + } + + /// 打开用户历史记录 + /// - [type] 0=漫画,1=小说 + static void toUserHistory({int type = 0}) { + toContentPage(RoutePath.kUserHistory, arg: type); + } + + /// 打开本地历史记录 + /// - [type] 0=漫画,1=小说 + static void toLocalHistory({int type = 0}) { + toContentPage(RoutePath.kLocalHistory, arg: type); + } + + /// 打开本地历史记录 + /// - [type] 0=漫画,1=小说,2=下载 + static void toSettings({int type = 0}) { + toContentPage(RoutePath.kSettings, arg: type); + } + + /// 打开漫画阅读 + static Future toComicReader({ + required int comicId, + required String comicTitle, + required String comicCover, + required List chapters, + required ComicDetailChapterItem chapter, + required bool isLongComic, + }) async { + // 使用主路由跳转 + await Get.toNamed(RoutePath.kComicReader, arguments: { + "comicId": comicId, + "comicTitle": comicTitle, + "comicCover": comicCover, + "chapters": chapters, + "chapter": chapter, + "isLongComic": isLongComic, + }); + } + + /// 打开漫画阅读 + static Future toNovelReader({ + required int novelId, + required String novelTitle, + required String novelCover, + required List chapters, + required NovelDetailChapter chapter, + }) async { + // 使用主路由跳转 + await Get.toNamed(RoutePath.kNovelReader, arguments: { + "novelId": novelId, + "novelTitle": novelTitle, + "novelCover": novelCover, + "chapters": chapters, + "chapter": chapter, + }); + } + + /// 打开漫画下载-选择章节 + static void toComicDownloadSelect(int id) { + toContentPage(RoutePath.kComicDownloadSelect, arg: id); + } + + /// 打开小说下载-选择章节 + static void toNovelDownloadSelect(int id) { + toContentPage(RoutePath.kNovelDownloadSelect, arg: id); + } + + /// 打开漫画下载管理 + /// * [type] 0=下载完成,1=下载中 + static void toComicDownloadManage(int type) { + toContentPage(RoutePath.kComicDownload, arg: type); + } + + /// 打开已下载的漫画 + /// * [info] 已下载的漫画信息 + static void toComicDownloadDetail(ComicDownloadedItem info) { + toContentPage(RoutePath.kComicDownloadDetail, arg: info); + } + + /// 打开小说下载管理 + /// * [type] 0=下载完成,1=下载中 + static void toNovelDownloadManage(int type) { + toContentPage(RoutePath.kNovelDownload, arg: type); + } + + /// 打开已下载的小说 + /// * [info] 已下载的漫画信息 + static void toNovelDownloadDetail(NovelDownloadedItem info) { + toContentPage(RoutePath.kNovelDownloadDetail, arg: info); + } + + /// 打开添加/回复评论 + static void toAddComment({ + required int objId, + required int type, + CommentItem? replyItem, + }) { + toContentPage(RoutePath.kCommentAdd, arg: { + "objId": objId, + "type": type, + "replyItem": replyItem, + }); + } + + /// 打开用户的评论 + /// * [userId] 用户ID + static void toUserComment(int userId) { + toContentPage(RoutePath.kUserComment, arg: userId); + } + + /// 打开用户中心 + /// * [userId] 用户ID + static void toUserCenter(int userId) { + //TODO 跳转至用户中心 + toUserComment(userId); + } + + /// 打开本机收藏 + static void tolocalFavorite() { + toContentPage(RoutePath.kLocalFavorite); + } + + static void showBottomSheet( + Widget widget, { + bool isScrollControlled = false, + }) { + showModalBottomSheet( + context: subNavigatorContext, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + ), + isScrollControlled: isScrollControlled, + backgroundColor: Get.theme.cardColor, + builder: (context) => widget, + routeSettings: const RouteSettings(name: "/modalBottomSheet"), + ); + } +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart new file mode 100644 index 0000000..6a3569d --- /dev/null +++ b/lib/routes/app_pages.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/modules/comic/author_detail/author_detail_page.dart'; +import 'package:flutter_dmzj/modules/comic/category_detail/category_detail_page.dart'; +import 'package:flutter_dmzj/modules/comic/detail/comic_detail_page.dart'; +import 'package:flutter_dmzj/modules/comic/home/comic_home_controller.dart'; +import 'package:flutter_dmzj/modules/comic/reader/comic_reader_controller.dart'; +import 'package:flutter_dmzj/modules/comic/reader/comic_reader_page.dart'; +import 'package:flutter_dmzj/modules/comic/search/comic_search_page.dart'; +import 'package:flutter_dmzj/modules/comic/select_chapter/comic_select_chapter_page.dart'; +import 'package:flutter_dmzj/modules/comic/special_detail/special_detail_page.dart'; +import 'package:flutter_dmzj/modules/common/comment/add_comment_page.dart'; +import 'package:flutter_dmzj/modules/common/comment/comment_page.dart'; +import 'package:flutter_dmzj/modules/common/download/comic/comic_download_page.dart'; +import 'package:flutter_dmzj/modules/common/download/comic/comic_downloaded_detail_page.dart'; +import 'package:flutter_dmzj/modules/common/download/novel/novel_download_page.dart'; +import 'package:flutter_dmzj/modules/common/download/novel/novel_downloaded_detail_page.dart'; +import 'package:flutter_dmzj/modules/common/empty_page.dart'; +import 'package:flutter_dmzj/modules/common/test_subroute_page.dart'; +import 'package:flutter_dmzj/modules/common/webview/webview_page.dart'; +import 'package:flutter_dmzj/modules/index/index_controller.dart'; +import 'package:flutter_dmzj/modules/index/index_page.dart'; +import 'package:flutter_dmzj/modules/news/detail/news_detail_page.dart'; +import 'package:flutter_dmzj/modules/novel/category_detail/novel_category_detail_page.dart'; +import 'package:flutter_dmzj/modules/novel/detail/novel_detail_page.dart'; +import 'package:flutter_dmzj/modules/novel/reader/novel_reader_controller.dart'; +import 'package:flutter_dmzj/modules/novel/reader/novel_reader_page.dart'; +import 'package:flutter_dmzj/modules/novel/search/novel_search_page.dart'; +import 'package:flutter_dmzj/modules/novel/select_chapter/novel_select_chapter_page.dart'; +import 'package:flutter_dmzj/modules/user/comment/user_comment_page.dart'; +import 'package:flutter_dmzj/modules/user/history/user_history_page.dart'; +import 'package:flutter_dmzj/modules/user/local_favorite/local_favorite_page.dart'; +import 'package:flutter_dmzj/modules/user/local_history/local_history_page.dart'; +import 'package:flutter_dmzj/modules/user/settings/settings_page.dart'; +import 'package:flutter_dmzj/modules/user/subscribe/user_subscribe_pgae.dart'; +import 'package:flutter_dmzj/modules/user/user_home_controller.dart'; +import 'package:flutter_dmzj/routes/route_path.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:get/get.dart'; + +class AppPages { + AppPages._(); + static const kIndex = RoutePath.kIndex; + static final routes = [ + GetPage( + name: RoutePath.kIndex, + page: () => const IndexPage(), + bindings: [ + BindingsBuilder.put( + () => IndexController(), + ), + BindingsBuilder.put( + () => ComicHomeController(), + ), + BindingsBuilder.put( + () => UserHomeController(), + ), + ], + ), + GetPage( + name: RoutePath.kComicReader, + page: () => const ComicReaderPage(), + binding: BindingsBuilder.put( + () => ComicReaderController( + comicId: Get.arguments["comicId"], + comicTitle: Get.arguments["comicTitle"], + comicCover: Get.arguments["comicCover"], + chapters: Get.arguments["chapters"], + chapter: Get.arguments["chapter"], + isLongComic: Get.arguments["isLongComic"] ?? false, + ), + ), + ), + GetPage( + name: RoutePath.kNovelReader, + page: () => const NovelReaderPage(), + binding: BindingsBuilder.put( + () => NovelReaderController( + novelId: Get.arguments["novelId"], + novelTitle: Get.arguments["novelTitle"], + novelCover: Get.arguments["novelCover"], + chapters: Get.arguments["chapters"], + chapter: Get.arguments["chapter"], + ), + ), + ), + ]; + + /// 定义子路由 + static Route? generateSubRoute(RouteSettings settings) { + switch (settings.name) { + case "/": + return GetPageRoute( + settings: settings, + page: () => const EmptyPage(), + ); + case RoutePath.kTestSubRoute: + return GetPageRoute( + settings: settings, + transition: Transition.native, + page: () => const TestSubRoutePage(), + ); + case RoutePath.kNewsDetail: + var parameter = settings.arguments as Map; + return GetPageRoute( + settings: settings, + page: () => NewsDetailPage( + title: parameter["title"], + newsUrl: parameter["newsUrl"], + newsId: parameter["newsId"], + ), + ); + case RoutePath.kComment: + var parameter = settings.arguments as Map; + return GetPageRoute( + settings: settings, + page: () => CommentPage( + objId: parameter["objId"], + type: parameter["type"], + ), + ); + case RoutePath.kCommentAdd: + var parameter = settings.arguments as Map; + return GetPageRoute( + settings: settings, + page: () => AddCommentPage( + objId: parameter["objId"], + type: parameter["type"], + replyItem: parameter["replyItem"], + ), + ); + case RoutePath.kWebView: + return GetPageRoute( + settings: settings, + page: () => WebViewPage( + url: settings.arguments.toString(), + ), + ); + case RoutePath.kComicCategoryDetail: + return GetPageRoute( + settings: settings, + page: () => CategoryDetailPage( + settings.arguments as int, + ), + ); + case RoutePath.kSpecialDetail: + return GetPageRoute( + settings: settings, + page: () => SpecialDetailPage( + settings.arguments as int, + ), + ); + case RoutePath.kComicAuthorDetail: + return GetPageRoute( + settings: settings, + page: () => ComicAuthorDetailPage( + settings.arguments as int, + ), + ); + case RoutePath.kComicDetail: + return GetPageRoute( + settings: settings, + page: () => ComicDetailPage( + settings.arguments as int, + ), + ); + case RoutePath.kComicSearch: + return GetPageRoute( + settings: settings, + page: () => ComicSearchPage( + keyword: settings.arguments.toString(), + ), + ); + case RoutePath.kNovelSearch: + return GetPageRoute( + settings: settings, + page: () => NovelSearchPage( + keyword: settings.arguments.toString(), + ), + ); + case RoutePath.kNovelCategoryDetail: + return GetPageRoute( + settings: settings, + page: () => NovelCategoryDetailPage( + settings.arguments as int, + ), + ); + case RoutePath.kUserSubscribe: + return GetPageRoute( + settings: settings, + page: () => UserSubscribePage( + type: settings.arguments as int, + ), + ); + case RoutePath.kUserHistory: + return GetPageRoute( + settings: settings, + page: () => UserHistoryPage( + type: settings.arguments as int, + ), + ); + case RoutePath.kLocalHistory: + return GetPageRoute( + settings: settings, + page: () => LocalHistoryPage( + type: settings.arguments as int, + ), + ); + case RoutePath.kSettings: + return GetPageRoute( + settings: settings, + page: () => SettingsPage( + index: settings.arguments as int, + ), + ); + case RoutePath.kNovelDetail: + return GetPageRoute( + settings: settings, + page: () => NovelDetailPage( + settings.arguments as int, + ), + ); + case RoutePath.kComicDownloadSelect: + return GetPageRoute( + settings: settings, + page: () => ComicSelectChapterPage( + settings.arguments as int, + ), + ); + case RoutePath.kComicDownload: + return GetPageRoute( + settings: settings, + page: () => ComicDownloadPage( + settings.arguments as int, + ), + ); + case RoutePath.kComicDownloadDetail: + return GetPageRoute( + settings: settings, + page: () => ComicDownloadedDetailPage( + settings.arguments as ComicDownloadedItem, + ), + ); + case RoutePath.kNovelDownloadSelect: + return GetPageRoute( + settings: settings, + page: () => NovelSelectChapterPage( + settings.arguments as int, + ), + ); + case RoutePath.kNovelDownload: + return GetPageRoute( + settings: settings, + page: () => NovelDownloadPage( + settings.arguments as int, + ), + ); + case RoutePath.kNovelDownloadDetail: + return GetPageRoute( + settings: settings, + page: () => NovelDownloadedDetailPage( + settings.arguments as NovelDownloadedItem, + ), + ); + case RoutePath.kUserComment: + return GetPageRoute( + settings: settings, + page: () => UserCommentPage( + settings.arguments as int, + ), + ); + case RoutePath.kLocalFavorite: + return GetPageRoute( + settings: settings, + page: () => LocalFavoritePage(), + ); + default: + return GetPageRoute(page: () => const EmptyPage()); + } + } +} diff --git a/lib/routes/route_path.dart b/lib/routes/route_path.dart new file mode 100644 index 0000000..4c2c0a2 --- /dev/null +++ b/lib/routes/route_path.dart @@ -0,0 +1,90 @@ +class RoutePath { + /// 首页 + static const kIndex = "/index"; + + /// 空白 + static const kEmpty = "/empty"; + + /// 登录 + static const kUserLogin = "/user/login"; + + static const kTestSubRoute = "/test/sub_route"; + + /// WebView + static const kWebView = "/other/webview"; + + /// 新闻详情 + static const kNewsDetail = "/news/detail"; + + /// 评论 + static const kComment = "/comment"; + + /// 漫画详情 + static const kComicDetail = "/comic/detail"; + + /// 漫画阅读 + static const kComicReader = "/comic/reader"; + + /// 漫画分类详情 + static const kComicCategoryDetail = "/comic/category/detail"; + + /// 专题详情 + static const kSpecialDetail = "/comic/special/detail"; + + /// 漫画作者详情 + static const kComicAuthorDetail = "/comic/author/detail"; + + /// 漫画搜索 + static const kComicSearch = "/comic/search"; + + /// 轻小说搜索 + static const kNovelSearch = "/novel/search"; + + /// 轻小说分类详情 + static const kNovelCategoryDetail = "/novel/category/detail"; + + /// 用户订阅 + static const kUserSubscribe = "/user/subscribe"; + + /// 用户观看记录 + static const kUserHistory = "/user/history"; + + /// 本机观看记录 + static const kLocalHistory = "/user/local/history"; + + /// 设置 + static const kSettings = "/user/settings"; + + /// 小说详情 + static const kNovelDetail = "/novel/detail"; + + /// 小说阅读 + static const kNovelReader = "/novel/reader"; + + /// 漫画下载,选择章节 + static const kComicDownloadSelect = "/comic/download/chapter"; + + /// 小说下载,选择章节 + static const kNovelDownloadSelect = "/novel/download/chapter"; + + /// 漫画下载管理 + static const kComicDownload = "/download/comic"; + + /// 漫画下载详情 + static const kComicDownloadDetail = "/download/comic/detail"; + + /// 小说下载管理 + static const kNovelDownload = "/download/novel"; + + /// 小说下载详情 + static const kNovelDownloadDetail = "/download/novel/chapter"; + + /// 添加/回复评论 + static const kCommentAdd = "/comment/add"; + + /// 用户的评论 + static const kUserComment = "/user/comment"; + + /// 本机收藏 + static const kLocalFavorite = "/user/local/favorite"; +} diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart new file mode 100644 index 0000000..b7be4cb --- /dev/null +++ b/lib/services/app_settings_service.dart @@ -0,0 +1,370 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/services/local_storage_service.dart'; +import 'package:get/get.dart'; + +class AppSettingsService extends GetxController { + static AppSettingsService get instance => Get.find(); + var themeMode = 0.obs; + var firstRun = false; + @override + void onInit() { + themeMode.value = LocalStorageService.instance + .getValue(LocalStorageService.kThemeMode, 0); + firstRun = LocalStorageService.instance + .getValue(LocalStorageService.kFirstRun, true); + //漫画 + comicReaderDirection.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderDirection, 0); + comicReaderFullScreen.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderFullScreen, true); + comicReaderShowStatus.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderShowStatus, true); + comicReaderShowStatus.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderShowStatus, true); + comicReaderShowViewPoint.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderShowViewPoint, true); + comicReaderLeftHandMode.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderLeftHandMode, false); + comicReaderHD.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderHD, false); + comicReaderPageAnimation.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderPageAnimation, true); + comicReaderOldViewPoint.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicReaderOldViewPoint, false); + //小说 + novelReaderDirection.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderDirection, 0); + novelReaderFontSize.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderFontSize, 16); + novelReaderLineSpacing.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderLineSpacing, 1.5); + novelReaderTheme.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderTheme, 0); + novelReaderFullScreen.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderFullScreen, true); + novelReaderShowStatus.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderShowStatus, true); + novelReaderLeftHandMode.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderLeftHandMode, false); + novelReaderPageAnimation.value = LocalStorageService.instance + .getValue(LocalStorageService.kNovelReaderPageAnimation, true); + //下载 + downloadAllowCellular.value = LocalStorageService.instance + .getValue(LocalStorageService.kDownloadAllowCellular, true); + downloadComicTaskCount.value = LocalStorageService.instance + .getValue(LocalStorageService.kDownloadComicTaskCount, 5); + downloadNovelTaskCount.value = LocalStorageService.instance + .getValue(LocalStorageService.kDownloadNovelTaskCount, 5); + //搜索API + comicSearchUseWebApi.value = LocalStorageService.instance + .getValue(LocalStorageService.kComicSearchUseWebApi, false); + //字体大小 + useSystemFontSize.value = LocalStorageService.instance + .getValue(LocalStorageService.kUseSystemFontSize, false); + //新闻字体 + newsFontSize.value = LocalStorageService.instance + .getValue(LocalStorageService.kNewsFontSize, 15); + //自动添加神隐漫画至收藏夹 + collectHideComic.value = LocalStorageService.instance + .getValue(LocalStorageService.kCollectHideComic, false); + //代理地址 + proxyAddress.value = LocalStorageService.instance + .getValue(LocalStorageService.kProxyAddress, ""); + //MD动态取色 + useDynamicColor.value = LocalStorageService.instance + .getValue(LocalStorageService.kUseDynamicColor, true); + super.onInit(); + } + + void changeTheme() { + Get.dialog( + SimpleDialog( + title: const Text("设置主题"), + children: [ + RadioListTile( + title: const Text("跟随系统"), + value: 0, + groupValue: themeMode.value, + onChanged: (e) { + Get.back(); + setTheme(e ?? 0); + }, + ), + RadioListTile( + title: const Text("浅色模式"), + value: 1, + groupValue: themeMode.value, + onChanged: (e) { + Get.back(); + setTheme(e ?? 1); + }, + ), + RadioListTile( + title: const Text("深色模式"), + value: 2, + groupValue: themeMode.value, + onChanged: (e) { + Get.back(); + setTheme(e ?? 2); + }, + ), + ], + ), + ); + } + + void setTheme(int i) { + themeMode.value = i; + var mode = ThemeMode.values[i]; + + LocalStorageService.instance.setValue(LocalStorageService.kThemeMode, i); + Get.changeThemeMode(mode); + } + + /// 漫画阅读方向 + /// * [0] 左右 + /// * [1] 上下 + /// * [2] 右左 + var comicReaderDirection = 0.obs; + void setComicReaderDirection(int direction) { + if (comicReaderDirection.value == direction) { + return; + } + comicReaderDirection.value = direction; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderDirection, direction); + } + + /// 漫画全屏阅读 + RxBool comicReaderFullScreen = true.obs; + void setComicReaderFullScreen(bool value) { + comicReaderFullScreen.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderFullScreen, value); + } + + /// 漫画阅读显示状态信息 + RxBool comicReaderShowStatus = true.obs; + void setComicReaderShowStatus(bool value) { + comicReaderShowStatus.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderShowStatus, value); + } + + /// 漫画阅读尾页显示观点/吐槽 + RxBool comicReaderShowViewPoint = true.obs; + void setComicReaderShowViewPoint(bool value) { + comicReaderShowViewPoint.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderShowViewPoint, value); + } + + /// 启用旧板吐槽 + RxBool comicReaderOldViewPoint = false.obs; + void setComicReaderOldViewPoint(bool value) { + comicReaderOldViewPoint.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderOldViewPoint, value); + } + + /// 小说阅读方向 + /// * [0] 左右 + /// * [1] 上下 + /// * [2] 右左 + var novelReaderDirection = 0.obs; + void setNovelReaderDirection(int direction) { + if (novelReaderDirection.value == direction) { + return; + } + novelReaderDirection.value = direction; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderDirection, direction); + } + + /// 小说字体 + var novelReaderFontSize = 16.obs; + void setNovelReaderFontSize(int size) { + if (size < 5) { + size = 5; + } + //应该没人需要这么大的字体吧... + if (size > 56) { + size = 56; + } + novelReaderFontSize.value = size; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderFontSize, size); + } + + /// 小说行距 + var novelReaderLineSpacing = 1.5.obs; + void setNovelReaderLineSpacing(double spacing) { + if (spacing < 1) { + spacing = 1; + } + //应该没人需要这么大的字体吧... + if (spacing > 5) { + spacing = 5; + } + novelReaderLineSpacing.value = spacing; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderLineSpacing, spacing); + } + + /// 小说阅读主题 + var novelReaderTheme = 0.obs; + void setNovelReaderTheme(int theme) { + novelReaderTheme.value = theme; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderTheme, theme); + } + + /// 漫画全屏阅读 + RxBool novelReaderFullScreen = true.obs; + void setNovelReaderFullScreen(bool value) { + novelReaderFullScreen.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderFullScreen, value); + } + + /// 漫画阅读显示状态信息 + RxBool novelReaderShowStatus = true.obs; + void setNovelReaderShowStatus(bool value) { + novelReaderShowStatus.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderShowStatus, value); + } + + /// 下载是否允许使用流量 + RxBool downloadAllowCellular = true.obs; + void setDownloadAllowCellular(bool value) { + downloadAllowCellular.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kDownloadAllowCellular, value); + } + + /// 下载漫画最大任务数 + var downloadComicTaskCount = 5.obs; + void setDownloadComicTaskCount(int task) { + downloadComicTaskCount.value = task; + LocalStorageService.instance + .setValue(LocalStorageService.kDownloadComicTaskCount, task); + } + + /// 下载漫画最大任务数 + var downloadNovelTaskCount = 5.obs; + void setDownloadNovelTaskCount(int task) { + downloadNovelTaskCount.value = task; + LocalStorageService.instance + .setValue(LocalStorageService.kDownloadNovelTaskCount, task); + } + + /// 漫画搜索使用Web接口 + var comicSearchUseWebApi = false.obs; + void setComicSearchUseWebApi(bool e) { + comicSearchUseWebApi.value = e; + LocalStorageService.instance + .setValue(LocalStorageService.kComicSearchUseWebApi, e); + } + + /// 显示字体大小跟随系统 + var useSystemFontSize = false.obs; + void setUseSystemFontSize(bool e) { + useSystemFontSize.value = e; + LocalStorageService.instance + .setValue(LocalStorageService.kUseSystemFontSize, e); + } + + /// 漫画阅读左手模式 + RxBool comicReaderLeftHandMode = false.obs; + void setComicReaderLeftHandMode(bool value) { + comicReaderLeftHandMode.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderLeftHandMode, value); + } + + /// 小说阅读左手模式 + RxBool novelReaderLeftHandMode = false.obs; + void setNovelReaderLeftHandMode(bool value) { + novelReaderLeftHandMode.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderLeftHandMode, value); + } + + /// 漫画阅读优先加载高清图 + RxBool comicReaderHD = false.obs; + void setComicReaderHD(bool value) { + comicReaderHD.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderHD, value); + } + + /// 漫画阅读翻页动画 + RxBool comicReaderPageAnimation = true.obs; + void setComicReaderPageAnimation(bool value) { + comicReaderPageAnimation.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kComicReaderPageAnimation, value); + } + + /// 小说阅读翻页动画 + RxBool novelReaderPageAnimation = true.obs; + void setNovelReaderPageAnimation(bool value) { + novelReaderPageAnimation.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kNovelReaderPageAnimation, value); + } + + /// 下载漫画最大任务数 + var newsFontSize = 15.obs; + void setNewsFontSize(int size) { + newsFontSize.value = size; + LocalStorageService.instance + .setValue(LocalStorageService.kNewsFontSize, size); + } + + /// 自动添加神隐漫画至收藏夹 + RxBool collectHideComic = false.obs; + void setCollectHideComic(bool value) { + collectHideComic.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kCollectHideComic, value); + } + + /// 代理地址 + var proxyAddress = "".obs; + void setProxyAddress(String address) { + proxyAddress.value = address; + LocalStorageService.instance + .setValue(LocalStorageService.kProxyAddress, address); + } + + void setNoFirstRun() { + LocalStorageService.instance.setValue(LocalStorageService.kFirstRun, false); + } + + // 动态颜色方案缓存(由DynamicColorBuilder提供) + ColorScheme? _lightDynamic; + ColorScheme? _darkDynamic; + + void storeDynamicColorSchemes(ColorScheme? light, ColorScheme? dark) { + _lightDynamic = light; + _darkDynamic = dark; + } + + /// 是否使用MD动态取色 + RxBool useDynamicColor = true.obs; + void setUseDynamicColor(bool value) { + useDynamicColor.value = value; + LocalStorageService.instance + .setValue(LocalStorageService.kUseDynamicColor, value); + final lightScheme = value ? _lightDynamic : null; + final darkScheme = value ? _darkDynamic : null; + // 同时更新亮色和暗色主题 + Get.rootController.theme = AppStyle.getLightTheme(colorScheme: lightScheme); + Get.rootController.darkTheme = + AppStyle.getDarkTheme(colorScheme: darkScheme); + Get.rootController.update(); + } +} diff --git a/lib/services/comic_download_service.dart b/lib/services/comic_download_service.dart new file mode 100644 index 0000000..aee1ddd --- /dev/null +++ b/lib/services/comic_download_service.dart @@ -0,0 +1,407 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; + +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/comic/detail_info.dart'; +import 'package:flutter_dmzj/models/db/comic_download_info.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; + +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/services/download_task/comic_downloader.dart'; +import 'package:get/get.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path_provider/path_provider.dart'; +// ignore: depend_on_referenced_packages +import 'package:collection/collection.dart'; +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; + +/// 漫画下载管理 +// TODO 整理代码 +class ComicDownloadService extends GetxService { + static ComicDownloadService get instance => Get.find(); + + AppSettingsService settings = AppSettingsService.instance; + + late Box box; + String savePath = ""; + + /// 连接信息监听 + StreamSubscription? connectivitySubscription; + + /// 当前连接类型 + ConnectivityResult? connectivityType; + + /// 当前正在下载的数量 + var currentNum = 0; + + Future init() async { + var dir = await getApplicationSupportDirectory(); + box = await Hive.openBox( + "ZaiComicDownload", + path: dir.path, + ); + savePath = await getSavePath(); + //监听网络状态 + initConnectivity(); + //更新ID + updateAllIds(); + + updateDownlaoded(); + } + + /// 初始化连接状态 + void initConnectivity() async { + try { + var connectivity = Connectivity(); + connectivitySubscription = connectivity.onConnectivityChanged + .listen((ConnectivityResult result) { + networkChanged(result); + }); + connectivityType = await connectivity.checkConnectivity(); + initTasks(); + } catch (e) { + Log.logPrint(e); + initTasks(); + } + } + + /// 网络变更 + void networkChanged(ConnectivityResult type) { + if (connectivityType != type && type == ConnectivityResult.mobile) { + //切换至流量 + switchCellular(); + } else if (connectivityType != type && type == ConnectivityResult.none) { + //网络断开 + switchNoNetwork(); + } else { + switchToWiFi(); + } + connectivityType = type; + } + + /// 切换至流量 + void switchCellular() { + if (settings.downloadAllowCellular.value) { + //允许使用流量,当成WiFi处理 + switchToWiFi(); + return; + } + //把任务状态改为pauseCellular + for (var item in taskQueues) { + if (item.status == DownloadStatus.wait || + item.status == DownloadStatus.loadding || + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.waitNetwork) { + item.stopTask(); + item.updateStatus(DownloadStatus.pauseCellular, updateTask: false); + } + } + updateQueue(); + } + + /// 无网络 + void switchNoNetwork() { + //把任务状态改为pauseCellular + for (var item in taskQueues) { + if (item.status == DownloadStatus.wait || + item.status == DownloadStatus.loadding || + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.pauseCellular) { + item.stopTask(); + item.updateStatus(DownloadStatus.waitNetwork, updateTask: false); + } + } + updateQueue(); + } + + void switchToWiFi() { + for (var item in taskQueues) { + if (item.status == DownloadStatus.pauseCellular || + item.status == DownloadStatus.waitNetwork) { + item.updateStatus(DownloadStatus.wait, updateTask: false); + } + } + updateQueue(); + } + + /// 任务列表 + RxList taskQueues = RxList(); + + /// 已下载完成的 + RxList downloaded = RxList(); + + /// 已下载、下载中的ID + RxSet downloadIds = RxSet(); + + /// 开始下载任务 + void initTasks() async { + var tasks = getDownloadingTask(); + for (var item in tasks) { + //任务已被取消 + if (item.status == DownloadStatus.cancel) { + box.delete(item.taskId); + continue; + } + //无网络 + if (connectivityType == ConnectivityResult.none) { + if (item.status != DownloadStatus.pause) { + item.status = DownloadStatus.waitNetwork; + } + } else if (connectivityType == ConnectivityResult.mobile) { + //不允许使用数据下载 + if (!settings.downloadAllowCellular.value) { + if (item.status != DownloadStatus.pause) { + item.status = DownloadStatus.pauseCellular; + } + } + } else { + //只要不是手动暂停的,全部改为等待,添加到下载队列 + if (item.status != DownloadStatus.pause) { + item.status = DownloadStatus.wait; + } + } + + taskQueues.add( + ComicDownloader(item, onUpdateTask: onUpdateTask), + ); + } + updateQueue(); + } + + /// 更新队列 + void updateQueue() { + //如果下载中任务数小于设定值,添加一个任务 + //如果任务取消或完成,移除队列 + for (var task in List.from(taskQueues)) { + //下载完成或取消,移除队列 + if (task.status == DownloadStatus.complete || + task.status == DownloadStatus.cancel) { + taskQueues.remove(task); + updateDownlaoded(); + continue; + } + } + var taskNum = settings.downloadComicTaskCount.value; + var count = taskQueues + .where((x) => + x.status == DownloadStatus.downloading || + x.status == DownloadStatus.loadding) + .length; + + currentNum = count; + if (taskNum == 0) { + var ls = taskQueues.where((x) => x.status == DownloadStatus.wait); + for (var item in ls) { + item.start(); + } + } else { + if (count < taskNum) { + var ls = taskQueues + .where((x) => x.status == DownloadStatus.wait) + .take(taskNum - count); + for (var item in ls) { + item.start(); + } + } + } + updateAllIds(); + } + + void updateAllIds() { + downloadIds.clear(); + downloadIds.addAll(box.keys.map((e) => e.toString())); + } + + ///读取未完成的任务 + List getDownloadingTask() { + return box.values + .toList() + .where((x) => x.status != DownloadStatus.complete) + .toList(); + } + + /// 更新下载完成 + void updateDownlaoded() { + var downlaodedList = box.values + .toList() + .where((x) => x.status == DownloadStatus.complete) + .toList(); + var comicMap = groupBy(downlaodedList, (ComicDownloadInfo x) => x.comicId); + List comicList = []; + for (var comicId in comicMap.keys) { + var items = comicMap[comicId]!; + var comicName = items.first.comicName; + var comicCover = items.first.comicCover; + var isLongComic = items.first.isLongComic; + List volumes = []; + var volumeMap = groupBy(items, (ComicDownloadInfo x) => x.volumeName); + for (var volumeName in volumeMap.keys) { + var chapters = volumeMap[volumeName]! + .map( + (e) => ComicDetailChapterItem( + chapterId: e.chapterId, + chapterTitle: e.chapterName, + updateTime: 0, + fileSize: 0, + chapterOrder: e.chapterSort, + ), + ) + .toList(); + volumes.add( + ComicDetailVolume( + title: volumeName, + chapters: RxList(chapters), + ), + ); + } + for (var item in volumes) { + item.sortType.value = 1; + item.sort(); + } + comicList.add( + ComicDownloadedItem( + comicName: comicName, + comicCover: comicCover, + comicId: comicId, + chapterCount: items.length, + volumes: volumes, + isLongComic: isLongComic, + ), + ); + } + downloaded.value = comicList; + } + + /// 继续 + void resumeAll() { + //更新状态至等待 + for (var task in taskQueues) { + if (task.status == DownloadStatus.pause) { + task.stopTask(); + task.updateStatus(DownloadStatus.wait, updateTask: false); + } + } + updateQueue(); + } + + /// 暂停 + void pauseAll() { + for (var task in taskQueues) { + if (task.status != DownloadStatus.pause && + task.status != DownloadStatus.error && + task.status != DownloadStatus.errorLoad) { + task.stopTask(); + task.updateStatus(DownloadStatus.pause, updateTask: false); + } + } + updateQueue(); + } + + /// 取消任务 + void cancelTask(ComicDownloader task) { + // 移除列表 + // 移除数据库 + // 取消任务 + // 删除文件 + } + + /// 添加一个任务 + void addTask({ + required int comicId, + required int chapterId, + required String chapterName, + required int chapterSort, + required String volumeName, + required String comicTitle, + required String comicCover, + required bool isVip, + required bool isLongComic, + }) async { + var taskId = "${comicId}_$chapterId"; + if (box.containsKey(taskId)) { + return; + } + var info = ComicDownloadInfo( + addTime: DateTime.now(), + chapterId: chapterId, + chapterSort: chapterSort, + comicCover: comicCover, + comicId: comicId, + comicName: comicTitle, + files: [], + index: 0, + savePath: p.join(savePath, taskId), + status: DownloadStatus.wait, + taskId: taskId, + total: 0, + volumeName: volumeName, + chapterName: chapterName, + urls: [], + isVip: isVip, + isLongComic: isLongComic, + ); + await box.put( + taskId, + info, + ); + taskQueues.add(ComicDownloader(info, onUpdateTask: onUpdateTask)); + updateQueue(); + } + + void onUpdateTask() { + updateQueue(); + } + + /// 读取保存目录 + Future getSavePath() async { + var dir = await getApplicationSupportDirectory(); + + var comicDir = Directory(p.join(dir.path, "comic")); + if (!await comicDir.exists()) { + comicDir = await comicDir.create(recursive: true); + } + return comicDir.path; + } + + ///删除 + void delete(ComicDownloadInfo info) async { + try { + var dir = Directory(p.join(savePath, info.taskId)); + await dir.delete(recursive: true); + } catch (e) { + Log.logPrint(e); + } finally { + await box.delete(info.taskId); + updateDownlaoded(); + } + updateAllIds(); + } + + ///删除 + void deleteChapter(int comicId, int chapterId) async { + var info = box.get("${comicId}_$chapterId"); + if (info != null) { + delete(info); + } + } +} + +class ComicDownloadedItem { + final String comicName; + final int comicId; + final String comicCover; + final List volumes; + final int chapterCount; + final bool isLongComic; + ComicDownloadedItem({ + required this.comicName, + required this.comicCover, + required this.comicId, + required this.chapterCount, + required this.volumes, + required this.isLongComic, + }); +} diff --git a/lib/services/db_service.dart b/lib/services/db_service.dart new file mode 100644 index 0000000..88f0e56 --- /dev/null +++ b/lib/services/db_service.dart @@ -0,0 +1,211 @@ +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/db/comic_history.dart'; +import 'package:flutter_dmzj/models/db/local_favorite.dart'; +import 'package:flutter_dmzj/models/db/novel_history.dart'; +import 'package:flutter_dmzj/models/user/comic_history_model.dart'; +import 'package:flutter_dmzj/models/user/novel_history_model.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; + +class DBService extends GetxService { + static DBService get instance => Get.find(); + late Box newsLikeBox; + late Box comicHistoryBox; + late Box novelHistoryBox; + late Box localFavoriteBox; + Future init() async { + if (kIsWeb) { + newsLikeBox = await Hive.openBox("ZaiNewsLike"); + comicHistoryBox = await Hive.openBox("ZaiComicHistory"); + novelHistoryBox = await Hive.openBox("ZaiNovelHistory"); + localFavoriteBox = await Hive.openBox("ZaiLocalFavorite"); + } else { + var dir = await getApplicationSupportDirectory(); + newsLikeBox = await Hive.openBox("ZaiNewsLike", path: dir.path); + comicHistoryBox = await Hive.openBox("ZaiComicHistory", path: dir.path); + novelHistoryBox = await Hive.openBox("ZaiNovelHistory", path: dir.path); + localFavoriteBox = + await Hive.openBox("ZaiLocalFavorite", path: dir.path); + } + } + + Future putComicHistory(ComicHistory history) async { + await comicHistoryBox.put(history.comicId, history); + } + + Future updateComicHistory(ComicHistory history) async { + var historyItem = getComicHistory(history.comicId); + if (historyItem != null) { + historyItem.chapterId = history.chapterId; + historyItem.chapterName = history.chapterName; + historyItem.page = history.page; + historyItem.updateTime = history.updateTime; + await putComicHistory(historyItem); + } else { + await putComicHistory(history); + } + } + + ComicHistory? getComicHistory(int comicId) { + return comicHistoryBox.get(comicId); + } + + List getComicHistoryList() { + var ls = comicHistoryBox.values.where((x) => x.chapterId != 0).toList(); + ls.sort((a, b) => b.updateTime.compareTo(a.updateTime)); + return ls; + } + + /// 同步远程的漫画记录 + void syncRemoteComicHistory(List items) { + try { + for (var item in items) { + var remoteTime = + DateTime.fromMillisecondsSinceEpoch((item.viewingTime ?? 0) * 1000); + //本地是否存在记录 + var local = comicHistoryBox.get(item.comicId); + if (local != null && local.chapterId != 0) { + //与本地记录时间做比对,如果较新则覆盖,否则直接跳过处理 + if ((local.updateTime.millisecondsSinceEpoch ~/ 1000) < + remoteTime.millisecondsSinceEpoch) { + putComicHistory( + ComicHistory( + comicId: item.comicId, + chapterId: item.chapterId ?? 0, + comicName: item.comicName, + comicCover: item.cover, + chapterName: item.chapterName ?? "-", + updateTime: remoteTime, + page: item.record ?? 0, + ), + ); + } + } else { + //不存在,直接添加一条 + putComicHistory( + ComicHistory( + comicId: item.comicId, + chapterId: item.chapterId ?? 0, + comicName: item.comicName, + comicCover: item.cover, + chapterName: item.chapterName ?? "-", + updateTime: remoteTime, + page: item.record ?? 0, + ), + ); + } + } + } catch (e) { + Log.logPrint(e); + } + } + + Future putNovelHistory(NovelHistory history) async { + await novelHistoryBox.put(history.novelId, history); + } + + Future updateNovelHistory(NovelHistory history) async { + var historyItem = getNovelHistory(history.novelId); + if (historyItem != null) { + historyItem.chapterId = history.chapterId; + historyItem.chapterName = history.chapterName; + historyItem.total = history.total; + historyItem.index = history.index; + historyItem.volumeId = history.volumeId; + historyItem.volumeName = history.volumeName; + historyItem.updateTime = history.updateTime; + await putNovelHistory(historyItem); + } else { + await putNovelHistory(history); + } + } + + NovelHistory? getNovelHistory(int novelId) { + return novelHistoryBox.get(novelId); + } + + List getNovelHistoryList() { + var ls = novelHistoryBox.values.where((x) => x.chapterId != 0).toList(); + ls.sort((a, b) => b.updateTime.compareTo(a.updateTime)); + return ls; + } + + /// 同步远程的小说记录 + void syncRemoteNovelHistory(List items) { + try { + for (var item in items) { + var remoteTime = + DateTime.fromMillisecondsSinceEpoch((item.viewingTime ?? 0) * 1000); + //本地是否存在记录 + var local = novelHistoryBox.get(item.lnovelId); + if (local != null && local.chapterId != 0) { + //与本地记录时间做比对,如果较新则覆盖,否则直接跳过处理 + if ((local.updateTime.millisecondsSinceEpoch ~/ 1000) < + remoteTime.millisecondsSinceEpoch) { + putNovelHistory( + NovelHistory( + novelId: item.lnovelId, + chapterId: item.chapterId ?? 0, + novelName: item.novelName, + novelCover: item.cover, + chapterName: item.chapterName ?? "-", + updateTime: remoteTime, + index: item.record ?? 0, + volumeId: item.volumeId ?? 0, + volumeName: item.volumeName ?? "", + total: item.totalNum ?? 0, + ), + ); + } + } else { + //不存在,直接添加一条 + putNovelHistory( + NovelHistory( + novelId: item.lnovelId, + chapterId: item.chapterId ?? 0, + novelName: item.novelName, + novelCover: item.cover, + chapterName: item.chapterName ?? "-", + updateTime: remoteTime, + index: item.record ?? 0, + volumeId: item.volumeId ?? 0, + volumeName: item.volumeName ?? "", + total: item.totalNum ?? 0, + ), + ); + } + } + } catch (e) { + Log.logPrint(e); + } + } + + bool hasComicFavorited({required int comicId}) { + var id = "${AppConstant.kTypeComic}_$comicId"; + return localFavoriteBox.containsKey(id); + } + + void putComicFavorite( + {required String title, required String cover, required int comicId}) { + var id = "${AppConstant.kTypeComic}_$comicId"; + localFavoriteBox.put( + id, + LocalFavorite( + id: id, + cover: cover, + objId: comicId, + title: title, + type: AppConstant.kTypeComic, + updateTime: DateTime.now(), + ), + ); + } + + void removeComicFavorite({required int comicId}) { + var id = "${AppConstant.kTypeComic}_$comicId"; + localFavoriteBox.delete(id); + } +} diff --git a/lib/services/download_task/comic_downloader.dart b/lib/services/download_task/comic_downloader.dart new file mode 100644 index 0000000..cbff5f5 --- /dev/null +++ b/lib/services/download_task/comic_downloader.dart @@ -0,0 +1,193 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/db/comic_download_info.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/requests/comic_request.dart'; +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/services/comic_download_service.dart'; +import 'package:get/get.dart'; + +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; + +class ComicDownloader { + late Rx info; + final Function() onUpdateTask; + ComicDownloader(ComicDownloadInfo item, {required this.onUpdateTask}) { + info = Rx(item); + } + final ComicRequest request = ComicRequest(); + DownloadStatus get status => info.value.status; + CancelToken? cancelToken; + Dio dio = Dio(BaseOptions( + headers: { + 'Referer': "http://www.zaimanhua.com/", + }, + )); + void start() { + _getPageUrls(); + } + + void retry() { + _getPageUrls(); + } + + void pause() { + stopTask(); + updateStatus(DownloadStatus.pause); + } + + void cancel() async { + var result = await DialogUtils.showAlertDialog("确定要取消此任务吗?", title: "取消任务"); + if (!result) { + return; + } + cancelToken?.cancel(); + cancelToken = null; + await _delete(); + } + + void resume() { + _startDownload(); + } + + void stopTask() { + cancelToken?.cancel(); + cancelToken = null; + } + + void _getPageUrls() async { + try { + if (info.value.urls.isNotEmpty) { + _startDownload(); + return; + } + updateStatus(DownloadStatus.loadding); + var detail = await request.chapterDetail( + comicId: info.value.comicId, + chapterId: info.value.chapterId, + useHD: AppSettingsService.instance.comicReaderHD.value, + ); + if (detail.pageUrls.isEmpty) { + updateStatus(DownloadStatus.errorLoad); + return; + } + info.update((val) { + val!.urls = detail.pageUrls; + val.total = detail.pageUrls.length; + }); + await _saveInfo(); + _startDownload(); + } catch (e) { + updateStatus(DownloadStatus.errorLoad); + } + } + + int retryTime = 0; + void _startDownload() async { + updateStatus(DownloadStatus.downloading); + for (var i = info.value.index; i < info.value.total; i++) { + try { + if (status != DownloadStatus.downloading) { + break; + } + var url = info.value.urls[i]; + retryTime = 0; + await _downloadImage(url, i); + } catch (e) { + break; + } + } + if (status == DownloadStatus.downloading && + (info.value.index == info.value.total - 1)) { + updateStatus(DownloadStatus.complete); + } + } + + Future _downloadImage(String url, int index) async { + try { + //检查本地是否有缓存,有缓存直接复制本地的 + Uint8List bytes; + var localFile = await getCachedImageFile(url); + if (localFile != null) { + bytes = await localFile.readAsBytes(); + } else { + cancelToken = CancelToken(); + + var result = await dio.get( + url, + options: Options( + responseType: ResponseType.bytes, + ), + cancelToken: cancelToken, + ); + bytes = result.data; + } + var baseName = Uri.parse(url).path; + var fileName = await _saveImage(bytes, index, p.extension(baseName)); + info.update((val) { + val!.index = index; + val.files.add(fileName); + }); + await _saveInfo(); + } catch (e) { + Log.logPrint(e); + if (e is DioException) { + if (e.type == DioExceptionType.cancel) rethrow; + if (status == DownloadStatus.waitNetwork || + status == DownloadStatus.pauseCellular) rethrow; + if (retryTime < 3) { + retryTime++; + await Future.delayed(const Duration(seconds: 1)); + return await _downloadImage(url, index); + } + } + updateStatus(DownloadStatus.error); + rethrow; + } + } + + Future _saveImage( + Uint8List bytes, int index, String extension) async { + var dir = info.value.savePath; + var fileName = "${(index + 1).toString().padLeft(3, "0")}$extension"; + var file = File(p.join(dir, fileName)); + if (!await file.exists()) { + file = await file.create(recursive: true); + } + await file.writeAsBytes(bytes); + return fileName; + } + + void updateStatus(DownloadStatus e, {bool updateTask = true}) async { + info.update((val) { + val!.status = e; + }); + if (updateTask) { + onUpdateTask(); + } + + await _saveInfo(); + } + + /// 保存信息 + Future _saveInfo() async { + await ComicDownloadService.instance.box.put(info.value.taskId, info.value); + } + + Future _delete() async { + try { + var dir = Directory(info.value.savePath); + await dir.delete(recursive: true); + } finally { + ComicDownloadService.instance.downloadIds.remove(info.value.taskId); + await ComicDownloadService.instance.box.delete(info.value.taskId); + updateStatus(DownloadStatus.cancel); + } + } +} diff --git a/lib/services/download_task/novel_downloader.dart b/lib/services/download_task/novel_downloader.dart new file mode 100644 index 0000000..3a247af --- /dev/null +++ b/lib/services/download_task/novel_downloader.dart @@ -0,0 +1,229 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:extended_image/extended_image.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/db/novel_download_info.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/requests/novel_request.dart'; +import 'package:flutter_dmzj/services/novel_download_service.dart'; +import 'package:get/get.dart'; + +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; + +class NovelDownloader { + late Rx info; + final Function() onUpdateTask; + NovelDownloader(NovelDownloadInfo item, {required this.onUpdateTask}) { + info = Rx(item); + } + final NovelRequest request = NovelRequest(); + DownloadStatus get status => info.value.status; + CancelToken? cancelToken; + Dio dio = Dio(BaseOptions( + headers: { + 'Referer': "http://www.zaimanhua.com/", + }, + )); + void start() { + _startDownload(); + } + + void retry() { + _startDownload(); + } + + void pause() { + stopTask(); + updateStatus(DownloadStatus.pause); + } + + void cancel() async { + var result = await DialogUtils.showAlertDialog("确定要取消此任务吗?", title: "取消任务"); + if (!result) { + return; + } + cancelToken?.cancel(); + cancelToken = null; + await _delete(); + } + + void resume() { + _startDownload(); + } + + void stopTask() { + cancelToken?.cancel(); + cancelToken = null; + } + + int retryTime = 0; + void _startDownload() async { + updateStatus(DownloadStatus.downloading); + retryTime = 0; + await _downloadContent(); + int i = 0; + for (var url in info.value.imageUrls) { + try { + if (status != DownloadStatus.downloading) { + break; + } + + retryTime = 0; + await _downloadImage(url, i); + i++; + } catch (e) { + break; + } + } + if (status == DownloadStatus.downloading) { + updateStatus(DownloadStatus.complete); + } + } + + Future _downloadContent() async { + try { + cancelToken = CancelToken(); + var content = await request.novelContent( + volumeId: info.value.volumeID, + chapterId: info.value.chapterId, + cancel: cancelToken, + cache: false, + ); + var fileName = await _saveContent(content); + var subStr = + content.substring(0, content.length < 200 ? content.length : 200); + //检查是否是插画 + if (subStr.contains(RegExp(''))) { + List imgs = []; + for (var item in RegExp(r'') + .allMatches(content)) { + var src = item.group(1); + if (src != null && src.isNotEmpty) { + imgs.add(src); + } + } + info.update((val) { + val!.fileName = fileName; + val.imageUrls = imgs; + val.isImage = true; + }); + } else { + info.update((val) { + val!.fileName = fileName; + val.isImage = false; + }); + } + + await _saveInfo(); + } catch (e) { + Log.logPrint(e); + if (e is DioException) { + if (e.type == DioExceptionType.cancel) rethrow; + if (status == DownloadStatus.waitNetwork || + status == DownloadStatus.pauseCellular) rethrow; + if (retryTime < 3) { + retryTime++; + await Future.delayed(const Duration(seconds: 1)); + return await _downloadContent(); + } + } + updateStatus(DownloadStatus.error); + rethrow; + } + } + + Future _downloadImage(String url, int index) async { + try { + //检查本地是否有缓存,有缓存直接复制本地的 + Uint8List bytes; + var localFile = await getCachedImageFile(url); + if (localFile != null) { + bytes = await localFile.readAsBytes(); + } else { + cancelToken = CancelToken(); + + var result = await dio.get( + url, + options: Options( + responseType: ResponseType.bytes, + ), + cancelToken: cancelToken, + ); + bytes = result.data; + } + var baseName = Uri.parse(url).path; + var fileName = await _saveImage(bytes, index, p.extension(baseName)); + info.update((val) { + val!.imageFiles.add(fileName); + }); + await _saveInfo(); + } catch (e) { + Log.logPrint(e); + if (e is DioException) { + if (e.type == DioExceptionType.cancel) rethrow; + if (status == DownloadStatus.waitNetwork || + status == DownloadStatus.pauseCellular) rethrow; + if (retryTime < 3) { + retryTime++; + await Future.delayed(const Duration(seconds: 1)); + return await _downloadImage(url, index); + } + } + updateStatus(DownloadStatus.error); + rethrow; + } + } + + Future _saveContent(String content) async { + var dir = info.value.savePath; + var fileName = "${info.value.taskId}.txt"; + var file = File(p.join(dir, fileName)); + if (!await file.exists()) { + file = await file.create(recursive: true); + } + await file.writeAsString(content); + return fileName; + } + + Future _saveImage( + Uint8List bytes, int index, String extension) async { + var dir = info.value.savePath; + var fileName = "${(index + 1).toString().padLeft(3, "0")}$extension"; + var file = File(p.join(dir, fileName)); + if (!await file.exists()) { + file = await file.create(recursive: true); + } + await file.writeAsBytes(bytes); + return fileName; + } + + void updateStatus(DownloadStatus e, {bool updateTask = true}) async { + info.update((val) { + val!.status = e; + }); + if (updateTask) { + onUpdateTask(); + } + + await _saveInfo(); + } + + /// 保存信息 + Future _saveInfo() async { + await NovelDownloadService.instance.box.put(info.value.taskId, info.value); + } + + Future _delete() async { + try { + var dir = Directory(info.value.savePath); + await dir.delete(recursive: true); + } finally { + await NovelDownloadService.instance.box.delete(info.value.taskId); + updateStatus(DownloadStatus.cancel); + } + } +} diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart new file mode 100644 index 0000000..8326532 --- /dev/null +++ b/lib/services/local_storage_service.dart @@ -0,0 +1,201 @@ +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; + +class LocalStorageService extends GetxService { + static LocalStorageService get instance => Get.find(); + + static bool kDebug = false; + + /// 显示模式 + /// * [0] 跟随系统 + /// * [1] 浅色模式 + /// * [2] 深色模式 + static const String kThemeMode = "ThemeMode"; + + /// 首次运行 + static const String kFirstRun = "FirstRun"; + + /// 用户登录信息 + /// * 类型:LoginResultModel + static const String kUserAuthInfo = "UserAuthInfo"; + + /// 漫画阅读方向 + static const String kComicReaderDirection = "ComicReaderDirection"; + + /// 漫画全屏阅读 + static const String kComicReaderFullScreen = "ComicReaderFullScreen"; + + /// 漫画阅读显示状态信息 + static const String kComicReaderShowStatus = "ComicReaderShowStatus"; + + /// 漫画阅读尾页显示观点/吐槽 + static const String kComicReaderShowViewPoint = "ComicReaderShowViewPoint"; + + /// 启用旧版吐槽 + static const String kComicReaderOldViewPoint = "ComicReaderOldViewPoint"; + + /// 小说阅读方向 + static const String kNovelReaderDirection = "NovelReaderDirection"; + + /// 小说字体大小 + static const String kNovelReaderFontSize = "NovelReaderFontSize"; + + /// 小说行距 + static const String kNovelReaderLineSpacing = "NovelReaderLineSpacing"; + + /// 小说阅读主题 + static const String kNovelReaderTheme = "NovelReaderTheme"; + + /// 小说阅读显示状态信息 + static const String kNovelReaderShowStatus = "NovelReaderShowStatus"; + + /// 小说全屏阅读 + static const String kNovelReaderFullScreen = "NovelReaderFullScreen"; + + /// 下载是否允许使用流量 + static const String kDownloadAllowCellular = "DownloadAllowCellular"; + + /// 下载小说最大任务数 + static const String kDownloadNovelTaskCount = "DownloadNovelTaskCount"; + + /// 下载漫画最大任务数 + static const String kDownloadComicTaskCount = "DownloadComicTaskCount"; + + /// 漫画搜索使用Web接口 + static const String kComicSearchUseWebApi = "ComicSearchUseWebApi"; + + /// 显示字体大小跟随系统 + static const String kUseSystemFontSize = "UseSystemFontSize"; + + /// 漫画-左手模式 + static const String kComicReaderLeftHandMode = "ComicReaderLeftHandMode"; + + /// 小说-左手模式 + static const String kNovelReaderLeftHandMode = "NovelReaderLeftHandMode"; + + /// 漫画阅读优先加载高清图 + static const String kComicReaderHD = "ComicReaderHD"; + + /// 漫画阅读-翻页动画 + static const String kComicReaderPageAnimation = "ComicReaderPageAnimation"; + + /// 小说阅读-翻页动画 + static const String kNovelReaderPageAnimation = "NovelReaderPageAnimation"; + + /// 新闻字体大小 + static const String kNewsFontSize = "NewsFontSize"; + + /// 自动添加神隐漫画至收藏夹 + static const String kCollectHideComic = "CollectHideComic"; + + /// 代理地址 + static const String kProxyAddress = "ProxyAddress"; + + /// 是否使用MD动态取色 + static const String kUseDynamicColor = "UseDynamicColor"; + + late Box settingsBox; + Future init() async { + if (kIsWeb) { + settingsBox = await Hive.openBox("LocalStorage"); + } else { + var dir = await getApplicationSupportDirectory(); + settingsBox = await Hive.openBox( + "LocalStorage", + path: dir.path, + ); + } + } + + T getValue(dynamic key, T defaultValue) { + var value = settingsBox.get(key, defaultValue: defaultValue) as T; + Log.d("Get LocalStorage:$key\r\n$value"); + return value; + } + + Future setValue(dynamic key, T value) async { + Log.d("Set LocalStorage:$key\r\n$value"); + return await settingsBox.put(key, value); + } + + Future removeValue(dynamic key) async { + Log.d("Remove LocalStorage:$key"); + return await settingsBox.delete(key); + } + + bool get isFirst => getValue("First", true); + + void setNoFirst() { + setValue("First", false); + } + + Future getNovelCacheDirectory() async { + var dir = await getApplicationSupportDirectory(); + var novelDir = Directory(p.join(dir.path, "novel_cache")); + if (!await novelDir.exists()) { + novelDir = await novelDir.create(); + } + return novelDir; + } + + Future saveNovelContent({ + required int volumeId, + required int chapterId, + required String content, + }) async { + try { + var novelDir = await getNovelCacheDirectory(); + + var fileName = p.join(novelDir.path, "${volumeId}_$chapterId.txt"); + var file = File(fileName); + await file.writeAsString(content); + } catch (e) { + Log.logPrint(e); + } + } + + Future getNovelContent( + {required int volumeId, required int chapterId}) async { + try { + var novelDir = await getNovelCacheDirectory(); + var fileName = p.join(novelDir.path, "${volumeId}_$chapterId.txt"); + var file = File(fileName); + + if (await file.exists()) { + var content = await file.readAsString(); + return content; + } + return null; + } catch (e) { + Log.logPrint(e); + return null; + } + } + + Future getNovelCacheSize() async { + var novelDir = await getNovelCacheDirectory(); + var size = 0; + await for (var item in novelDir.list()) { + size += item.statSync().size; + } + return size; + } + + Future cleanNovelCacheSize() async { + try { + var novelDir = await getNovelCacheDirectory(); + + await novelDir.delete(recursive: true); + return true; + } catch (e) { + Log.logPrint(e); + return false; + } + } +} diff --git a/lib/services/novel_download_service.dart b/lib/services/novel_download_service.dart new file mode 100644 index 0000000..4115479 --- /dev/null +++ b/lib/services/novel_download_service.dart @@ -0,0 +1,401 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; + +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/models/db/novel_download_info.dart'; +import 'package:flutter_dmzj/models/db/download_status.dart'; +import 'package:flutter_dmzj/models/novel/novel_detail_model.dart'; + +import 'package:flutter_dmzj/services/app_settings_service.dart'; +import 'package:flutter_dmzj/services/download_task/novel_downloader.dart'; +import 'package:get/get.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path_provider/path_provider.dart'; +// ignore: depend_on_referenced_packages +import 'package:collection/collection.dart'; +// ignore: depend_on_referenced_packages +import 'package:path/path.dart' as p; + +/// 小说下载管理 +// TODO 整理代码 +class NovelDownloadService extends GetxService { + static NovelDownloadService get instance => Get.find(); + + AppSettingsService settings = AppSettingsService.instance; + + late Box box; + String savePath = ""; + + /// 连接信息监听 + StreamSubscription? connectivitySubscription; + + /// 当前连接类型 + ConnectivityResult? connectivityType; + + /// 当前正在下载的数量 + var currentNum = 0; + + Future init() async { + var dir = await getApplicationSupportDirectory(); + box = await Hive.openBox( + "NovelDownload", + path: dir.path, + ); + savePath = await getSavePath(); + //监听网络状态 + initConnectivity(); + //更新ID + updateAllIds(); + + updateDownlaoded(); + } + + /// 初始化连接状态 + void initConnectivity() async { + try { + var connectivity = Connectivity(); + connectivitySubscription = connectivity.onConnectivityChanged + .listen((ConnectivityResult result) { + networkChanged(result); + }); + connectivityType = await connectivity.checkConnectivity(); + initTasks(); + } catch (e) { + Log.logPrint(e); + initTasks(); + } + } + + /// 网络变更 + void networkChanged(ConnectivityResult type) { + if (connectivityType != type && type == ConnectivityResult.mobile) { + //切换至流量 + switchCellular(); + } else if (connectivityType != type && type == ConnectivityResult.none) { + //网络断开 + switchNoNetwork(); + } else { + switchToWiFi(); + } + connectivityType = type; + } + + /// 切换至流量 + void switchCellular() { + if (settings.downloadAllowCellular.value) { + //允许使用流量,当成WiFi处理 + switchToWiFi(); + return; + } + //把任务状态改为pauseCellular + for (var item in taskQueues) { + if (item.status == DownloadStatus.wait || + item.status == DownloadStatus.loadding || + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.waitNetwork) { + item.stopTask(); + item.updateStatus(DownloadStatus.pauseCellular, updateTask: false); + } + } + updateQueue(); + } + + /// 无网络 + void switchNoNetwork() { + //把任务状态改为pauseCellular + for (var item in taskQueues) { + if (item.status == DownloadStatus.wait || + item.status == DownloadStatus.loadding || + item.status == DownloadStatus.downloading || + item.status == DownloadStatus.pauseCellular) { + item.stopTask(); + item.updateStatus(DownloadStatus.waitNetwork, updateTask: false); + } + } + updateQueue(); + } + + void switchToWiFi() { + for (var item in taskQueues) { + if (item.status == DownloadStatus.pauseCellular || + item.status == DownloadStatus.waitNetwork) { + item.updateStatus(DownloadStatus.wait, updateTask: false); + } + } + updateQueue(); + } + + /// 任务列表 + RxList taskQueues = RxList(); + + /// 已下载完成的 + RxList downloaded = RxList(); + + /// 已下载、下载中的ID + RxSet downloadIds = RxSet(); + + /// 开始下载任务 + void initTasks() async { + var tasks = getDownloadingTask(); + for (var item in tasks) { + //任务已被取消 + if (item.status == DownloadStatus.cancel) { + box.delete(item.taskId); + continue; + } + //无网络 + if (connectivityType == ConnectivityResult.none) { + if (item.status != DownloadStatus.pause) { + item.status = DownloadStatus.waitNetwork; + } + } else if (connectivityType == ConnectivityResult.mobile) { + //不允许使用数据下载 + if (!settings.downloadAllowCellular.value) { + if (item.status != DownloadStatus.pause) { + item.status = DownloadStatus.pauseCellular; + } + } + } else { + //只要不是手动暂停的,全部改为等待,添加到下载队列 + if (item.status != DownloadStatus.pause) { + item.status = DownloadStatus.wait; + } + } + + taskQueues.add( + NovelDownloader(item, onUpdateTask: onUpdateTask), + ); + } + updateQueue(); + } + + /// 更新队列 + void updateQueue() { + //如果下载中任务数小于设定值,添加一个任务 + //如果任务取消或完成,移除队列 + for (var task in List.from(taskQueues)) { + //下载完成或取消,移除队列 + if (task.status == DownloadStatus.complete || + task.status == DownloadStatus.cancel) { + taskQueues.remove(task); + updateDownlaoded(); + continue; + } + } + var taskNum = settings.downloadNovelTaskCount.value; + var count = taskQueues + .where((x) => + x.status == DownloadStatus.downloading || + x.status == DownloadStatus.loadding) + .length; + + currentNum = count; + if (taskNum == 0) { + var ls = taskQueues.where((x) => x.status == DownloadStatus.wait); + for (var item in ls) { + item.start(); + } + } else { + if (count < taskNum) { + var ls = taskQueues + .where((x) => x.status == DownloadStatus.wait) + .take(taskNum - count) + .toList(); + for (var item in ls) { + item.start(); + } + } + } + updateAllIds(); + } + + void updateAllIds() { + downloadIds.clear(); + downloadIds.addAll(box.keys.map((e) => e.toString())); + } + + ///读取未完成的任务 + List getDownloadingTask() { + return box.values + .toList() + .where((x) => x.status != DownloadStatus.complete) + .toList(); + } + + /// 更新下载完成 + void updateDownlaoded() { + var downlaodedList = box.values + .toList() + .where((x) => x.status == DownloadStatus.complete) + .toList(); + var novelMap = groupBy(downlaodedList, (NovelDownloadInfo x) => x.novelId); + List novelList = []; + for (var novelId in novelMap.keys) { + var items = novelMap[novelId]!; + var novelName = items.first.novelName; + var novelCover = items.first.novelCover; + + List volumes = []; + var volumeMap = groupBy(items, (NovelDownloadInfo x) => x.volumeID); + for (var volumeID in volumeMap.keys) { + var chapters = volumeMap[volumeID]! + .map( + (e) => NovelDetailChapter( + chapterId: e.chapterId, + chapterName: e.chapterName, + volumeId: e.volumeID, + volumeName: e.volumeName, + volumeOrder: e.volumeOrder, + chapterOrder: e.chapterSort, + ), + ) + .toList(); + chapters.sort((a, b) => a.chapterOrder.compareTo(b.chapterOrder)); + volumes.add( + NovelDetailVolume( + volumeName: chapters.first.volumeName, + volumeId: chapters.first.volumeId, + volumeOrder: chapters.first.volumeOrder, + chapters: chapters, + ), + ); + } + volumes.sort((a, b) => a.volumeOrder.compareTo(b.volumeOrder)); + novelList.add( + NovelDownloadedItem( + novelName: novelName, + novelCover: novelCover, + novelId: novelId, + chapterCount: items.length, + volumes: volumes, + ), + ); + } + downloaded.value = novelList; + } + + /// 继续 + void resumeAll() { + //更新状态至等待 + for (var task in taskQueues) { + if (task.status == DownloadStatus.pause) { + task.stopTask(); + task.updateStatus(DownloadStatus.wait, updateTask: false); + } + } + updateQueue(); + } + + /// 暂停 + void pauseAll() { + for (var task in taskQueues) { + if (task.status != DownloadStatus.pause && + task.status != DownloadStatus.error && + task.status != DownloadStatus.errorLoad) { + task.stopTask(); + task.updateStatus(DownloadStatus.pause, updateTask: false); + } + } + updateQueue(); + } + + /// 添加一个任务 + void addTask({ + required int novelId, + required int chapterId, + required String chapterName, + required int chapterSort, + required int volumeId, + required int volumeOrder, + required String volumeName, + required String novelTitle, + required String novelCover, + required bool isVip, + }) async { + var taskId = "${novelId}_${volumeId}_$chapterId"; + if (box.containsKey(taskId)) { + return; + } + var info = NovelDownloadInfo( + addTime: DateTime.now(), + chapterId: chapterId, + chapterSort: chapterSort, + novelCover: novelCover, + novelId: novelId, + novelName: novelTitle, + savePath: p.join(savePath, taskId), + status: DownloadStatus.wait, + taskId: taskId, + volumeName: volumeName, + chapterName: chapterName, + isVip: isVip, + progress: 0, + fileName: '', + imageFiles: [], + isImage: false, + volumeID: volumeId, + volumeOrder: volumeOrder, + imageUrls: [], + ); + await box.put( + taskId, + info, + ); + taskQueues.add(NovelDownloader(info, onUpdateTask: onUpdateTask)); + updateQueue(); + } + + void onUpdateTask() { + updateQueue(); + } + + /// 读取保存目录 + Future getSavePath() async { + var dir = await getApplicationSupportDirectory(); + + var novelDir = Directory(p.join(dir.path, "novel")); + if (!await novelDir.exists()) { + novelDir = await novelDir.create(recursive: true); + } + return novelDir.path; + } + + ///删除 + void delete(NovelDownloadInfo info) async { + try { + var dir = Directory(p.join(savePath, info.taskId)); + await dir.delete(recursive: true); + } catch (e) { + Log.logPrint(e); + } finally { + await box.delete(info.taskId); + updateDownlaoded(); + } + updateAllIds(); + } + + ///删除 + void deleteChapter(int novelId, int volumeId, int chapterId) async { + var info = box.get("${novelId}_${volumeId}_$chapterId"); + if (info != null) { + delete(info); + } + } +} + +class NovelDownloadedItem { + final String novelName; + final int novelId; + final String novelCover; + final List volumes; + final int chapterCount; + NovelDownloadedItem({ + required this.novelName, + required this.novelCover, + required this.novelId, + required this.chapterCount, + required this.volumes, + }); +} diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart new file mode 100644 index 0000000..669b61a --- /dev/null +++ b/lib/services/user_service.dart @@ -0,0 +1,333 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_dmzj/app/app_constant.dart'; +import 'package:flutter_dmzj/app/event_bus.dart'; +import 'package:flutter_dmzj/app/log.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/db/comic_history.dart'; +import 'package:flutter_dmzj/models/db/novel_history.dart'; +import 'package:flutter_dmzj/models/user/login_result_model.dart'; +import 'package:flutter_dmzj/models/user/user_profile_model.dart'; +import 'package:flutter_dmzj/modules/user/login/user_login_dialog.dart'; +import 'package:flutter_dmzj/requests/user_request.dart'; +import 'package:flutter_dmzj/services/db_service.dart'; +import 'package:flutter_dmzj/services/local_storage_service.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class UserService extends GetxService { + static StreamController loginedStreamController = + StreamController.broadcast(); + static StreamController logoutStreamController = StreamController.broadcast(); + + ///登录事件流 + static Stream get loginedStream => loginedStreamController.stream; + + ///退出登录事件流 + static Stream get logoutStream => logoutStreamController.stream; + + static UserService get instance => Get.find(); + final LocalStorageService storage = Get.find(); + final request = UserRequest(); + LoginResultModel? userAuthInfo; + + Rx userProfile = Rx(null); + + String get dmzjToken => userAuthInfo?.token ?? ''; + String get userId => userAuthInfo?.uid.toString() ?? ''; + String get nickname => userAuthInfo?.nickname ?? ''; + + bool get isVip => (userProfile.value?.userfeeinfo?.isVip ?? false); + + String get sign => (userProfile.value?.description ?? "").isEmpty + ? "无个性签名" + : userProfile.value?.description ?? ""; + + String get vipInfo => + "会员有效期至${Utils.dateFormat.format(userProfile.value?.userfeeinfo?.expiresTime ?? DateTime.now())}"; + + /// 是否已经绑定手机号 + var bindTel = true.obs; + + /// 是否已经设置密码 + var setPwd = true.obs; + + /// 是否已经登录 + var logined = false.obs; + + /// 已经订阅的漫画ID + var subscribedComicIds = RxSet(); + + /// 已经订阅的小说ID + var subscribedNovelIds = RxSet(); + + void init() { + var value = storage.getValue(LocalStorageService.kUserAuthInfo, ''); + if (value.isEmpty) { + return; + } + LoginResultModel info = LoginResultModel.fromJson(json.decode(value)); + + userAuthInfo = info; + logined.value = true; + if (logined.value) { + //syncRemoteHistory(); + } + } + + /// 设置登录信息 + void setAuthInfo(LoginResultModel info) { + userAuthInfo = info; + storage.setValue(LocalStorageService.kUserAuthInfo, info.toString()); + logined.value = true; + UserService.loginedStreamController.add(true); + //refreshProfile(); + syncRemoteHistory(); + } + + void logout() { + storage.removeValue(LocalStorageService.kUserAuthInfo); + userProfile.value = null; + logined.value = false; + UserService.logoutStreamController.add(true); + } + + Future login() async { + if (logined.value) { + return true; + } + var result = await Get.dialog(UserLoginDialog()); + + return (result != null && result == true); + } + + /// 刷新个人资料 + Future refreshProfile() async { + try { + if (!logined.value) { + return; + } + userProfile.value = await request.userProfile(); + //updateCookie(); + updateBindStatus(); + } catch (e) { + Log.logPrint(e); + } + } + + /// 更新一下用户的历史记录 + void syncRemoteHistory() { + if (!logined.value) { + return; + } + syncRemoteComicHistory(); + syncRemoteNovelHistory(); + } + + void syncRemoteComicHistory() async { + try { + await request.comicHistory(); + } catch (e) { + Log.logPrint(e); + } + } + + void syncRemoteNovelHistory() async { + try { + await request.novelHistory(); + } catch (e) { + Log.logPrint(e); + } + } + + /// 更新绑定状态 + void updateBindStatus() async { + try { + if (!logined.value) { + return; + } + var result = await request.isBindTelPwd(); + bindTel.value = result.isBindTel == 1; + setPwd.value = result.isBindTel == 1; + } catch (e) { + Log.logPrint(e); + } + } + + /// 添加订阅 + Future addSubscribe(List ids, int type) async { + try { + if (!await login()) { + return false; + } + await request.addSubscribe( + ids: ids, + type: type, + ); + if (type == AppConstant.kTypeComic) { + subscribedComicIds.addAll(ids); + } else if (type == AppConstant.kTypeNovel) { + subscribedNovelIds.addAll(ids); + } + + SmartDialog.showToast("订阅成功"); + return true; + } catch (e) { + SmartDialog.showToast(e.toString()); + return false; + } + } + + /// 取消订阅 + Future cancelSubscribe(List ids, int type) async { + try { + if (!await login()) { + return false; + } + await request.removeSubscribe( + ids: ids, + type: type, + ); + if (type == AppConstant.kTypeComic) { + subscribedComicIds.removeAll(ids); + } else if (type == AppConstant.kTypeNovel) { + subscribedNovelIds.removeAll(ids); + } + SmartDialog.showToast("已取消订阅"); + return true; + } catch (e) { + SmartDialog.showToast(e.toString()); + return false; + } + } + + /// 更新漫画记录 + Future updateComicHistory({ + required int comicId, + required int chapterId, + required int page, + required String comicName, + required String comicCover, + required String chapterName, + }) async { + try { + var time = DateTime.now(); + await DBService.instance.updateComicHistory( + ComicHistory( + comicId: comicId, + chapterId: chapterId, + comicName: comicName, + comicCover: comicCover, + chapterName: chapterName, + updateTime: time, + page: page, + ), + ); + EventBus.instance.emit(EventBus.kUpdatedComicHistory, comicId); + if (!logined.value) { + return; + } + // await request.uploadComicHistory( + // comicId: comicId, + // chapterId: chapterId, + // page: page, + // time: time, + // ); + } catch (e) { + Log.logPrint(e); + } + } + + /// 更新漫画记录 + Future updateNovelHistory({ + required int novelId, + required int chapterId, + required int index, + required int total, + required String novelName, + required String novelCover, + required String chapterName, + required int volumeId, + required String volumeName, + }) async { + try { + var time = DateTime.now(); + await DBService.instance.updateNovelHistory( + NovelHistory( + novelId: novelId, + chapterId: chapterId, + volumeName: volumeName, + volumeId: volumeId, + chapterName: chapterName, + updateTime: time, + index: index, + total: total, + novelCover: novelCover, + novelName: novelName, + ), + ); + EventBus.instance.emit(EventBus.kUpdatedNovelHistory, novelId); + if (!logined.value) { + return; + } + await request.uploadNovelHistory( + novelId: novelId, + volumeId: volumeId, + chapterId: chapterId, + page: 1, + total: total, + time: time, + ); + } catch (e) { + Log.logPrint(e); + } + } + + void updateCookie() async { + if (Platform.isAndroid || Platform.isIOS) { + final WebViewCookieManager cookieManager = WebViewCookieManager(); + for (var item in getCookies()) { + await cookieManager.setCookie(item); + } + } + } + + List getCookies() { + var cookie = userProfile.value?.cookieVal ?? ""; + if (cookie.isEmpty) { + return []; + } + List cookies = []; + + cookie.split(";").forEach((element) { + List keyValue = element.split("="); + if (keyValue.length == 2) { + cookies.add( + WebViewCookie( + domain: ".dmzj.com", + value: keyValue[1], + name: keyValue[0], + ), + ); + cookies.add( + WebViewCookie( + domain: ".idmzj.com", + value: keyValue[1], + name: keyValue[0], + ), + ); + cookies.add( + WebViewCookie( + domain: ".muwai.com", + value: keyValue[1], + name: keyValue[0], + ), + ); + } + }); + return cookies; + } +} diff --git a/lib/widgets/border_text.dart b/lib/widgets/border_text.dart new file mode 100644 index 0000000..f0d6e91 --- /dev/null +++ b/lib/widgets/border_text.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class BorderText extends StatelessWidget { + final String text; + final TextAlign textAlign; + final Color color; + final double fontSize; + final double strokeWidth; + const BorderText( + this.text, { + this.textAlign = TextAlign.left, + this.color = Colors.white, + this.fontSize = 16, + this.strokeWidth = 2.0, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Text( + text, + softWrap: false, + textAlign: textAlign, + style: TextStyle( + fontSize: fontSize, + foreground: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..color = getBorderColor(color), + ), + ), + Text( + text, + softWrap: false, + textAlign: textAlign, + style: TextStyle( + fontSize: fontSize, + color: color, + ), + ), + ], + ); + } + + Color getBorderColor(Color color) { + var brightness = + ((color.red * 299) + (color.green * 587) + (color.blue * 114)) / 1000; + return brightness > 70 ? Colors.black : Colors.white; + } +} diff --git a/lib/widgets/comment_item_widget.dart b/lib/widgets/comment_item_widget.dart new file mode 100644 index 0000000..d640665 --- /dev/null +++ b/lib/widgets/comment_item_widget.dart @@ -0,0 +1,328 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/dialog_utils.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_dmzj/models/comment/comment_item.dart'; +import 'package:flutter_dmzj/requests/comment_request.dart'; +import 'package:flutter_dmzj/routes/app_navigator.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:flutter_dmzj/widgets/user_photo.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'dart:ui' as ui; + +import 'package:remixicon/remixicon.dart'; + +// ignore: must_be_immutable +class CommentItemWidget extends StatelessWidget { + final CommentItem item; + CommentItemWidget(this.item, {Key? key}) : super(key: key); + var expand = false.obs; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + onTap(item); + }, + child: Container( + padding: AppStyle.edgeInsetsA12, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () { + AppNavigator.toUserCenter(item.userId); + }, + child: UserPhoto( + url: item.photo, + ), + ), + AppStyle.hGap12, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + item.nickname, + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + // const Text( + // "-", + // style: TextStyle(color: Colors.grey), + // ) + ], + ), + AppStyle.vGap12, + item.parents.isNotEmpty + ? Obx( + () => expand.value + ? createMasterCommentAll(item.parents) + : createMasterComment(item), + ) + : Container(), + Text( + item.content, + style: Get.theme.textTheme.bodyMedium, + ), + item.images.isNotEmpty + ? Padding( + padding: AppStyle.edgeInsetsT12, + child: Wrap( + spacing: 4, + runSpacing: 4, + children: item.images.map((f) { + var str = f.split(".").toList(); + var fileImg = str[0]; + var fileImgSuffix = str[1]; + return InkWell( + onTap: () { + DialogUtils.showImageViewer(0, [ + "https://images.zaimanhua.com/commentImg/${item.objId % 500}/$f" + ]); + }, + child: NetImage( + "https://images.zaimanhua.com/commentImg/${item.objId % 500}/${fileImg}_small.$fileImgSuffix", + width: 100, + height: 100, + borderRadius: 4, + ), + ); + }).toList(), + ), + ) + : const SizedBox(), + AppStyle.vGap12, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Text( + Utils.formatTimestamp(item.createTime), + style: + const TextStyle(color: Colors.grey, fontSize: 12), + ), + ), + Obx( + () => GestureDetector( + onTap: () { + likeComment(item); + }, + child: Row( + children: [ + Icon( + Remix.thumb_up_fill, + size: 16, + color: Theme.of(context).colorScheme.secondary, + ), + Visibility( + visible: item.likeAmount.value > 0, + child: Padding( + padding: AppStyle.edgeInsetsL4, + child: Text( + item.likeAmount.value.toString(), + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ) + ], + )) + ], + ), + ), + ); + } + + Widget createMasterComment(CommentItem comment) { + var list = comment.parents; + if (list.isEmpty) return const SizedBox(); + List items = []; + if (list.length > 2) { + items.add(createMsterCommentItem(list.first)); + items.add(InkWell( + onTap: () { + expand.value = true; + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(4)), + padding: AppStyle.edgeInsetsA8, + child: Center( + child: Text( + "点击展开${list.length - 2}条评论", + style: const TextStyle(fontSize: 12, color: Colors.grey), + )), + ), + )); + items.add(AppStyle.vGap8); + items.add(createMsterCommentItem(list.last)); + } else { + for (var item in list) { + items.add(createMsterCommentItem(item)); + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items, + ); + } + + Widget createMasterCommentAll(List list) { + List items = list.map((item) { + return createMsterCommentItem(item); + }).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: items, + ); + } + + Widget createMsterCommentItem(CommentItem item) { + return Padding( + padding: AppStyle.edgeInsetsB8, + child: InkWell( + onTap: () { + onTap(item); + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(4)), + padding: AppStyle.edgeInsetsA8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan(children: [ + WidgetSpan( + alignment: ui.PlaceholderAlignment.middle, + child: InkWell( + child: Text( + item.nickname, + style: + TextStyle(color: Get.theme.colorScheme.secondary), + ), + ), + ), + TextSpan( + text: ": ${item.content}", + style: Get.theme.textTheme.bodyMedium, + ) + ]), + ), + item.images.isNotEmpty + ? Padding( + padding: AppStyle.edgeInsetsT8, + child: Wrap( + spacing: 4, + runSpacing: 4, + children: item.images.map((f) { + var str = f.split(".").toList(); + var fileImg = str[0]; + var fileImgSuffix = str[1]; + return InkWell( + onTap: () { + DialogUtils.showImageViewer(0, [ + "https://images.idmzj.com/commentImg/${item.objId % 500}/$f" + ]); + }, + child: NetImage( + "https://images.idmzj.com/commentImg/${item.objId % 500}/${fileImg}_small.$fileImgSuffix", + width: 100, + height: 100, + borderRadius: 4, + ), + ); + }).toList(), + ), + ) + : Container(), + ], + ), + ), + ), + ); + } + + void likeComment(CommentItem item) async { + try { + await CommentRequest().likeComment( + commentId: item.id, + objId: item.objId, + type: item.type, + ); + item.likeAmount.value += 1; + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } + + void onTap(CommentItem item) { + AppNavigator.showBottomSheet(Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + title: Text(item.nickname), + leading: UserPhoto( + url: item.photo, + size: 32, + showBoder: true, + ), + onTap: () { + AppNavigator.toUserCenter(item.userId); + }, + ), + ListTile( + title: const Text("复制内容"), + leading: const Icon(Icons.content_copy), + onTap: () { + Utils.copyText(item.content); + + AppNavigator.closePage(); + }, + ), + ListTile( + title: const Text("点赞评论"), + leading: const Icon(Icons.thumb_up_outlined), + onTap: () { + AppNavigator.closePage(); + likeComment(item); + }, + ), + ListTile( + title: const Text("回复评论"), + leading: const Icon(Icons.message_outlined), + onTap: () { + AppNavigator.closePage(); + AppNavigator.toAddComment( + objId: item.objId, + type: item.type, + replyItem: item, + ); + }, + ), + ], + )); + } +} diff --git a/lib/widgets/custom_header.dart b/lib/widgets/custom_header.dart new file mode 100644 index 0000000..c71b68a --- /dev/null +++ b/lib/widgets/custom_header.dart @@ -0,0 +1,439 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart' as physics; + +import 'dart:math' as math; + +/// Material header. +class MaterialHeader2 extends Header { + final Key? key; + + /// See [ProgressIndicator.backgroundColor]. + final Color? backgroundColor; + + /// See [ProgressIndicator.color]. + final Color? color; + + /// See [ProgressIndicator.valueColor]. + final Animation? valueColor; + + /// See [ProgressIndicator.semanticsLabel]. + final String? semanticsLabel; + + /// See [ProgressIndicator.semanticsLabel]. + final String? semanticsValue; + + /// Icon when [IndicatorResult.noMore]. + final Widget? noMoreIcon; + + /// Show bezier background. + final bool showBezierBackground; + + /// Bezier background color. + /// See [BezierBackground.color]. + final Color? bezierBackgroundColor; + + /// Bezier background animation. + /// See [BezierBackground.useAnimation]. + final bool bezierBackgroundAnimation; + + /// Bezier background bounce. + /// See [BezierBackground.bounce]. + final bool bezierBackgroundBounce; + + final Widget child; + + const MaterialHeader2({ + this.key, + double triggerOffset = 100, + bool clamping = true, + IndicatorPosition position = IndicatorPosition.above, + Duration processedDuration = const Duration(milliseconds: 200), + physics.SpringDescription? spring, + bool springRebound = false, + SpringBuilder? readySpringBuilder, + FrictionFactor? frictionFactor, + bool safeArea = true, + double? infiniteOffset, + bool? hitOver, + bool? infiniteHitOver, + bool hapticFeedback = false, + bool triggerWhenRelease = false, + double maxOverOffset = double.infinity, + required this.child, + this.backgroundColor, + this.color, + this.valueColor, + this.semanticsLabel, + this.semanticsValue, + this.noMoreIcon, + this.showBezierBackground = false, + this.bezierBackgroundColor, + this.bezierBackgroundAnimation = false, + this.bezierBackgroundBounce = false, + }) : super( + triggerOffset: triggerOffset, + clamping: clamping, + processedDuration: processedDuration, + spring: spring, + readySpringBuilder: readySpringBuilder ?? + (bezierBackgroundAnimation + ? kBezierSpringBuilder + : kMaterialSpringBuilder), + springRebound: springRebound, + frictionFactor: frictionFactor ?? + (showBezierBackground + ? kBezierFrictionFactor + : kMaterialFrictionFactor), + horizontalFrictionFactor: frictionFactor ?? + (showBezierBackground + ? kBezierHorizontalFrictionFactor + : kMaterialHorizontalFrictionFactor), + safeArea: safeArea, + infiniteOffset: infiniteOffset, + hitOver: hitOver, + infiniteHitOver: infiniteHitOver, + position: position, + hapticFeedback: hapticFeedback, + triggerWhenRelease: triggerWhenRelease, + maxOverOffset: maxOverOffset, + ); + + @override + Widget build(BuildContext context, IndicatorState state) { + return _MaterialIndicator( + key: key, + state: state, + disappearDuration: processedDuration, + reverse: state.reverse, + backgroundColor: backgroundColor, + color: color, + valueColor: valueColor, + semanticsLabel: semanticsLabel, + semanticsValue: semanticsValue, + noMoreIcon: noMoreIcon, + showBezierBackground: showBezierBackground, + bezierBackgroundColor: bezierBackgroundColor, + bezierBackgroundAnimation: bezierBackgroundAnimation, + bezierBackgroundBounce: bezierBackgroundBounce, + child: child, + ); + } +} + +class MaterialFooter2 extends Footer { + final Key? key; + + /// See [ProgressIndicator.backgroundColor]. + final Color? backgroundColor; + + /// See [ProgressIndicator.color]. + final Color? color; + + /// See [ProgressIndicator.valueColor]. + final Animation? valueColor; + + /// See [ProgressIndicator.semanticsLabel]. + final String? semanticsLabel; + + /// See [ProgressIndicator.semanticsLabel]. + final String? semanticsValue; + + /// Icon when [IndicatorResult.noMore]. + final Widget? noMoreIcon; + + /// Show bezier background. + final bool showBezierBackground; + + /// Bezier background color. + /// See [BezierBackground.color]. + final Color? bezierBackgroundColor; + + /// Bezier background animation. + /// See [BezierBackground.useAnimation]. + final bool bezierBackgroundAnimation; + + /// Bezier background bounce. + /// See [BezierBackground.bounce]. + final bool bezierBackgroundBounce; + final Widget child; + const MaterialFooter2({ + this.key, + double triggerOffset = 100, + bool clamping = true, + IndicatorPosition position = IndicatorPosition.above, + Duration processedDuration = const Duration(milliseconds: 200), + physics.SpringDescription? spring, + SpringBuilder? readySpringBuilder, + bool springRebound = false, + FrictionFactor? frictionFactor, + bool safeArea = true, + double? infiniteOffset, + bool? hitOver, + bool? infiniteHitOver, + bool hapticFeedback = false, + bool triggerWhenRelease = false, + double maxOverOffset = double.infinity, + required this.child, + this.backgroundColor, + this.color, + this.valueColor, + this.semanticsLabel, + this.semanticsValue, + this.noMoreIcon, + this.showBezierBackground = false, + this.bezierBackgroundColor, + this.bezierBackgroundAnimation = false, + this.bezierBackgroundBounce = false, + }) : super( + triggerOffset: triggerOffset, + clamping: clamping, + processedDuration: processedDuration, + spring: spring, + readySpringBuilder: readySpringBuilder ?? + (bezierBackgroundAnimation + ? kBezierSpringBuilder + : kMaterialSpringBuilder), + springRebound: springRebound, + frictionFactor: frictionFactor ?? + (showBezierBackground + ? kBezierFrictionFactor + : kMaterialFrictionFactor), + horizontalFrictionFactor: frictionFactor ?? + (showBezierBackground + ? kBezierHorizontalFrictionFactor + : kMaterialHorizontalFrictionFactor), + safeArea: safeArea, + infiniteOffset: infiniteOffset, + hitOver: hitOver, + infiniteHitOver: infiniteHitOver, + position: position, + hapticFeedback: hapticFeedback, + triggerWhenRelease: triggerWhenRelease, + maxOverOffset: maxOverOffset, + ); + + @override + Widget build(BuildContext context, IndicatorState state) { + return _MaterialIndicator( + key: key, + state: state, + disappearDuration: processedDuration, + reverse: !state.reverse, + backgroundColor: backgroundColor, + color: color, + valueColor: valueColor, + semanticsLabel: semanticsLabel, + semanticsValue: semanticsValue, + noMoreIcon: noMoreIcon, + showBezierBackground: showBezierBackground, + bezierBackgroundColor: bezierBackgroundColor, + bezierBackgroundAnimation: bezierBackgroundAnimation, + bezierBackgroundBounce: bezierBackgroundBounce, + child: child, + ); + } +} + +/// Material indicator. +/// Base widget for [MaterialHeader] and [MaterialFooter]. +class _MaterialIndicator extends StatefulWidget { + /// Indicator properties and state. + final IndicatorState state; + + /// See [ProgressIndicator.backgroundColor]. + final Color? backgroundColor; + + /// See [ProgressIndicator.color]. + final Color? color; + + /// See [ProgressIndicator.valueColor]. + final Animation? valueColor; + + /// See [ProgressIndicator.semanticsLabel]. + final String? semanticsLabel; + + /// See [ProgressIndicator.semanticsLabel]. + final String? semanticsValue; + + /// Indicator disappears duration. + /// When the mode is [IndicatorMode.processed]. + final Duration disappearDuration; + + /// True for up and left. + /// False for down and right. + final bool reverse; + + /// Icon when [IndicatorResult.noMore]. + final Widget? noMoreIcon; + + /// Show bezier background. + final bool showBezierBackground; + + /// Bezier background color. + /// See [BezierBackground.color]. + final Color? bezierBackgroundColor; + + /// Bezier background animation. + /// See [BezierBackground.useAnimation]. + final bool bezierBackgroundAnimation; + + /// Bezier background bounce. + /// See [BezierBackground.bounce]. + final bool bezierBackgroundBounce; + + final Widget child; + + const _MaterialIndicator({ + Key? key, + required this.state, + required this.disappearDuration, + required this.reverse, + required this.child, + this.backgroundColor, + this.color, + this.valueColor, + this.semanticsLabel, + this.semanticsValue, + this.noMoreIcon, + this.showBezierBackground = false, + this.bezierBackgroundColor, + this.bezierBackgroundAnimation = false, + this.bezierBackgroundBounce = false, + }) : super(key: key); + + @override + State<_MaterialIndicator> createState() => _MaterialIndicatorState(); +} + +/// See [ProgressIndicator] _kMinCircularProgressIndicatorSize. +const double _kCircularProgressIndicatorSize = 48; + +/// Friction factor used by material. +double kMaterialFrictionFactor(double overscrollFraction) => + 0.875 * math.pow(1 - overscrollFraction, 2); + +/// Friction factor used by material horizontal. +double kMaterialHorizontalFrictionFactor(double overscrollFraction) => + 1.0 * math.pow(1 - overscrollFraction, 2); + +/// Spring description used by material. +physics.SpringDescription kMaterialSpringBuilder({ + required IndicatorMode mode, + required double offset, + required double actualTriggerOffset, + required double velocity, +}) => + physics.SpringDescription.withDampingRatio( + mass: 1, + stiffness: 500, + ratio: 1.1, + ); + +class _MaterialIndicatorState extends State<_MaterialIndicator> { + IndicatorMode get _mode => widget.state.mode; + + IndicatorResult get _result => widget.state.result; + + Axis get _axis => widget.state.axis; + + double get _offset => widget.state.offset; + + double get _actualTriggerOffset => widget.state.actualTriggerOffset; + + /// Build [RefreshProgressIndicator]. + Widget _buildIndicator() { + return Container( + alignment: _axis == Axis.vertical + ? (widget.reverse ? Alignment.topCenter : Alignment.bottomCenter) + : (widget.reverse ? Alignment.centerLeft : Alignment.centerRight), + height: _axis == Axis.vertical ? _actualTriggerOffset : double.infinity, + width: _axis == Axis.horizontal ? _actualTriggerOffset : double.infinity, + child: Stack( + alignment: Alignment.center, + children: [ + widget.child, + if (_mode == IndicatorMode.inactive && + _result == IndicatorResult.noMore) + widget.noMoreIcon ?? const Icon(Icons.inbox_outlined), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + double offset = _offset; + if (widget.state.indicator.infiniteOffset != null && + widget.state.indicator.position == IndicatorPosition.locator && + (_mode != IndicatorMode.inactive || + _result == IndicatorResult.noMore)) { + offset = _actualTriggerOffset; + } + final padding = math.max(_offset - _kCircularProgressIndicatorSize, 0) / 2; + return Stack( + clipBehavior: Clip.none, + children: [ + SizedBox( + width: _axis == Axis.vertical ? double.infinity : offset, + height: _axis == Axis.horizontal ? double.infinity : offset, + ), + if (widget.showBezierBackground) + Positioned( + top: _axis == Axis.vertical + ? widget.reverse + ? null + : 0 + : 0, + left: _axis == Axis.horizontal + ? widget.reverse + ? null + : 0 + : 0, + right: _axis == Axis.horizontal + ? widget.reverse + ? 0 + : null + : 0, + bottom: _axis == Axis.vertical + ? widget.reverse + ? 0 + : null + : 0, + child: BezierBackground( + state: widget.state, + color: widget.bezierBackgroundColor, + useAnimation: widget.bezierBackgroundAnimation, + bounce: widget.bezierBackgroundBounce, + reverse: widget.reverse, + ), + ), + Positioned( + top: _axis == Axis.vertical + ? widget.reverse + ? padding + : null + : 0, + bottom: _axis == Axis.vertical + ? widget.reverse + ? null + : padding + : 0, + left: _axis == Axis.horizontal + ? widget.reverse + ? padding + : null + : 0, + right: _axis == Axis.horizontal + ? widget.reverse + ? null + : padding + : 0, + child: Center( + child: _buildIndicator(), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/keep_alive_wrapper.dart b/lib/widgets/keep_alive_wrapper.dart new file mode 100644 index 0000000..1b7d143 --- /dev/null +++ b/lib/widgets/keep_alive_wrapper.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class KeepAliveWrapper extends StatefulWidget { + final Widget child; + + const KeepAliveWrapper({Key? key, required this.child}) : super(key: key); + + @override + State createState() => _KeepAliveWrapperState(); +} + +class _KeepAliveWrapperState extends State + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/loadding.dart b/lib/widgets/loadding.dart new file mode 100644 index 0000000..990d60a --- /dev/null +++ b/lib/widgets/loadding.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; + +class LoaddingWidget extends StatelessWidget { + const LoaddingWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + padding: AppStyle.edgeInsetsA24, + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + ), + child: const CircularProgressIndicator(), + ), + ); + } +} diff --git a/lib/widgets/local_image.dart b/lib/widgets/local_image.dart new file mode 100644 index 0000000..39514af --- /dev/null +++ b/lib/widgets/local_image.dart @@ -0,0 +1,69 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class LocalImage extends StatelessWidget { + final String path; + final double? width; + final double? height; + final BoxFit? fit; + final double borderRadius; + final bool progress; + const LocalImage(this.path, + {this.width, + this.height, + this.fit = BoxFit.cover, + this.borderRadius = 0, + this.progress = false, + Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (path.isEmpty) { + return Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.1), + ), + child: const Icon( + Icons.image, + color: Colors.grey, + size: 24, + ), + ); + } + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: FutureBuilder( + future: File(path).readAsBytes(), + builder: (_, snap) { + if (snap.hasError) { + return Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.1), + ), + child: const Icon( + Icons.broken_image, + color: Colors.grey, + size: 24, + ), + ); + } + if (!snap.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + return Image.memory( + snap.data as Uint8List, + fit: fit, + height: height, + width: width, + ); + }, + ), + ); + } +} diff --git a/lib/widgets/net_image.dart b/lib/widgets/net_image.dart new file mode 100644 index 0000000..032dbc4 --- /dev/null +++ b/lib/widgets/net_image.dart @@ -0,0 +1,137 @@ +import 'package:extended_image/extended_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; + +class NetImage extends StatefulWidget { + final String picUrl; + final double? width; + final double? height; + final BoxFit? fit; + final double borderRadius; + final bool progress; + const NetImage(this.picUrl, + {this.width, + this.height, + this.fit = BoxFit.cover, + this.borderRadius = 0, + this.progress = false, + Key? key}) + : super(key: key); + + @override + State createState() => _NetImageState(); +} + +class _NetImageState extends State + with SingleTickerProviderStateMixin { + late AnimationController animationController; + + @override + void initState() { + animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 400), + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var picUrl = widget.picUrl; + + if (picUrl.isEmpty) { + return Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.1), + ), + child: const Icon( + Icons.image, + color: Colors.grey, + size: 24, + ), + ); + } + return ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius), + child: ExtendedImage.network( + picUrl, + fit: widget.fit, + height: widget.height, + width: widget.width, + shape: BoxShape.rectangle, + handleLoadingProgress: widget.progress, + borderRadius: BorderRadius.circular(widget.borderRadius), + headers: const {'Referer': "http://www.zaimanhua.com/"}, + loadStateChanged: (e) { + if (e.extendedImageLoadState == LoadState.loading) { + animationController.reset(); + final double? progress = + e.loadingProgress?.expectedTotalBytes != null + ? e.loadingProgress!.cumulativeBytesLoaded / + e.loadingProgress!.expectedTotalBytes! + : null; + if (widget.progress) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + value: progress, + ), + AppStyle.vGap4, + Text( + '${((progress ?? 0.0) * 100).toInt()}%', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12), + ), + ], + ), + ); + } + return Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.1), + ), + child: const Icon( + Icons.image, + color: Colors.grey, + size: 24, + ), + ); + } + if (e.extendedImageLoadState == LoadState.failed) { + animationController.reset(); + return Container( + decoration: BoxDecoration( + color: Colors.grey.withOpacity(.1), + ), + child: const Icon( + Icons.broken_image, + color: Colors.grey, + size: 24, + ), + ); + } + if (e.extendedImageLoadState == LoadState.completed) { + if (e.wasSynchronouslyLoaded) { + return e.completedWidget; + } + animationController.forward(); + + return FadeTransition( + opacity: animationController, + child: e.completedWidget, + ); + } + return null; + }, + ), + ); + } + + @override + void dispose() { + animationController.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/page_grid_view.dart b/lib/widgets/page_grid_view.dart new file mode 100644 index 0000000..ef9b26f --- /dev/null +++ b/lib/widgets/page_grid_view.dart @@ -0,0 +1,83 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; + +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + +import 'package:get/get.dart'; + +class PageGridView extends StatelessWidget { + final BasePageController pageController; + final IndexedWidgetBuilder itemBuilder; + final EdgeInsets? padding; + final bool firstRefresh; + final Function()? onLoginSuccess; + final bool showPageLoadding; + final double crossAxisSpacing, mainAxisSpacing; + final int crossAxisCount; + final bool loadMore; + const PageGridView({ + required this.itemBuilder, + required this.pageController, + this.padding, + this.firstRefresh = false, + this.showPageLoadding = false, + this.onLoginSuccess, + this.crossAxisSpacing = 0.0, + this.mainAxisSpacing = 0.0, + required this.crossAxisCount, + this.loadMore = true, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => Stack( + children: [ + EasyRefresh( + header: const MaterialHeader(), + footer: loadMore + ? const MaterialFooter( + clamping: false, infiniteOffset: 70, triggerOffset: 70) + : null, + controller: pageController.easyRefreshController, + refreshOnStart: firstRefresh, + onLoad: loadMore ? pageController.loadData : null, + onRefresh: pageController.refreshData, + child: MasonryGridView.count( + padding: padding ?? EdgeInsets.zero, + controller: pageController.scrollController, + itemCount: pageController.list.length, + itemBuilder: itemBuilder, + crossAxisCount: crossAxisCount, + crossAxisSpacing: crossAxisSpacing, + mainAxisSpacing: mainAxisSpacing, + ), + ), + Offstage( + offstage: !pageController.pageEmpty.value, + child: AppEmptyWidget( + onRefresh: () => pageController.refreshData(), + ), + ), + Offstage( + offstage: !(showPageLoadding && pageController.pageLoadding.value), + child: const AppLoaddingWidget(), + ), + Offstage( + offstage: !pageController.pageError.value, + child: AppErrorWidget( + errorMsg: pageController.errorMsg.value, + error: pageController.error, + onRefresh: () => pageController.refreshData(), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/page_list_view.dart b/lib/widgets/page_list_view.dart new file mode 100644 index 0000000..b9c0403 --- /dev/null +++ b/lib/widgets/page_list_view.dart @@ -0,0 +1,97 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/controller/base_controller.dart'; +import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_error_widget.dart'; +import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart'; + +import 'package:get/get.dart'; + +typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index); + +class PageListView extends StatelessWidget { + final BasePageController pageController; + final IndexedWidgetBuilder itemBuilder; + final IndexedWidgetBuilder? separatorBuilder; + final EdgeInsets? padding; + final bool firstRefresh; + final Function()? onLoginSuccess; + final bool showPageLoadding; + final bool loadMore; + final Widget? header; + const PageListView({ + required this.itemBuilder, + required this.pageController, + this.padding, + this.firstRefresh = false, + this.showPageLoadding = false, + this.separatorBuilder, + this.onLoginSuccess, + this.loadMore = true, + this.header, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => Stack( + children: [ + EasyRefresh( + header: const MaterialHeader(), + footer: loadMore + ? const MaterialFooter( + clamping: false, infiniteOffset: 70, triggerOffset: 70) + : null, + controller: pageController.easyRefreshController, + refreshOnStart: firstRefresh, + onLoad: loadMore ? pageController.loadData : null, + onRefresh: pageController.refreshData, + child: ListView.separated( + padding: padding ?? EdgeInsets.zero, + controller: pageController.scrollController, + itemCount: header == null + ? pageController.list.length + : pageController.list.length + 1, + itemBuilder: header == null + ? itemBuilder + : (context, index) { + if (index == 0) { + return header; + } + return itemBuilder.call(context, index - 1); + }, + separatorBuilder: header == null + ? (separatorBuilder ?? (context, i) => const SizedBox()) + : (context, index) { + if (index == 0) { + return const SizedBox(); + } + return separatorBuilder?.call(context, index - 1) ?? + const SizedBox(); + }, + ), + ), + Offstage( + offstage: !pageController.pageEmpty.value, + child: AppEmptyWidget( + onRefresh: () => pageController.refreshData(), + ), + ), + Offstage( + offstage: !(showPageLoadding && pageController.pageLoadding.value), + child: const AppLoaddingWidget(), + ), + Offstage( + offstage: !pageController.pageError.value, + child: AppErrorWidget( + errorMsg: pageController.errorMsg.value, + error: pageController.error, + onRefresh: () => pageController.refreshData(), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/refresh_until_widget.dart b/lib/widgets/refresh_until_widget.dart new file mode 100644 index 0000000..c8b880e --- /dev/null +++ b/lib/widgets/refresh_until_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:remixicon/remixicon.dart'; + +/// 一个加载图标会旋转的加载按钮。加载图标([Remix.refresh_line])在左,文字([text])在 +/// 右。 +/// +/// 在点击widget时会在执行[onRefresh]函数的同时旋转加载图标。加载图标会一直旋转直到该函数 +/// 返还。 +/// +/// 加载图标会旋转不小于1秒的时间,即如果[onRefresh]函数在1秒之内执行完毕,加载图标会继续旋 +/// 转直到距离onRefresh函数开始执行已经过了1秒。 +class RefreshUntilWidget extends StatefulWidget { + final Future Function() onRefresh; + final String text; + + const RefreshUntilWidget({ + super.key, + required this.onRefresh, + required this.text, + }); + + @override + State createState() => _RefreshUntilWidgetState(); +} + +class _RefreshUntilWidgetState extends State + with TickerProviderStateMixin { + late final AnimationController _controller = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + ); + late final Animation _animation = CurvedAnimation( + parent: _controller, + curve: Curves.linear, + ); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () async { + _controller.repeat(); + // 确保在网络很好的情况下,动画不会太快结束(至少1秒) + await Future.wait([ + widget.onRefresh(), + Future.delayed(const Duration(seconds: 1)), + ]); + _controller.stop(canceled: false); + }, + child: Row( + children: [ + RotationTransition( + turns: _animation, + child: const Icon(Remix.refresh_line, size: 18, color: Colors.grey), + ), + AppStyle.hGap4, + Text( + widget.text, + style: const TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/shadow_card.dart b/lib/widgets/shadow_card.dart new file mode 100644 index 0000000..ce45ab1 --- /dev/null +++ b/lib/widgets/shadow_card.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:get/get.dart'; + +class ShadowCard extends StatelessWidget { + final Widget child; + final double radius; + final Function()? onTap; + final Function()? onLongPress; + const ShadowCard({ + required this.child, + this.radius = 8.0, + this.onTap, + this.onLongPress, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + boxShadow: Get.isDarkMode + ? [] + : [ + BoxShadow( + blurRadius: 4, + color: Colors.grey.withOpacity(.2), + ) + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: Material( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(radius), + child: InkWell( + borderRadius: BorderRadius.circular(radius), + onTap: onTap, + onLongPress: onLongPress, + child: Container( + decoration: BoxDecoration( + borderRadius: AppStyle.radius8, + ), + child: child, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/status/app_empty_widget.dart b/lib/widgets/status/app_empty_widget.dart new file mode 100644 index 0000000..e075190 --- /dev/null +++ b/lib/widgets/status/app_empty_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:lottie/lottie.dart'; + +class AppEmptyWidget extends StatelessWidget { + final Function()? onRefresh; + const AppEmptyWidget({this.onRefresh, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: GestureDetector( + onTap: () { + onRefresh?.call(); + }, + child: Padding( + padding: AppStyle.edgeInsetsA12, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LottieBuilder.asset( + 'assets/lotties/empty.json', + width: 200, + height: 200, + repeat: false, + ), + const Text( + "这里什么都没有", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/status/app_error_widget.dart b/lib/widgets/status/app_error_widget.dart new file mode 100644 index 0000000..a95d107 --- /dev/null +++ b/lib/widgets/status/app_error_widget.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/app/utils.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +import 'package:lottie/lottie.dart'; + +class AppErrorWidget extends StatelessWidget { + final Function()? onRefresh; + final String errorMsg; + final Error? error; + const AppErrorWidget( + {this.errorMsg = "", this.onRefresh, this.error, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: GestureDetector( + onTap: () { + onRefresh?.call(); + }, + child: Padding( + padding: AppStyle.edgeInsetsA12, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LottieBuilder.asset( + 'assets/lotties/error.json', + width: 260, + repeat: false, + ), + Text( + "$errorMsg\r\n点击刷新", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + Visibility( + visible: error != null, + child: Padding( + padding: AppStyle.edgeInsetsT12, + child: OutlinedButton( + style: OutlinedButton.styleFrom( + textStyle: Get.textTheme.bodySmall, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + Utils.copyText( + "$errorMsg\n${error?.stackTrace?.toString()}"); + SmartDialog.showToast("已复制详细信息"); + }, + child: const Text("复制详细信息"), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/status/app_loadding_widget.dart b/lib/widgets/status/app_loadding_widget.dart new file mode 100644 index 0000000..3789852 --- /dev/null +++ b/lib/widgets/status/app_loadding_widget.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:lottie/lottie.dart'; + +class AppLoaddingWidget extends StatelessWidget { + const AppLoaddingWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: AppStyle.edgeInsetsA12, + child: LottieBuilder.asset( + 'assets/lotties/loadding.json', + width: 200, + ), + ), + ); + } +} diff --git a/lib/widgets/tab_appbar.dart b/lib/widgets/tab_appbar.dart new file mode 100644 index 0000000..a138bf9 --- /dev/null +++ b/lib/widgets/tab_appbar.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:get/get.dart'; + +class TabAppBar extends StatelessWidget implements PreferredSizeWidget { + final List tabs; + final TabController? controller; + final Widget? action; + const TabAppBar({required this.tabs, this.controller, this.action, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: Get.isDarkMode + ? SystemUiOverlayStyle.light.copyWith( + systemNavigationBarColor: Colors.transparent, + ) + : SystemUiOverlayStyle.dark.copyWith( + systemNavigationBarColor: Colors.transparent, + ), + child: Container( + padding: + EdgeInsets.only(top: MediaQuery.of(context).padding.top, right: 4), + height: 56 + MediaQuery.of(context).padding.top, + child: Row( + children: [ + Expanded( + child: TabBar( + isScrollable: true, + controller: controller, + labelColor: Theme.of(context).colorScheme.primary, + tabAlignment: TabAlignment.start, + unselectedLabelColor: + Get.isDarkMode ? Colors.white70 : Colors.black87, + labelStyle: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + labelPadding: AppStyle.edgeInsetsH12, + indicatorSize: TabBarIndicatorSize.tab, + indicatorColor: Colors.transparent, + dividerColor: Colors.transparent, + tabs: tabs, + ), + ), + action ?? const SizedBox(), + ], + ), + ), + ); + } + + @override + Size get preferredSize => Size.fromHeight(56 + AppStyle.statusBarHeight); +} diff --git a/lib/widgets/user_photo.dart b/lib/widgets/user_photo.dart new file mode 100644 index 0000000..f5ec5bd --- /dev/null +++ b/lib/widgets/user_photo.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dmzj/app/app_style.dart'; +import 'package:flutter_dmzj/widgets/net_image.dart'; +import 'package:remixicon/remixicon.dart'; + +class UserPhoto extends StatelessWidget { + final String? url; + final bool showBoder; + final double size; + const UserPhoto({ + this.url, + this.showBoder = true, + this.size = 48, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (url == null || (url?.isEmpty ?? true)) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + border: showBoder + ? Border.all( + color: Colors.grey.withOpacity(.2), + ) + : null, + color: Colors.grey.withOpacity(.2), + borderRadius: AppStyle.radius32, + ), + child: const Icon( + Remix.user_fill, + color: Colors.white, + size: 24, + ), + ); + } + return Container( + width: size, + height: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(56), + border: showBoder + ? Border.all( + color: Colors.grey.withOpacity(.2), + ) + : null, + ), + child: NetImage( + url!, + width: size, + height: size, + borderRadius: size, + ), + ); + } +} diff --git a/lib/widgets/windows_tab_page.dart b/lib/widgets/windows_tab_page.dart new file mode 100644 index 0000000..1fc17aa --- /dev/null +++ b/lib/widgets/windows_tab_page.dart @@ -0,0 +1,144 @@ +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/material.dart'; + +/// Windows平台专用的标签页容器,使用Fluent UI的TabView样式 +/// 替代移动端的Scaffold + TabAppBar + TabBarView组合 +class WindowsTabPage extends StatefulWidget { + final List tabs; + final int initialIndex; + final Widget? headerAction; + final ValueChanged? onTabChanged; + + const WindowsTabPage({ + Key? key, + required this.tabs, + this.initialIndex = 0, + this.headerAction, + this.onTabChanged, + }) : super(key: key); + + @override + State createState() => _WindowsTabPageState(); +} + +class _WindowsTabPageState extends State { + late int _currentIndex; + + @override + void initState() { + super.initState(); + _currentIndex = widget.initialIndex; + } + + @override + Widget build(BuildContext context) { + // 使用maybeOf避免没有FluentTheme祖先时抛出异常 + final fluentTheme = + fluent.FluentTheme.maybeOf(context) ?? fluent.FluentThemeData(); + final materialTheme = Theme.of(context); + final isDark = materialTheme.brightness == Brightness.dark; + final tabBarBg = isDark ? const Color(0xff202020) : const Color(0xfff0f0f0); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Fluent 样式标签栏 + Container( + color: tabBarBg, + child: Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(widget.tabs.length, (i) { + final selected = i == _currentIndex; + return _TabButton( + label: widget.tabs[i].label, + selected: selected, + onTap: () { + setState(() => _currentIndex = i); + widget.onTabChanged?.call(i); + }, + ); + }), + ), + ), + ), + if (widget.headerAction != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: widget.headerAction!, + ), + ], + ), + ), + // 分隔线 + Divider( + height: 1, + thickness: 1, + color: materialTheme.dividerColor, + ), + // 内容区 + Expanded( + child: IndexedStack( + index: _currentIndex, + children: widget.tabs.map((t) => t.body).toList(), + ), + ), + ], + ); + } +} + +class WindowsTabItem { + final String label; + final Widget body; + const WindowsTabItem({required this.label, required this.body}); +} + +/// 单个标签按钮,Fluent Pivot样式 +class _TabButton extends StatelessWidget { + final String label; + final bool selected; + final VoidCallback onTap; + + const _TabButton({ + required this.label, + required this.selected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final fluentTheme = + fluent.FluentTheme.maybeOf(context) ?? fluent.FluentThemeData(); + final accent = fluentTheme.accentColor; + final textColor = selected + ? accent + : fluentTheme.resources.textFillColorSecondary; + + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: selected ? accent : Colors.transparent, + width: 2, + ), + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: selected ? 18 : 15, + fontWeight: selected ? FontWeight.bold : FontWeight.normal, + color: textColor, + ), + ), + ), + ); + } +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..1998db3 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_dmzj") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.xycz.zmhx") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..36af2d7 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..f308ccc --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color + file_selector_linux + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 0000000..e56f22e --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_dmzj"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "flutter_dmzj"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml new file mode 100644 index 0000000..da71197 --- /dev/null +++ b/linux/packaging/deb/make_config.yaml @@ -0,0 +1,21 @@ +display_name: 动漫之家 +package_name: dmzjx +maintainer: + name: xiaoyaocz + email: xiaoyaocz@52uwp.com +priority: optional +section: x11 +installed_size: 24400 +essential: false +icon: assets/images/logo.png + +keywords: + - 动漫之家 + - DMZJ + +generic_name: 动漫之家 + +categories: + - Media + +startup_notify: true \ No newline at end of file diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..049b9eb --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,28 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import battery_plus +import connectivity_plus +import dynamic_color +import file_selector_macos +import package_info_plus +import path_provider_foundation +import share_plus +import url_launcher_macos +import webview_flutter_wkwebview + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..049abe2 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..e5a4c56 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,60 @@ +PODS: + - battery_plus (0.0.1): + - FlutterMacOS + - connectivity_plus (0.0.1): + - FlutterMacOS + - ReachabilitySwift + - FlutterMacOS (1.0.0) + - package_info_plus (0.0.1): + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - ReachabilitySwift (5.0.0) + - share_plus (0.0.1): + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - battery_plus (from `Flutter/ephemeral/.symlinks/plugins/battery_plus/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +SPEC REPOS: + trunk: + - ReachabilitySwift + +EXTERNAL SOURCES: + battery_plus: + :path: Flutter/ephemeral/.symlinks/plugins/battery_plus/macos + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + battery_plus: 906cd081df7f2274f5235581515b59a628dcaec7 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 + +PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 + +COCOAPODS: 1.11.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3df8bc7 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,641 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 8362B6BD20D827D43827959E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08BF592266B41306EE8B4A5D /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 08BF592266B41306EE8B4A5D /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* ZAI-X.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ZAI-X.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7F70FB95E4E95C9588662517 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 8C1EEC1FA8142C33E7510B52 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 93C9635D35AB565747B6A956 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8362B6BD20D827D43827959E /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + DDE7347FEE90AFC5158BA519 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* ZAI-X.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 08BF592266B41306EE8B4A5D /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + DDE7347FEE90AFC5158BA519 /* Pods */ = { + isa = PBXGroup; + children = ( + 7F70FB95E4E95C9588662517 /* Pods-Runner.debug.xcconfig */, + 93C9635D35AB565747B6A956 /* Pods-Runner.release.xcconfig */, + 8C1EEC1FA8142C33E7510B52 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 22A9C47AA19FE174307D4E17 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 86558A4AA97788AA39A460FC /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* ZAI-X.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 22A9C47AA19FE174307D4E17 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 86558A4AA97788AA39A460FC /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9R87RMF9C9; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "ZAI-X"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_NAME = "ZAI-X"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9R87RMF9C9; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "ZAI-X"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_NAME = "ZAI-X"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = 9R87RMF9C9; + INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "ZAI-X"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_NAME = "ZAI-X"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..ede85de --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..d53ef64 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..1a60158 --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "info": { + "version": 1, + "author": "icon.wuruihong.com" + }, + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..ae6bee0 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..e18c13f Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..ed2e9a9 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..a37a275 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..dbf1fd6 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..2ea83cc Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..61ebf5c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128.png new file mode 100644 index 0000000..ec18cbd Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png new file mode 100644 index 0000000..098db8e Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-128@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16.png new file mode 100644 index 0000000..b2559c8 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png new file mode 100644 index 0000000..9da597c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-16@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256.png new file mode 100644 index 0000000..098db8e Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png new file mode 100644 index 0000000..82ccac8 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-256@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32.png new file mode 100644 index 0000000..9da597c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png new file mode 100644 index 0000000..77e2a3a Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-32@2x.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512.png new file mode 100644 index 0000000..82ccac8 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png new file mode 100644 index 0000000..bcf2d64 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/icon-512@2x.png differ diff --git a/macos/Runner/Assets.xcassets/Contents.json b/macos/Runner/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/macos/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Runner/Assets.xcassets/Image.imageset/Contents.json b/macos/Runner/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 0000000..a19a549 --- /dev/null +++ b/macos/Runner/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..6c40e25 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_dmzj + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.xycz.zmhx + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..a900fa6 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..2722837 --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..7deefe1 --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + + + diff --git a/macos/packaging/dmg/make_config.yaml b/macos/packaging/dmg/make_config.yaml new file mode 100644 index 0000000..501b23d --- /dev/null +++ b/macos/packaging/dmg/make_config.yaml @@ -0,0 +1,10 @@ +title: 动漫之家 +contents: + - x: 448 + y: 344 + type: link + path: "/Applications" + - x: 192 + y: 344 + type: file + path: 动漫之家.app diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..c06e5bf --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1391 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.4.1" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.13.0" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: ba605aeafd6609cb5f8020c609a51941803a5fb2b6a7576f7c7eeeb52d29e750 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.3" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.12.4" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.4" + console: + dependency: transitive + description: + name: console + sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.3+8" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" + crypton: + dependency: "direct main" + description: + name: crypton + sha256: "17b6631fbf89e389d421b46629132287ed37d601b2ad1357445826ab85022271" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + csslib: + dependency: "direct main" + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.6" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.12" + dio: + dependency: "direct main" + description: + name: dio + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.9.1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "0a2e95fc6bdeb623bb623fc41e90e6924e9a3bbd65089f9221f83c185366b479" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.1" + easy_refresh: + dependency: "direct main" + description: + name: easy_refresh + sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.0" + extended_image: + dependency: "direct main" + description: + name: extended_image + sha256: "85199f9233e03abc2ce2e68cbb2991648666af4a527ae4e6250935be8edfddae" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.1.0" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: e61dafd94400fff6ef7ed1523d445ff3af137f198f3228e4a3107bc5b4bec5d1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: bd15e43e9268db636b53eeaca9f56324d1622af30e5c34d6e267649758c84d9a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "51e8fd0446de75e4b62c065b76db2210c704562d072339d333bd89c57a7f8a7c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.2+4" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "628ec99afd8bb40620b4c8707d5fd5fc9e89d83e9b0b327d471fe5f7bc5fc33f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.3+4" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: c0f025d460de3301b7bbbf837fc8d0759df85f182c635f1dd94934b4cdc92352 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.3" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.3+5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + fluent_ui: + dependency: "direct main" + description: + name: fluent_ui + sha256: "3fe3b66351e0c47b778e9778c4c1b9c9086515d0d61ea9be3010a9de23cdf28b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.14.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_smart_dialog: + dependency: "direct main" + description: + name: flutter_smart_dialog + sha256: "0852df132cb03fd8fc5144eb404c31eb7eb50c22aecb1cc2504f2f598090d756" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.9.8+9" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + flutter_swiper_view: + dependency: "direct main" + description: + name: flutter_swiper_view + sha256: "2a165b259e8a4c49d4da5626b967ed42a73dac2d075bd9e266ad8d23b9f01879" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.8" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_widget_from_html_core: + dependency: "direct main" + description: + name: flutter_widget_from_html_core + sha256: b1048fd119a14762e2361bd057da608148a895477846d6149109b2151d2f7abf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.2" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + get: + dependency: "direct main" + description: + name: get + sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.6" + get_it: + dependency: transitive + description: + name: get_it + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.2.1" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.6" + html_unescape: + dependency: "direct main" + description: + name: html_unescape + sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + http: + dependency: transitive + description: + name: http + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.8.0" + image_gallery_saver_plus: + dependency: "direct main" + description: + name: image_gallery_saver_plus + sha256: "199b9e24f8d85e98f11e3d35571ab68ae50626ad40e2bb85c84383f69a6950ad" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.11.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.6.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.3.2" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + math_expressions: + dependency: transitive + description: + name: math_expressions + sha256: "2e1ceb974c2b1893c809a68c7005f1b63f7324db0add800a0e792b1ac8ff9f03" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.6" + msix: + dependency: "direct dev" + description: + name: msix + sha256: b6b08e7a7b5d1845f2b1d31216d5b1fb558e98251efefe54eb79ed00d27bc2ac + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.16.13" + multi_split_view: + dependency: "direct main" + description: + name: multi_split_view + sha256: d68e129bff71fc9e6b66de59e1b79deaf4b91f30940130bfbd2d704c1c713499 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + path_drawing: + dependency: transitive + description: + name: path_drawing + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.2" + photo_view: + dependency: "direct main" + description: + name: photo_view + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.14.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.5.0" + preload_page_view: + dependency: "direct main" + description: + name: preload_page_view + sha256: "488a10c158c5c2e9ba9d77e5dbc09b1e49e37a20df2301e5ba02992eac802b7a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.0" + protobuf: + dependency: "direct main" + description: + name: protobuf + sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" + remixicon: + dependency: "direct main" + description: + name: remixicon + sha256: "9a0e6a67d622a1d6ddc5e5cff4898f9711ec8880fc2ebeb720f6e0bc268ea905" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + scroll_pos: + dependency: transitive + description: + name: scroll_pos + sha256: cebf602b2dd939de6832bb902ffefb574608d1b84f420b82b381a4007d3c1e1b + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.8" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.2.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + tab_indicator_styler: + dependency: "direct main" + description: + name: tab_indicator_styler + sha256: "9e7e90367e20f71f3882fc6578fdcced35ab1c66ab20fcb623cdcc20d2796c76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.6" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + universal_html: + dependency: "direct main" + description: + name: universal_html + sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + upower: + dependency: transitive + description: + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.3" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.13" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: "108bd85d0ff20bff1e8b52a040f5c19b6b9fc4a78fdf3160534ff5a11a82e267" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.23.7" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.15.0" + windows_single_instance: + dependency: "direct main" + description: + name: windows_single_instance + sha256: "27ac7ef70a2930a42431d94263941c76884036f797c6d749d1ce5d9d386e66d9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..62eddb2 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,98 @@ +name: flutter_dmzj +version: 1.0.3+10003 +publish_to: none +description: "动漫之家Flutter" +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + #图标ICON + cupertino_icons: ^1.0.5 #Cupertino图标 + remixicon: ^1.0.0 #Remix图标 + + #框架、工具 + get: ^4.6.6 #状态管理 + dio: ^5.4.1 #网络请求 + hive: ^2.2.3 #持久化存储 + hive_flutter: ^1.1.0 #持久化存储 + crypton: ^2.1.0 #加解密 + crypto: ^3.0.2 #加解密 + logger: ^2.0.2+1 #日志输出 + protobuf: ^2.1.0 #Protobuf + intl: any + universal_html: ^2.2.4 #HTML解析 + html_unescape: ^2.0.0 #HTML解码 + csslib: ^1.0.0 #CSS解析 + + #Windows Fluent UI + fluent_ui: ^4.9.0 #Windows Fluent Design UI组件库 + + #Widget + flutter_smart_dialog: ^4.9.6 #各种弹窗 Toast\Dialog\Popup + flutter_staggered_grid_view: ^0.7.0 #瀑布流/GridView + multi_split_view: ^2.4.0 #SplitView + tab_indicator_styler: ^2.0.0 #Tab样式 + extended_image: ^9.0.0 #拓展Image + flutter_swiper_view: ^1.1.8 #幻灯片 + easy_refresh: ^3.4.0 #下拉刷新、上拉加载 + lottie: ^3.0.0 #lottie动画 + photo_view: ^0.14.0 #图片浏览 + preload_page_view: ^0.2.0 #预加载PageView + scrollable_positioned_list: ^0.3.8 + + + #系统交互 + url_launcher: ^6.2.4 #打开链接 + webview_flutter: ^4.7.0 #WebView + package_info_plus: ^5.0.1 #包信息 + flutter_widget_from_html_core: ^0.15.0 #HTML转Widget + share_plus: ^7.2.2 #分享 + permission_handler: ^11.3.0 #权限检查 + path_provider: ^2.1.2 #常用目录 + image_gallery_saver_plus: ^4.0.1 #保存图片至相册 + connectivity_plus: ^5.0.2 #连接状态 + battery_plus: ^5.0.3 #电池状态 + file_selector: ^1.0.3 #文件选择 + windows_single_instance: ^1.0.1 #Windows单实例 + + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + dynamic_color: ^1.8.1 + +dev_dependencies: + flutter_launcher_icons: ^0.13.1 + flutter_lints: ^2.0.0 + build_runner: ^2.3.3 + hive_generator: ^2.0.0 + msix: ^3.9.1 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/lotties/ + - assets/images/ + - assets/statement.txt + +flutter_launcher_icons: + android: true + ios: true + image_path: "assets/images/logo.png" + min_sdk_android: 21 # android min sdk min:16, default 21 + web: + generate: true + image_path: "assets/images/logo.png" + background_color: "#hex_code" + theme_color: "#hex_code" + windows: + generate: true + image_path: "assets/images/logo.png" + icon_size: 48 # min:48, max:256, default: 48 + macos: + generate: true + image_path: "assets/images/logo.png" + + diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..f9d98c2 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_dmzj/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..ed2e9a9 Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..d06170b Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..2ea83cc Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..d06170b Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..2ea83cc Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e5a9a1c --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + flutter_dmzj + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..de40709 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "动漫之家", + "short_name": "动漫之家", + "start_url": ".", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#4196f9", + "description": "动漫之家 Flutter", + "icons": [ + { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..655316c --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_dmzj LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_dmzj") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..406fcc0 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,35 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + BatteryPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowsSingleInstancePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowsSingleInstancePlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..8060b2b --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,31 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + battery_plus + connectivity_plus + dynamic_color + file_selector_windows + permission_handler_windows + share_plus + url_launcher_windows + windows_single_instance +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/packaging/msix/make_config.yaml b/windows/packaging/msix/make_config.yaml new file mode 100644 index 0000000..ff4e190 --- /dev/null +++ b/windows/packaging/msix/make_config.yaml @@ -0,0 +1,7 @@ +display_name: 动漫之家 +publisher_display_name: xiaoyaocz +identity_name: com.xycz.zmhx +logo_path: assets/images/logo.png +capabilities: internetClient +languages: zh-cn +install_certificate: "false" \ No newline at end of file diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..9d450f0 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.xycz" "\0" + VALUE "FileDescription", "flutter_dmzj" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_dmzj" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 com.xycz. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_dmzj.exe" "\0" + VALUE "ProductName", "flutter_dmzj" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..f417da6 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,94 @@ +#include "flutter_window.h" + +#include +#include +#include + +#include "flutter/generated_plugin_registrant.h" + +// Helper: Read AppsUseLightTheme from registry (0 = dark mode, 1 = light mode) +static BOOL IsDarkMode() { + HKEY hKey; + DWORD value = 1; // default: light + DWORD size = sizeof(value); + if (RegOpenKeyExW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + 0, KEY_READ, &hKey) == ERROR_SUCCESS) { + RegQueryValueExW(hKey, L"AppsUseLightTheme", nullptr, nullptr, + reinterpret_cast(&value), &size); + RegCloseKey(hKey); + } + return (value == 0) ? TRUE : FALSE; +} + +// Apply dark/light title bar based on current system setting +static void ApplyDarkTitleBar(HWND hwnd) { + BOOL dark = IsDarkMode(); + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); +} + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + // Apply dark/light title bar to match system theme + ApplyDarkTitleBar(GetHandle()); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + case WM_SETTINGCHANGE: + if (lparam && + wcscmp(reinterpret_cast(lparam), L"ImmersiveColorSet") == 0) { + ApplyDarkTitleBar(hwnd); + } + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..227f62e --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"\u52A8\u6F2B\u4E4B\u5BB6 X", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..900b9bb Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..c977c4a --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..f5bf9fa --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..c10f08d --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..17ba431 --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_