[
  {
    "path": ".gitignore",
    "content": "*.iml\n/.gradle\n/.idea\n.DS_Store\n/local.properties\n/build\n/captures\n/app/release/\ngoogle-services.json\n\nsrc/androidTest/\n/back\n/tool/书源整理工具/bin/*.exe\n/tool/书源整理工具/*.otares\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# 本项目已转到新地址使用kotlin重新开发,项目地址 https://github.com/gedoor/legado\n# 本软件为开源软件,不要在任何地方购买!\n# 关注公众号请搜索:开源阅读,有福利噢\n# 开发\n- 本项目Fork于 https://github.com/ZhangQinhao/MONKOVEL\n\n**代码贡献人员**\n- 大古队员 https://github.com/DaguDuiyuan\n- atbest https://github.com/atbest\n- Antecer https://github.com/Antecer\n- mabDc https://github.com/mabDc\n- 繁体-翻译者:Cello琴弦之間\n\n**其它贡献人员**\n- 图标绘制 群管理员-新奥尔良烤鲟魚堡\n\n# 软件截图\n![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook1.jpg)\n![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook2.jpg)\n![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook3.jpg)\n![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook4.jpg)\n![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook5.jpg)\n![image](https://github.com/gedoor/gedoor.github.io/blob/master/MyBookshelf/image/mybook6.jpg)\n\n# 免责声明（Disclaimer）\n阅读是一款提供网络文学搜索的工具，为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。\n\n当您搜索一本书的时，阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。\n各第三方网站返回的内容与阅读无关，阅读对其概不负责，亦不承担任何法律责任。\n任何通过使用阅读而链接到的第三方网页均系他人制作或提供，您可能从第三方网页上获得其他服务，\n阅读对其合法性概不负责，亦不承担任何法律责任。\n第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读，\n不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。\n您应该对使用搜索引擎的结果自行承担风险。\n\n阅读不做任何形式的保证：不保证第三方搜索引擎的搜索结果满足您的要求，\n不保证搜索服务不中断，不保证搜索结果的安全性、正确性、及时性、合法性。\n因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读，\n阅读不承担任何法律责任。阅读尊重并保护所有使用阅读用户的个人隐私权。\n\n阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费，\n通过专业搜索展示不同网站中网络文学的最新章节。\n阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时，\n也使优秀网络文学得以迅速、更广泛的传播，从而达到了在一定程度促进网络文学充分繁荣发展之目的。\n\n阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商，\n并建议阅读正版图书。\n任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权，\n应该及时向阅读提出书面权力通知，并提供身份证明、权属证明及详细侵权情况证明。\n阅读在收到上述法律文件后，将会依法尽快断开相关链接内容。\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n/src/main/java/com/kunfei/bookshelf/dao/\n"
  },
  {
    "path": "app/ReadMe.md",
    "content": "# ע\r\n MyBookshelf_Keys ѹļС\r\n\r\n google-services.json\r\n gradle.properties\r\n key.properties.jks"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'org.greenrobot.greendao'\napply plugin: 'kotlin-parcelize'\napply plugin: \"de.timfreiheit.resourceplaceholders\"\napply plugin: 'kotlin-kapt'\n\nstatic def releaseTime() {\n    return new Date().format(\"yy.MMddHH\", TimeZone.getTimeZone(\"GMT+8\"))\n}\n\ndef name = \"YueDu\"\ndef version = \"2.\" + releaseTime()\ndef gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], project.rootDir).text.trim())\n\nandroid {\n    compileSdkVersion 31\n    signingConfigs {\n        myConfig {\n            storeFile file(RELEASE_STORE_FILE)\n            storePassword RELEASE_KEY_PASSWORD\n            keyAlias RELEASE_KEY_ALIAS\n            keyPassword RELEASE_STORE_PASSWORD\n        }\n    }\n\n    defaultConfig {\n        applicationId \"com.gedoor.monkeybook\"\n        minSdkVersion 21\n        targetSdkVersion 31\n        versionCode 10000 + gitCommits\n        versionName version\n        project.ext.set(\"archivesBaseName\", name + \"_\" + version)\n        multiDexEnabled true\n\n    }\n    buildFeatures {\n        viewBinding true\n    }\n    lintOptions {\n        abortOnError false\n    }\n    buildTypes {\n        release {\n            signingConfig signingConfigs.myConfig\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n        debug {\n            signingConfig signingConfigs.myConfig\n            applicationIdSuffix '.debug'\n            versionNameSuffix 'debug'\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n        android.applicationVariants.all { variant ->\n            variant.outputs.all {\n                outputFileName = \"${name}_${defaultConfig.versionName}.apk\"\n            }\n        }\n    }\n    kotlinOptions {\n        jvmTarget = \"1.8\"\n    }\n    buildToolsVersion '30.0.3'\n    compileOptions {\n        // Flag to enable support for the new language APIs\n        coreLibraryDesugaringEnabled true\n        // Sets Java compatibility to Java 8\n        sourceCompatibility JavaVersion.VERSION_11\n        targetCompatibility JavaVersion.VERSION_11\n    }\n    tasks.withType(JavaCompile) {\n        options.compilerArgs << \"-Xlint:unchecked\"\n        //options.compilerArgs << \"-Xlint:deprecation\"\n    }\n}\n\nresourcePlaceholders {\n    files = ['xml/shortcuts.xml']\n}\n\ndependencies {\n    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'\n    testImplementation 'junit:junit:4.13.2'\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(':basemvplib')\n    implementation('androidx.multidex:multidex:2.0.1')\n    implementation(\"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version\")\n    implementation \"org.jetbrains.kotlin:kotlin-reflect:$kotlin_version\"\n\n    //协程\n    def coroutines_version = '1.6.0'\n    implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version\")\n    implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version\")\n\n    //anko\n    def anko_version = '0.10.8'\n    implementation \"org.jetbrains.anko:anko-sdk27:$anko_version\"\n    implementation \"org.jetbrains.anko:anko-sdk27-listeners:$anko_version\"\n\n    //lifecycle\n    def lifecycle_version = '2.4.1'\n    implementation(\"androidx.lifecycle:lifecycle-common-java8:$lifecycle_version\")\n\n    //androidX\n    implementation('androidx.appcompat:appcompat:1.4.1')\n    implementation('androidx.core:core-ktx:1.7.0')\n    implementation(\"androidx.activity:activity-ktx:1.4.0\")\n    implementation(\"androidx.fragment:fragment-ktx:1.4.1\")\n    implementation('androidx.preference:preference-ktx:1.2.0')\n    implementation('androidx.constraintlayout:constraintlayout:2.1.3')\n    implementation('androidx.swiperefreshlayout:swiperefreshlayout:1.1.0')\n    implementation('androidx.viewpager2:viewpager2:1.0.0')\n    implementation('com.google.android.material:material:1.5.0')\n    implementation('com.google.android.flexbox:flexbox:3.0.0')\n    implementation('com.google.code.gson:gson:2.9.0')\n    implementation('androidx.webkit:webkit:1.4.0')\n\n    //Splitties\n    def splitties_version = '3.0.0'\n    implementation(\"com.louiscad.splitties:splitties-appctx:$splitties_version\")\n    implementation(\"com.louiscad.splitties:splitties-systemservices:$splitties_version\")\n    implementation(\"com.louiscad.splitties:splitties-views:$splitties_version\")\n\n    //media\n    implementation(\"androidx.media:media:1.5.0\")\n    def exoplayer_version = '2.17.1'\n    implementation \"com.google.android.exoplayer:exoplayer-core:$exoplayer_version\"\n    implementation \"com.google.android.exoplayer:extension-okhttp:$exoplayer_version\"\n    implementation \"com.google.android.exoplayer:exoplayer-hls:$exoplayer_version\"\n    implementation \"com.google.android.exoplayer:exoplayer-smoothstreaming:$exoplayer_version\"\n    implementation \"com.google.android.exoplayer:exoplayer-dash:$exoplayer_version\"\n    implementation \"com.google.android.exoplayer:exoplayer-rtsp:$exoplayer_version\"\n\n    //google\n    implementation 'com.google.android.material:material:1.5.0'\n    implementation 'com.google.code.gson:gson:2.9.0'\n\n    //RxAndroid\n    implementation 'io.reactivex.rxjava2:rxjava:2.2.19'\n    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'\n\n    //RxBus\n    implementation 'com.hwangjr.rxbus:rxbus:2.0.1'\n\n    //Retrofit\n    //noinspection GradleDependency\n    implementation 'com.squareup.retrofit2:retrofit:2.9.0'\n    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'\n    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'\n\n    //J_SOUP\n    implementation 'org.jsoup:jsoup:1.14.3'\n    implementation 'cn.wanghaomiao:JsoupXpath:2.5.1'\n    implementation 'com.jayway.jsonpath:json-path:2.7.0'\n\n    //JS\n    //noinspection GradleDependency\n    implementation 'com.github.gedoor:rhino-android:1.3'\n\n    //GreenDao\n    implementation 'org.greenrobot:greendao:3.3.0'\n    implementation 'com.github.yuweiguocn:GreenDaoUpgradeHelper:v2.2.1'\n\n    //Glide\n    implementation 'com.github.bumptech.glide:glide:4.13.1'\n    kapt 'com.github.bumptech.glide:compiler:4.13.1'\n\n    //CircleImageView\n    implementation 'de.hdodenhof:circleimageview:3.1.0'\n\n    //webServer\n    implementation 'org.nanohttpd:nanohttpd:2.3.1'\n    implementation 'org.nanohttpd:nanohttpd-websocket:2.3.1'\n\n    //二维码\n    implementation 'cn.bingoogolapple:bga-qrcode-zxing:1.3.7'\n\n    //颜色选择\n    implementation 'com.jaredrummler:colorpicker:1.1.0'\n\n    //apache\n    implementation('org.apache.commons:commons-text:1.9')\n\n    //简繁转换\n    implementation 'com.luhuiguo:chinese-utils:1.0'\n\n    //字符串比较\n    implementation 'net.ricecode:string-similarity:1.0.0'\n\n    //MarkDown\n    implementation 'ru.noties.markwon:core:3.1.0'\n\n    //epub\n    implementation('com.positiondev.epublib:epublib-core:3.1') {\n        exclude group: 'org.slf4j'\n        exclude group: 'xmlpull'\n    }\n\n}\n\ngreendao {\n    schemaVersion 68\n    daoPackage 'com.kunfei.bookshelf.dao'\n    targetGenDir 'src/main/java'\n}\n\nafterEvaluate {\n//    for (Task task : project.tasks.matching { it.name.startsWith('crashlyticsUploadDeobs') }) {\n//        task.enabled = false\n//    }\n}\n\n"
  },
  {
    "path": "app/gradle.properties",
    "content": "# RELEASE_STORE_FILE = \"..\\\\key.properties.jks\"\nRELEASE_STORE_FILE=.\\\\key.properties.jks\nRELEASE_KEY_PASSWORD=android\nRELEASE_KEY_ALIAS=key0\nRELEASE_STORE_PASSWORD=android"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in D:\\CodeTool\\Android\\Android_SDK/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n#\n# 对于一些基本指令的添加\n#\n#############################################\n# 代码混淆压缩比，在0~7之间，默认为5，一般不做修改\n-optimizationpasses 5\n\n# 混合时不使用大小写混合，混合后的类名为小写\n-dontusemixedcaseclassnames\n\n# 指定不去忽略非公共库的类\n-dontskipnonpubliclibraryclasses\n\n# 这句话能够使我们的项目混淆后产生映射文件\n# 包含有类名->混淆后类名的映射关系\n-verbose\n\n# 指定不去忽略非公共库的类成员\n-dontskipnonpubliclibraryclassmembers\n\n# 不做预校验，preverify是proguard的四个步骤之一，Android不需要preverify，去掉这一步能够加快混淆速度。\n-dontpreverify\n\n# 保留Annotation不混淆\n-keepattributes *Annotation*,InnerClasses\n\n# 避免混淆泛型\n-keepattributes Signature\n\n# 抛出异常时保留代码行号\n-keepattributes SourceFile,LineNumberTable\n\n# 指定混淆是采用的算法，后面的参数是一个过滤器\n# 这个过滤器是谷歌推荐的算法，一般不做更改\n-optimizations !code/simplification/cast,!field/*,!class/merging/*\n\n\n#############################################\n#\n# Android开发中一些需要保留的公共部分\n#\n#############################################\n\n# 保留我们使用的四大组件，自定义的Application等等这些类不被混淆\n# 因为这些子类都有可能被外部调用\n-keep public class * extends android.app.Activity\n-keep public class * extends android.app.Application\n-keep public class * extends android.app.Service\n-keep public class * extends android.content.BroadcastReceiver\n-keep public class * extends android.content.ContentProvider\n-keep public class * extends android.app.backup.BackupAgentHelper\n-keep public class * extends android.preference.Preference\n-keep public class * extends android.view.View\n-keep public class com.android.vending.licensing.ILicensingService\n\n\n# 保留androidx下的所有类及其内部类\n-keep class androidx.** {*;}\n\n# 保留继承的\n-keep public class * extends androidx.**\n\n# 保留R下面的资源\n-keep class **.R$* {*;}\n\n# 保留本地native方法不被混淆\n-keepclasseswithmembernames class * {\n    native <methods>;\n}\n\n# 保留在Activity中的方法参数是view的方法，\n# 这样以来我们在layout中写的onClick就不会被影响\n-keepclassmembers class * extends android.app.Activity{\n    public void *(android.view.View);\n}\n\n# 保留枚举类不被混淆\n-keepclassmembers enum * {\n    public static **[] values();\n    public static ** valueOf(java.lang.String);\n}\n\n# 保留我们自定义控件（继承自View）不被混淆\n-keep public class * extends android.view.View{\n    *** get*();\n    void set*(***);\n    public <init>(android.content.Context);\n    public <init>(android.content.Context, android.util.AttributeSet);\n    public <init>(android.content.Context, android.util.AttributeSet, int);\n}\n\n# 保留Parcelable序列化类不被混淆\n-keep class * implements android.os.Parcelable {\n    public static final android.os.Parcelable$Creator *;\n}\n\n# 保留Serializable序列化的类不被混淆\n-keepclassmembers class * implements java.io.Serializable {\n    static final long serialVersionUID;\n    private static final java.io.ObjectStreamField[] serialPersistentFields;\n    !static !transient <fields>;\n    !private <fields>;\n    !private <methods>;\n    private void writeObject(java.io.ObjectOutputStream);\n    private void readObject(java.io.ObjectInputStream);\n    java.lang.Object writeReplace();\n    java.lang.Object readResolve();\n}\n\n# 对于带有回调函数的onXXEvent、**On*Listener的，不能被混淆\n-keepclassmembers class * {\n    void *(**On*Event);\n    void *(**On*Listener);\n}\n\n# webView处理，项目中没有使用到webView忽略即可\n-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n    public *;\n}\n-keepclassmembers class * extends android.webkit.webViewClient {\n    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);\n    public boolean *(android.webkit.WebView, java.lang.String);\n}\n-keepclassmembers class * extends android.webkit.webViewClient {\n    public void *(android.webkit.webView, jav.lang.String);\n}\n\n# 移除Log类打印各个等级日志的代码，打正式包的时候可以做为禁log使用，这里可以作为禁止log打印的功能使用\n# 记得proguard-android.txt中一定不要加-dontoptimize才起作用\n# 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制\n-assumenosideeffects class android.util.Log {\n    public static int v(...);\n    public static int i(...);\n    public static int w(...);\n    public static int d(...);\n    public static int e(...);\n}\n\n# 保持js引擎调用的java类\n-keep class **.analyzeRule.**{*;}\n# 保持web类\n-keep class **.web.**{*;}\n\n### greenDAO 3\n-keep class org.greenrobot.greendao.**{ *; }\n-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {\npublic static java.lang.String TABLENAME;\n}\n-keep class **$Properties\n-dontwarn org.greenrobot.greendao.database.**\n-dontwarn rx.**\n\n-dontwarn okio.**\n-dontwarn retrofit2.**\n-dontwarn javax.annotation.**\n-dontwarn org.apache.log4j.lf5.viewer.**\n-dontnote org.apache.log4j.lf5.viewer.**\n-dontwarn freemarker.**\n-dontnote org.python.core.**\n-dontwarn com.hwangjr.rxbus.**\n-dontwarn okhttp3.**\n\n-keep class retrofit2.**{*;}\n-keep class okhttp3.**{*;}\n-keep class okio.**{*;}\n-keep class com.hwangjr.rxbus.**{*;}\n-keep class org.conscrypt.**{*;}\n-keep class com.kunfei.bookshelf.widget.**{*;}\n-keep class com.kunfei.bookshelf.bean.**{*;}\n-keep class android.support.**{*;}\n-keep class me.grantland.widget.**{*;}\n-keep class de.hdodenhof.circleimageview.**{*;}\n-keep class tyrant.explosionfield.**{*;}\n-keep class tyrantgit.explosionfield.**{*;}\n-keep class freemarker.**{*;}\n-keep class com.gyf.barlibrary.* {*;}\n##JSOUP\n-keep class org.jsoup.**{*;}\n-keep class com.monke.mprogressbar.**{ *;}\n\n-keep class org.slf4j.**{*;}\n-dontwarn org.slf4j.**\n\n-keep class org.codehaus.**{*;}\n-dontwarn org.codehaus.**\n-keep class com.jayway.**{*;}\n-dontwarn com.jayway.**\n-keep class com.fasterxml.**{*;}\n\n-keep class javax.swing..**{*;}\n-dontwarn javax.swing.**\n-keep class java.awt.**{*;}\n-dontwarn java.awt.**\n-keep class sun.misc.**{*;}\n-dontwarn sun.misc.**\n-keep class sun.reflect.**{*;}\n-dontwarn sun.reflect.**\n\n## Rhino\n-keep class javax.script.** { *; }\n-keep class com.sun.script.javascript.** { *; }\n-keep class org.mozilla.javascript.** { *; }\n-dontwarn org.mozilla.javascript.**\n-dontwarn sun.**\n\n###EPUB\n-dontwarn nl.siegmann.epublib.**\n-dontwarn org.xmlpull.**\n-keep class nl.siegmann.epublib.**{*;}\n-keep class javax.xml.**{*;}\n-keep class org.xmlpull.**{*;}\n\n-keep class org.simpleframework.xml.**{*;}\n-dontwarn org.simpleframework.xml.**\n\n-keepclassmembers class * {\n    public <init> (org.json.JSONObject);\n}\n-keep public class com.kunfei.bookshelf.R$*{\n    public static final int *;\n}\n-keepclassmembers enum * {\n    public static **[] values();\n    public static ** valueOf(java.lang.String);\n}\n"
  },
  {
    "path": "app/src/debug/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">阅读.debug</string>\n    <string name=\"receiving_shared_label\">阅读.debug·搜索</string>\n</resources>"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.kunfei.bookshelf\">\n\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        tools:ignore=\"ScopedStorage\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n    <uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS\" />\n\n    <application\n        android:name=\".MApplication\"\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:persistent=\"true\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/CAppTheme\"\n        android:requestLegacyExternalStorage=\"true\"\n        tools:ignore=\"AllowBackup,UnusedAttribute\"\n        tools:replace=\"android:theme\">\n        <activity\n            android:name=\".view.activity.WelcomeActivity\"\n            android:theme=\"@style/CAppWelcomeTheme\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.app.shortcuts\"\n                android:resource=\"@xml/shortcuts\" />\n        </activity>\n        <activity\n            android:name=\".view.activity.WelcomeBookActivity\"\n            android:enabled=\"false\"\n            android:icon=\"@mipmap/book_launcher\"\n            android:roundIcon=\"@mipmap/book_launcher_round\"\n            android:theme=\"@style/CAppWelcomeTheme\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.app.shortcuts\"\n                android:resource=\"@xml/shortcuts\" />\n        </activity>\n        <activity\n            android:name=\".view.activity.ReadBookActivity\"\n            android:configChanges=\"locale|keyboardHidden\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"behind\"\n            android:exported=\"true\">\n            <intent-filter tools:ignore=\"AppLinkUrlError\">\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n\n                <data android:mimeType=\"text/plain\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\".view.activity.ReceivingSharedActivity\"\n            android:enabled=\"true\"\n            android:label=\"@string/receiving_shared_label\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.PROCESS_TEXT\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n\n                <data android:mimeType=\"text/plain\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.SEND\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n\n                <data android:mimeType=\"text/plain\" />\n            </intent-filter>\n        </activity>\n        <activity\n            android:name=\".view.activity.QRCodeScanActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.MainActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\"\n            android:alwaysRetainTaskState=\"true\" />\n        <activity\n            android:name=\".view.activity.SearchBookActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\"\n            android:windowSoftInputMode=\"stateHidden|adjustUnspecified\" />\n        <activity\n            android:name=\".view.activity.ChoiceBookActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.BookDetailActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"behind\"\n            android:theme=\"@style/CAppTransparentTheme\" />\n        <activity\n            android:name=\".view.activity.ImportBookActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.BookSourceActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.ReplaceRuleActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.SettingActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.ThemeSettingActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.AboutActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.DonateActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.SourceEditActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\"\n            android:windowSoftInputMode=\"adjustResize|stateHidden\" />\n        <activity\n            android:name=\".view.activity.ReadStyleActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.DownloadActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.BookInfoEditActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\"\n            android:windowSoftInputMode=\"adjustResize\" />\n        <activity\n            android:name=\".view.activity.ChapterListActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\"\n            android:screenOrientation=\"behind\" />\n        <activity\n            android:name=\".view.activity.SourceLoginActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.SourceDebugActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.TxtChapterRuleActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.BookCoverEditActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"singleTask\" />\n        <activity\n            android:name=\".view.activity.WebViewActivity\"\n            android:configChanges=\"locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout\"\n            android:launchMode=\"standard\" />\n        <activity\n            android:name=\".help.permission.PermissionActivity\"\n            android:theme=\"@style/Activity.Permission\" />\n\n        <service android:name=\".service.DownloadService\" />\n        <service android:name=\".service.ReadAloudService\" />\n        <service android:name=\".service.CheckSourceService\" />\n        <service android:name=\".service.WebService\" />\n        <service android:name=\".service.ShareService\" />\n\n        <receiver\n            android:name=\".service.MediaButtonIntentReceiver\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MEDIA_BUTTON\" />\n            </intent-filter>\n        </receiver>\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileProvider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/file_paths\" />\n        </provider>\n\n    </application>\n\n    <queries>\n        <intent>\n            <action android:name=\"android.intent.action.TTS_SERVICE\" />\n        </intent>\n        <intent>\n            <action android:name=\"android.intent.action.PROCESS_TEXT\" />\n            <data android:mimeType=\"text/plain\" />\n        </intent>\n    </queries>\n\n</manifest>"
  },
  {
    "path": "app/src/main/assets/18PlusList.txt",
    "content": "OGN5dS5jb20=\nc2cwMC54eXo=\naXRyYWZmaWNuZXQuY29t\neGlhb3FpYW5nNTIw\nMTIzeGlhb3FpYW5n\neGlhb3FpYW5neHM=\neGlhb3FpYW5nNTIw\nMzM1eHM=\neGN4czk=\neGN4czUyMA==\nc2h1YmFvYW4=\nc2h1YmFvd2FuZzEyMw==\nc2h1YmFvYW4=\naGFpdGFuZzEyMw==\neXV6aGFpd3VsYQ==\ncG8xOA==\nYmwtbm92ZWw=\nNXRucw==\nc2hhb3NodWdl\namluamlzaHV3dQ==\nNDJ3Zw==\neWlxdXNodQ==\nc2h1YmFvd2FuZzEyMw==\nM2hlYmFv\nMzNoZWJhbw==\nbHVvcWl1enc=\nbXlzaHVnZQ==\nc3NzeHN3\neWl0ZQ==\nY3Vpd2VpanV1\nY3Vpd2VpanV4cw==\nY3Vpd2VpanV4\neGlhb3FpYW5nd3g=\nYXN6dw==\nYXN6dzY=\nc2FuaGFveHM=\nODdzaHV3dQ==\nNDh3eA==\nbG9uZ3Rlbmcy\nNnF3eA==\nbG9uZ3Rlbmd4cw==\naGF4ZHU=\nM3R3eA==\naGF4d3g1\nNjZsZXdlbg==\neGJhbnpodQ==\naGR5cA==\nZHliejk=\nZGl5aWJhbnpodTk=\nZGl5aWJhbnpodQ==\nZGl5aWJhbnpodTc=\nYnoyMjI=\nd29kZWFwaTAwMQ==\ndGFuZ3poZWthbg==\nYmF4aWFueHM=\neGlhb3NodW9zaGVuemhhbg==\nZGFtb2tl\nemh3ZW5wZw==\neXV6aGFpZ2U=\nd21wOA==\nOXhpYW53ZW4=\nbmFucmVudmlw\ncmV5b28=\neWZ4aWFvc2h1b2U=\nc2Fuaml1enc=\nN3Fpbmc3\ncWR4aWFvc2h1bw==\nY2hpbmVzZXpq\nMzlzaHViYW8=\na3l4czU=\nNTZtcw==\nbml1c2hh\nbWt4czY=\nMjIyMjJ4cw==\nOTVkdXNodQ==\nYmFuemh1MjI=\nd3JsdHh0\ndHVkb3V0eHQ=\ncm5neHM=\nOTl3ZW5rdQ==\nbGFvc2lqaXhz\nZnVzaHV6aGFpMQ==\n"
  },
  {
    "path": "app/src/main/assets/data/BookSourceXxl.json",
    "content": "{\n  \"bookSourceUrl\": \"https://www.kaixin7days.com\",\n  \"bookSourceName\": \"消消乐听书\",\n  \"bookSourceGroup\": \"听书\",\n  \"bookSourceType\": \"AUDIO\",\n  \"loginUrl\": \"var loginInfo = source.getLoginInfo()\\nvar json = java.getResponse(\\\"https://www.kaixin7days.com/login@\\\" + loginInfo).body()\\nvar loginRes = JSON.parse(json)\\nvar header = null\\nif (loginRes.statusCode == 200) {\\n    var accessToken = {\\n        Authorization: \\\"Bearer \\\" + loginRes.content.accessToken\\n    }\\n    header = JSON.stringify(accessToken)\\n    source.putLoginHeader(header)\\n}\\nheader\",\n  \"loginUi\": \"[{\\\"name\\\": \\\"telephone\\\",\\\"type\\\": \\\"text\\\"},{\\\"name\\\": \\\"password\\\",\\\"type\\\": \\\"password\\\"},{\\\"type\\\": \\\"button\\\",\\\"name\\\": \\\"注册\\\", \\\"action\\\": \\\"http://www.yooike.com/xiaoshuo/#/register?title=%E6%B3%A8%E5%86%8C\\\"}]\",\n  \"loginCheckJs\": \"var strRes = result\\nvar c = JSON.parse(result.body())\\nif (c.statusCode == 301) {\\n    var loginInfo = source.getLoginInfo()\\n    var dl = null\\n    if (loginInfo) {\\n        dl = java.getResponse(\\\"https://www.kaixin7days.com/login@\\\" + loginInfo).body()\\n    } else {\\n        dl = java.getResponse('https://www.kaixin7days.com/visitorLogin@{\\\"deviceId\\\":\\\"'+java.androidId()+'\\\"}').body()\\n    }\\n    c = JSON.parse(dl)\\n    var accessToken = {\\n        Authorization: \\\"Bearer \\\" + c.content.accessToken\\n    }\\n    var header = JSON.stringify(accessToken)\\n    source.putLoginHeader(header)\\n    strRes = java.getResponse(\\\"@Header:\\\" + header + url)\\n}\\nstrRes\",\n  \"serialNumber\": -100,\n  \"enable\": true,\n  \"ruleFindUrl\": \"@js:var header = source.getLoginHeader()\\nvar json = \\\"\\\"\\nvar j = null\\nif (header != null) {\\n    json = java.getResponse(\\\"@Header:\\\" + header + \\\"https://www.kaixin7days.com/book-service/bookMgt/getBookCategroy@{}\\\").body()\\n    j = JSON.parse(json)\\n}\\nif (j == null || j.statusCode != 200) {\\n    json = java.getResponse(\\\"https://www.kaixin7days.com/visitorLogin@{}\\\").body()\\n    j = JSON.parse(json)\\n    var accessToken = {\\n        Authorization: \\\"Bearer \\\" + j.content.accessToken\\n    }\\n    header = JSON.stringify(accessToken)\\n    source.putLoginHeader(header)\\n    json = java.getResponse(\\\"@Header:\\\" + header + \\\"https://www.kaixin7days.com/book-service/bookMgt/getBookCategroy@{}\\\").body()\\n    j = JSON.parse(json)\\n}\\nvar fls = j.content\\nvar fx = \\\"\\\"\\nfor (var i = 0; i < fls.length; i++) {\\n    fx = fx + fls[i].categoryName + '::/book-service/bookMgt/getAllBookByCategroyId@{\\\"categoryIds\\\": \\\"' + fls[i].associationCategoryIDs + '\\\",\\\"pageNum\\\": {{searchPage}},\\\"pageSize\\\": 100}&&'\\n}\\nfx\",\n  \"ruleFindList\": \"$.content.content\",\n  \"ruleFindName\": \"$.title\",\n  \"ruleFindAuthor\": \"$.author\",\n  \"ruleFindKind\": \"\",\n  \"ruleFindIntroduce\": \"$.desc\",\n  \"ruleFindLastChapter\": \"$.newestChapter\",\n  \"ruleFindCoverUrl\": \"$.cover@js:var cover = JSON.parse(result);'https://www.shuidi.online:9021/fileMgt/getPicture?filePath='+cover.storeFilePath\",\n  \"ruleFindNoteUrl\": \"$.id@js:java.put('bookId', result);'https://www.kaixin7days.com/book-service/bookMgt/getAllChapterByBookId@{\\\"bookId\\\": \\\"'+result+'\\\",\\\"pageNum\\\": 1,\\\"pageSize\\\": 100000}'\",\n  \"ruleSearchUrl\": \"https://www.kaixin7days.com/book-service/bookMgt/findBookName@{\\\"title\\\": \\\"searchKey\\\",\\\"pageNum\\\": {{searchPage}},\\\"pageSize\\\": 100}\",\n  \"ruleSearchList\": \"$.content.content\",\n  \"ruleSearchName\": \"$.title\",\n  \"ruleSearchAuthor\": \"$.author\",\n  \"ruleSearchIntroduce\": \"$.desc\",\n  \"ruleSearchLastChapter\": \"$.newestChapter\",\n  \"ruleSearchCoverUrl\": \"$.cover@js:var cover = JSON.parse(result);'https://www.shuidi.online:9021/fileMgt/getPicture?filePath='+cover.storeFilePath\",\n  \"ruleSearchNoteUrl\": \"$.id@js:java.put('bookId', result);'https://www.kaixin7days.com/book-service/bookMgt/getAllChapterByBookId@{\\\"bookId\\\": \\\"'+result+'\\\",\\\"pageNum\\\": 1,\\\"pageSize\\\": 100000}'\",\n  \"ruleChapterList\": \"$.content.content\",\n  \"ruleChapterName\": \"$.chapterTitle\",\n  \"ruleChapterVip\": \"$.isFree@js:var vip = false; if (result == '0') { vip = true } vip\",\n  \"ruleChapterPay\": \"$.isPay\",\n  \"ruleContentUrl\": \"$.id@js:\\\"https://www.shuidi.online:9021/fileMgt/getAudioByChapterId?bookId=\\\" + java.getString(\\\"$.bookId\\\") + \\\"&chapterId=\\\" + result + \\\"&pageNum=1&pageSize=50&{{var header = JSON.parse(source.getLoginHeader());var reg = /&chapterId=(.*?)&/;var chapterId = reg.exec(result)[1];var keyId = '1632746188011002';var ks = java.md5Encode(keyId + chapterId + header.Authorization);'Authorization=' + header.Authorization + '&keyId=' + keyId + '&keySecret=' + ks}\\\" + \\\"}\\\"\",\n  \"ruleBookContent\": \"\",\n  \"payAction\": \"var header = JSON.parse(source.getLoginHeader())\\nvar chapterUrl = chapter.getDurChapterUrl(); var reg = /&chapterId=(.*?)&/; var chapterId = reg.exec(chapterUrl)[1]\\n'http://www.shuidi.online/?name='+book.getName()+'&type=2&cover=' + book.getCoverPath() + '&chapterId=' + chapterId + '&chapter=203&allNumber=' + book.getChapterListSize()+'&bookId=' + book.getVariableMap().get('bookId') + '&chapterIds=' + chapterId + '&number=' + chapter.getDurChapterIndex() + '&accessToken=' + header.Authorization.substring(7) + '#/pay'\"\n}"
  },
  {
    "path": "app/src/main/assets/disclaimer.md",
    "content": "# 免责声明（Disclaimer）\n\n* 阅读是一款提供网络文学搜索的工具，为广大网络文学爱好者提供一种方便、快捷舒适的试读体验。\n* 当您搜索一本书的时，阅读会将该书的书名以关键词的形式提交到各个第三方网络文学网站。\n各第三方网站返回的内容与阅读无关，阅读对其概不负责，亦不承担任何法律责任。\n任何通过使用阅读而链接到的第三方网页均系他人制作或提供，您可能从第三方网页上获得其他服务，阅读对其合法性概不负责，亦不承担任何法律责任。\n第三方搜索引擎结果根据您提交的书名自动搜索获得并提供试读，不代表阅读赞成或被搜索链接到的第三方网页上的内容或立场。\n您应该对使用搜索引擎的结果自行承担风险。\n* 阅读不做任何形式的保证：不保证第三方搜索引擎的搜索结果满足您的要求，不保证搜索服务不中断，不保证搜索结果的安全性、正确性、及时性、合法性。\n因网络状况、通讯线路、第三方网站等任何原因而导致您不能正常使用阅读，阅读不承担任何法律责任。\n阅读尊重并保护所有使用阅读用户的个人隐私权，您注册的用户名、电子邮件地址等个人资料，非经您亲自许可或根据相关法律、法规的强制性规定，阅读不会主动地泄露给第三方。\n* 阅读致力于最大程度地减少网络文学阅读者在自行搜寻过程中的无意义的时间浪费，通过专业搜索展示不同网站中网络文学的最新章节。\n阅读在为广大小说爱好者提供方便、快捷舒适的试读体验的同时，也使优秀网络文学得以迅速、更广泛的传播，从而达到了在一定程度促进网络文学充分繁荣发展之目的。\n阅读鼓励广大小说爱好者通过阅读发现优秀网络小说及其提供商，并建议阅读正版图书。\n任何单位或个人认为通过阅读搜索链接到的第三方网页内容可能涉嫌侵犯其信息网络传播权，应该及时向阅读提出书面权力通知，并提供身份证明、权属证明及详细侵权情况证明。\n阅读在收到上述法律文件后，将会依法尽快断开相关链接内容。"
  },
  {
    "path": "app/src/main/assets/txtChapterRule.json",
    "content": "[\n    {\n        \"enable\": true,\n        \"name\": \"目录\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 0\n    },\n    {\n        \"enable\": false,\n        \"name\": \"目录(去空白)\",\n        \"rule\": \"(?<=[　\\\\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 1\n    },\n    {\n        \"enable\": false,\n        \"name\": \"目录(去简介)\",\n        \"rule\": \"(?<=[　\\\\s])(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 2\n    },\n    {\n        \"enable\": false,\n        \"name\": \"目录(古典、轻小说备用)\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\\\s{0,4}[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$\",\n        \"serialNumber\": 3\n    },\n    {\n        \"enable\": false,\n        \"name\": \"数字(纯数字标题)\",\n        \"rule\": \"(?<=[　\\\\s])\\\\d+[ 　\\\\t]{0,4}$\",\n        \"serialNumber\": 4\n    },\n    {\n        \"enable\": true,\n        \"name\": \"数字 分隔符 标题名称\",\n        \"rule\": \"^[ 　\\\\t]{0,4}\\\\d{1,5}[\\\\,\\\\.， 、\\\\-].{1,30}$\",\n        \"serialNumber\": 5\n    },\n    {\n        \"enable\": true,\n        \"name\": \"正文 标题/序号\",\n        \"rule\": \"^[ 　\\\\t]{0,4}正文[ 　]{1,4}.{0,20}$\",\n        \"serialNumber\": 6\n    },\n    {\n        \"enable\": true,\n        \"name\": \"Chapter/Section/Part/Episode 序号 标题\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|ＰＡＲＴ|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\\\s{0,4}\\\\d{1,4}.{0,30}$\",\n        \"serialNumber\": 7\n    },\n    {\n        \"enable\": false,\n        \"name\": \"Chapter(去简介)\",\n        \"rule\": \"^[ 　\\\\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|ＰＡＲＴ|[Ee]pisode)\\\\s{0,4}\\\\d{1,4}.{0,30}$\",\n        \"serialNumber\": 8\n    },\n    {\n        \"enable\": true,\n        \"name\": \"特殊符号 序号 标题\",\n        \"rule\": \"(?<=[\\\\s　]{0,4}).{1,3}(?:第|卷|[Cc]hapter)[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节]?[\\\\.:： \\f\\t].{0,20}$\",\n        \"serialNumber\": 9\n    },\n    {\n        \"enable\": false,\n        \"name\": \"特殊符号 标题(成对)\",\n        \"rule\": \"(?<=[\\\\s　]{0,4})(?:[\\\\[〈「『〖〔《（【\\\\(].{1,30}[\\\\)】）》〕〗』」〉\\\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[ 　]{0,4}$\",\n        \"serialNumber\": 10\n    },\n    {\n        \"enable\":true,\n        \"name\": \"特殊符号 标题(单个)\",\n        \"rule\": \"(?<=[\\\\s　]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[ 　]{0,4}$\",\n        \"serialNumber\": 11\n    },\n    {\n        \"enable\": true,\n        \"name\": \"章/卷 序号 标题\",\n        \"rule\": \"^[ \\\\t　]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[ 　]{0,4}.{0,30}$\",\n        \"serialNumber\": 12\n    },\n    {\n        \"enable\":false,\n        \"name\": \"顶格标题\",\n        \"rule\": \"^\\\\S.{1,20}$\",\n        \"serialNumber\": 13\n    },\n    {\n        \"enable\":false,\n        \"name\": \"双标题(前向)\",\n        \"rule\": \"(?m)(?<=[ \\\\t　]{0,4})第[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\\\s　]{0,8}第[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)\",\n        \"serialNumber\": 14\n    },\n    {\n        \"enable\":false,\n        \"name\": \"双标题(后向)\",\n        \"rule\": \"(?m)(?<=[ \\\\t　]{0,4}第[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\\\s　]{0,8})第[\\\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$\",\n        \"serialNumber\": 15\n    }\n]\n"
  },
  {
    "path": "app/src/main/assets/updateLog.md",
    "content": "# 更新日志\n\n* 2.0版本以停止更新,请关注公众号[开源阅读]()获取3.0版本。\n\n**2022/04/01**\n\n* 更新到SDK31\n* 更新一些库\n\n**2021/10/24**\n\n* 字体和备份适配android 11\n\n**2021/10/18**\n\n* 去除更新失败提醒\n* 第二次换源先取消原先换源\n\n**2021/10/17**\n\n* 修复状态栏和导航栏问题\n\n**2021/10/01**\n\n* 使用exoPlayer支持更多音频格式\n* 添加一个默认听书源\n* 添加登录界面规则,登录检查规则,添加的默认书源可作为学习用\n* 大量优化\n\n**2021/07/26**\n\n* 更新一些库文件\n\n**2021/02/16**\n\n* 详情页目录添加跳转功能\n* 优化发现界面文字可能因为主题看不请的bug\n\n**2021/01/04**\n\n* 修复本地导入崩溃bug\n\n**2020/12/26**\n* 更新库,更新为viewBinding\n* 书籍信息界面添加目录按钮\n\n**2020/12/07**\n* 修改风险\n\n**2020/11/07**\n* 在书架查看书籍的界面，增加复制小说网址的菜单。\n* 在书源列表界面，增加检查书源是否包含发现规则，并增加/删除“发现”标签的功能。\n* 在阅读界面，增加长按按钮“广告”，可以快速把正文添加到命名为“广告话术-xxx网址”的规则中去。针对此规则，使用了专门的增强算法。只需要多次标记不想看的文字，就可以获得较好的广告替换效果。用户不需要熟悉正则语法，不需要维护复杂的规则。同一个网站的广告话术自动保存在一个规则内，避免了“替换净化”列表里有太多多的规则。\n* 在阅读界面，优化长按按钮“替换”，自动添加书名、网址到应用范围中。\n* 进一步优化自动分段算法\n* 长按选择文本的高亮色块的形状由弧边改为圆角矩形\n\n**2020/10/28**\n* 增加“分享书籍”功能。\n* 优化阅读3.0书源转2.0的功能。\n* by [tumuyan](https://github.com/tumuyan)\n\n**2020/10/24**\n* 增加自动重分段落的功能。部分网站错字、错误标点、错误换行极度严重。使用此功能可以一定程度上矫正引号和换行，从而改善阅读体验。\n* 修复书签列表删除书签时UI不实时刷新的异常\n* by [tumuyan](https://github.com/tumuyan)\n\n**2020/10/16**\n* 添加js方法实现重定向拦截,同3.0\n\n**2020/10/02**\n* 改善添加网址功能 by [tumuyan]\n\n**2020/09/02**\n* 添加正文合并后替换规则\n\n**2020/08/28**\n* 应用被杀死时停止朗读\n\n**2020/08/07**\n* 修复一些网络问题\n\n**2020/08/04**\n* 提高搜索结果的准确性,过滤掉书名+作者与搜索关键词不匹配的搜索结果。（在设置页面增加了相关选项）by [tumuyan](https://github.com/tumuyan)\n\n**2020/07/19**\n* 导入书源时系统文件选择器可以选择json文件\n* 换源时保留是否更新\n\n**2020/07/04**\n* 更新一些库文件\n* 优化一些代码\n\n**2020/06/07**\n* 升级到android studio 4.0\n* sdk升级到29\n* 升级一些库文件\n\n**2020/05/17**\n* 修复jsonpath解析的bug\n\n**2020/05/01**\n* UA用作header，支持Map\n\n**2020/04/25**\n* 修复jsonpath解析的bug\n\n**2020/04/11**\n* 修复一些bug\n* 更新一些库\n\n**2020/03/20**\n* 自动备份文件和手动备份文件分开\n* 更新一些库文件\n\n**2020/03/12**\n* 更新txt解析目录 by 52fisher\n* web看书加翻页 by Celeter\n\n**2020/02/26**\n* 更新默认txt目录规则,由https://github.com/52fisher提供\n* 新版本快上架了,基本功能以完成,上架后再慢慢优化\n* 别人说加广告一天能有几千收入,我不加广告大家给点力多看看公众号广告啊\n\n**2020/02/15**\n* 修复低版本手机无法联网的问题\n* 优化备份恢复\n\n**2020/02/10**\n* 优化备份恢复\n* 朗读定时会记住\n\n**2020/02/05**\n* 修复bug\n\n**2020/02/04**\n* 修复bug\n* 更换默认封面,默认封面绘制书名作者\n\n**2020/01/24**\n* 修复备份bug\n* 修复切换图标后不在后台显示的bug\n\n**2020/01/21**\n* 将规则解析后得到的字符串反转义(unescape)\n* 在正文规则使用all可查看源码，例:`tag.body@all`\n* 添加备份时选择备份文件夹--适配安卓10\n\n**2020/01/09**\n* 修复按键码为0时翻页的bug\n* 去除tts中文检测判断\n\n**2019/12/30**\n* 修复一些会崩溃BUG\n\n**2019/11/29**\n* 增加耳机上一首下一首控制\n* 自定义按键增加上一页配置\n* 关注公众号【小说拾遗】为你推荐好看的小说\n\n**2019/11/16**\n* 自动翻页由N秒/页改为字数每分钟(CPM)\n* 添加自定义翻页按键\n\n**2019/10/30**\n* 修复从搜索界面打开书籍,是否在上架不对的bug\n* 封面换源不显示已经删除的书源\n\n**2019/10/23**\n* 繁体-翻译者:Cello琴弦之間\n* 封面换源显示封面图片\n\n**2019/10/15**\n* 添加繁体语言\n* 修复换源界面编辑书源等无法保存的bug\n* 其它一些优化\n\n**2019/10/05**\n* 搜索时显示书架里的书\n\n**2019/09/25**\n* 网络导入书源的记录添加了删除按钮\n\n**2019/09/24**\n* 网络导入书源可以记住多个导入网址,方便书源更新\n\n**2019/09/14**\n* 阅读界面设置添加微调\n* 选择图片在有些手机上可能会崩溃的bug\n\n**2019/09/09**\n* 网格书架增加3列4列的选择\n* 正文规则为空时内容为章节url\n* merged commit cac2689, 更新阅读设置界面,增加一些自带背景\n\n**2019/08/29**\n* 修复导入本地不显示文件的bug\n* 优化书架网格界面\n\n**2019/08/22**\n* 给扩展到刘海屏加了个开关\n\n**2019/08/21**\n* 修复一些bug\n* 口袋阅书架可以显示了\n* 刘海屏阅读界面隐藏状态栏不显示黑条\n\n**2019/08/15**\n* 修复拷贝书源问题\n\n**2019/08/13**\n* 在退出软件之前会记住搜索分组\n\n**2019/08/12**\n* 书源管理分组添加显示勾选\n* 详情页点击书名可搜索\n* 优化web写源加载速度\n* 修复text没了换行的bug\n* 修复阅读界面点击章节名称可编辑书源换源不变的bug\n\n**2019/08/10**\n* 修复正则写的不对会崩溃的BUG\n* 修复正则书源会返回null书籍的BUG\n* 阅读界面点击章节名称可编辑书源\n* 添加替换净化默认启用关闭的配置\n\n**2019/07/30**\n* 修复更新详情页不保存的bug\n* 高级功能改为一个月点一次\n* 修复web写源会丢音频标志的bug\n\n**2019/07/25**\n* 修复一些bug\n\n**2019/07/20**\n* 阅读界面菜单添加是否启用替换净化,默认禁用\n* Android O以上系统支持正则命名分组\n* 其它一些优化\n\n**2019/07/15**\n* 优化搜索列表正则AllInOne效率; 增强目录列表正则AllInOne兼容性 by Antecer\n* web写源添加一个web书架入口\n* 调整书源管理菜单\n\n**2019/07/13**\n* 修复BUG\n* 修复因加入长按复制导致有声书源崩溃的问题\n\n**2019/07/8**\n* 参考搜神添加了长按选择,需主动开启\n\n**2019/07/5**\n* 发现添加单个清除缓存\n* 尝试修复换源界面禁用错位的问题\n\n**2019/07/4**\n* 换源不再会改变书名和作者,防止换到不好的源之后不能再换源\n* 规则添加了 ##替换内容##替换为\n* 修复了详情页init规则的报错\n\n**2019/07/3**\n* 修复一个会导致崩溃的问题\n* 修复----在发现或搜索里，点击书籍进入详情页，然后点击换源，选择要换的源，第二次再点击换源，仍然选择之前的那个源，这时候加入书架会变成删除书籍，实际上并没有加入到书架中的问题\n\n**2019/07/1**\n* 修复搜索直达详情页的正则处理\n\n**2019/06/30**\n* 修复搜索直达详情页的正则处理\n* 修复一个会导致崩溃的问题\n\n**2019/06/26**\n* 修复书源全选会导致排序混乱的问题\n* 修复提示缓存被删除的问题\n\n**2019/06/23**\n* 书源规则id节点可以获取多个\n* 修复换源后首次不能下载的BUG\n* 修复章节可能空白的问题\n* 修复没有加入书架不能下载的问题\n\n**2019/06/16**\n* 修复文件选择器,路径不能朗读的bug\n\n**2019/06/14**\n* 加入kotlin库,安装包又大了一些,后续一些优化会使用kotlin来写\n* 重写权限获取,修复没有存储权限时不提示的问题\n\n**2019/06/10**\n* 修复书架文字显示不全的问题\n* 其它一些优化\n\n**2019/06/08**\n* 修复换源界面宽度显示不对的BUG\n\n**2019/06/06**\n* 修复对话框内的一些问题\n\n**2019/06/01**\n* js库升级到Rhino 1.7.11\n\n**2019/05/31**\n* 侧边栏背景跟随主题背景\n* 其它一些优化\n\n**2019/05/22**\n* 完成整理书架\n* 修复一个阅读页面空白的bug\n\n**2019/05/20**\n* 修复一键缓存崩溃的问题\n* 将切换图标移到主题设置里\n* 将清空缓存移到设置里\n* 解决校验书源和搜索卡死的问题\n\n**2019/05/19**\n* 这是一个比较稳定的版本,没什么大的BUG\n* 翻页BUG据反馈已经没有了\n\n**2019/05/18**\n* 尝试修复一些翻页问题\n* 搜索界面添加简介显示\n\n**2019/05/16**\n* 修复部分手机书源编辑界面无法编辑的问题\n* 尝试修复可能会出现的翻不了页问题\n\n**2019/05/15**\n* 搜索页和详情页为同一网址时不再重复获取网页\n* 详情页和目录页为同一网址时不再重复获取网页\n* 优化翻页动画,尝试解决一些手机翻页不变的问题\n* 书源规则增加一些字段,发现规则独立出来\n* 重写书源编辑界面\n* 其它一些优化,修复一些bug\n\n**2019/05/13**\n* 并行解析多页目录列表,提高解析速度\n* 增加js方法,java.put(key, value) java.get(key)\n* 搜索增加按书源分组\n* 章节绝对url放到访问时再组合,提高解析速度\n* 音频播放结束自动下一章\n\n**2019/05/12**\n* 修复bug\n\n**2019/05/11**\n* 添加有声阅读功能,正文内容返回mp3地址可播放\n* 优化解析逻辑,大幅提高解析速度,书源有一些新规则后续会放出说明\n* 感谢大佬mabDc提出的优化方案\n* 修复bug\n\n**2019/05/06**\n* 电量显示放到电池图标内\n* url添加()作为保留字符,不编码\n* 修改目录加载失败时的报错\n\n**2019/05/05**\n* 正文规则$开头使用webView加载网页时js规则会在webView内执行,js会每秒执行一次直到返回值不为空\n\n**2019/05/04**\n* 添加txt目录正则管理\n* 其它一些优化\n\n**2019/04/30**\n* 添加书架的web接口\n\n**2019/04/29**\n* 应该解决部分手机翻页时停在中间不执行动画的问题\n* 更新build gradle 和一些库文件\n* 解决时间可能不刷新的问题\n\n**2019/04/26**\n* 优化翻页动画\n* 其它一些bug修复\n\n**2019/04/23**\n* 修复一个备份恢复bug\n* 修改发现界面,解决部分机型卡顿的问题\n\n**2019/04/22**\n* web写源功能全部完成\n* 修复一个搜索时可能崩溃的bug\n\n**2019/04/20**\n* web写书源功能基本完成,主界面菜单开启,感谢Antecer mabDc 的贡献\n* 修复网址加书的一个bug\n* js发现添加缓存\n* wifi分享书源完成,接收书源使用网络导入输入分享IP\n\n**2019/04/18**\n* 领红包开启高级功能保持7天\n\n**2019/04/17**\n* Modificator commented 侧边栏-主题，添加 E-Ink 模式\n* 减少调试日志打印量,目录正文下一页不打印日志\n\n**2019/04/16**\n* 重写书源调试,调试更方便直观\n* 书源调试可以输入url,添加扫描二维码按钮\n* 修复网格视图下拖动排序和切换发现页冲突的bug\n* 效验书源改为用搜索和发现效验,搜索使用关键词\"我的\"\n* 效验书源搜索和发现都不可用时才标识失效并禁用\n\n**2019/04/15**\n* xpath添加&&,||规则\n* 修复部分手机动画问题\n\n**2019/04/14**\n* 添加{{js}}规则写法会替换为js执行结果\n* 修复手动排序置顶失效的问题\n\n**2019/04/12**\n* 修复设置夜间模式会多次Recreate的问题\n* WebDav恢复时添加存储权限检查\n* 书源调试添加时间戳\n* 增加自定义js方法\n\n**2019/04/10**\n* 发现规则支持js生成规则文本, \\<js>\\</js>\n\n**2019/04/09**\n* 阅读时如果原书源被删除会自动换源\n* 修改JSoup解析文件，修复css规则解析 by mabDc\n* 其它一些优化\n\n**2019/04/08**\n* 优化内存使用\n* 优化替换净化速度\n\n**2019/04/01**\n* 修复耳机键不能唤起阅读的BUG\n* 搜索和发现可以不写baseUrl\n* 添加字间距调整\n\n**2019/03/30**\n* 仿真翻页背面加回\n* 修复在有些手机上切换夜间模式,书架不跟着切换的bug\n\n**2019/03/29**\n* 添加Tip边距调节\n* 更新fireBase版本\n\n**2019/03/26**\n* 添加RxJavaPlugins.setErrorHandler(Functions.emptyConsumer());防止rxJava内部错误导致崩溃\n* 去除打开软件时的权限获取提示\n* fix SourceDebug variable missing bug by mabDc was merged a day ago \n\n**2019/03/21**\n* 同时解决了背景色差和翻页问题\n\n**2019/03/20**\n* 修复了背景色差的问题\n\n**2019/03/19**\n* url编码之前判断是否已经编码\n* 添加缩进设置\n* 修改默认下载文件夹为Files,原cache文件夹会被清理软件清理\n* 修复bug\n\n**2019/03/16**\n* 搜索URL支持JS语法\n* 字符串基本上都写到values\\strings,方便制作多语言文件\n* 修复离线下载只能输入4位数的问题\n* 修复一些可能导致崩溃的BUG\n* 修复上版本判断url正则错误的bug\n* 修复一些WebDav不显示文件名的bug\n\n**2019/03/14**\n* 修复bug\n\n**2019/03/13**\n* 修复@header{}写到前面会导致结果出现BaseUrl + url的bug\n* webView添加25秒超时,防止一直加载\n* 修复trim()去不掉全角空格引起的导入书源失败问题\n\n**2019/03/12**\n* 修复bug\n* 优化动态网站的内容获取,可以更好地判断网站是否加载完成\n\n**2019/03/10**\n* 修复bug\n\n**2019/03/08**\n* 添加\\<js>\\</js>规则,可以写在前面或后面\n\n**2019/03/06**\n* 软件崩溃时会复制错误报告到剪贴板\n\n**2019/03/05**\n* 优化数字选择器\n* 边距调整上限改为100\n* 其它一些细节优化\n\n**2019/03/02**\n* 修复几个崩溃BUG\n* 减小防误触区域\n\n**2019/03/01**\n* 添加一个书架ToolBar自动隐藏开关\n* 目录去除倒序按钮,添加滚到底部和滚到顶部按钮\n\n**2019/02/28**\n* 合并Antecer提交的代码\n* 优化Table表格结构标签元素的选择结果,补全Table结构,否则会因为html标准丢失独立于table结构以外的tr和td标签.\n* 删除XPath匹配规则的 \",\" 逗号过滤代码,因为JsoupXpath库已经修复了这个BUG.\n\n**2019/02/27**\n* 修复一些本地txt问题\n* 修复使用阅读打开本地TXT,EPUB显示空白的问题\n* 更新JsoupXpath库,修复部分解析规则选择不到元素的BUG\n\n**2019/02/26**\n* 修复一个EPUB乱码的BUG\n\n**2019/02/24**\n* 合并Antecer和tumuyan提交的代码\n* 改善导入功能：\n1.由添加单个网址改善为批量添加网址（使用换行符分割）\n2.如果从其他软件分享链接到阅读，阅读自动导入；如果不包含网址，使用原有的搜索。\n* 修复Xpath规则匹配章节目录时候,匹配结果没有子元素时,获取目录列表失败的BUG。\n\n**2019/02/20**\n* 修复一些WebDav的兼容问题\n\n**2019/02/18**\n* WebDav备份与恢复\n\n**2019/02/15**\n* 更新XPath库,修复XPath排序混乱的问题\n* 其它一些优化\n\n**2019/02/12**\n* 换源界面添加搜索框\n\n**2019/02/10**\n* 尝试修复上版本部分手机闪退的问题\n\n**2019/02/09**\n* 添加setCookie支持\n* 合并Antecer提交的代码,解决XPath规则匹配的BUG,xpath可以不写@xpath:开头\n\n**2019/01/29**\n* 添加文字操作里搜索菜单的设置\n* AJAX动态网站延时两秒获取网站代码\n* 年前最后一次更新,过年休息暂停更新\n\n**2019/01/21**\n* JsonPath获取一个列表转成字符会自动添加换行符\n\n**2019/01/20**\n* 修复一个编码识别BUG\n\n**2019/01/19**\n* 隐藏侧边栏滚动条\n* 继续优化本地文件编码识别\n* 修复朗读时加载错误一直翻页的BUG\n\n**2019/01/18**\n* 更换了侧边栏图片,不知道哪个大神制作的\n* 更换一个编码识别库,优化本地文件编码识别\n\n**2019/01/16**\n* 升级到Android Studio 3.3\n* js添加两个方法,java.ajax(String) java.base64Decoder(String)\n\n**2019/01/14**\n* 修复屏幕常亮无效的BUG\n\n**2019/01/12**\n* 分类规则支持多个结果用&&连接多个规则,可添加字数的规则\n\n**2019/01/11**\n* 完善主题\n* 修复@get:{key}获取不到值的BUG\n* 由于2群被封,修改了入群信息\n\n**2019/01/10**\n* 完善主题,改不了颜色的暂时都变成了黑白色\n* 添加了恢复默认主题\n\n**2019/01/08**\n* 支持@Header:{key:value, key:value}定义http头\n* 支持@put:{key:rule, key:rule}存储变量\n* 支持@get:{key}获取变量\n* 所有自定义变量类型必须为String\n* 修复部分书源无法使用的问题\n\n**2019/01/07**\n* 继续完善主题\n* 基本上所有地址都支持搜索地址的规则了,可以写POST\n\n**2019/01/06**\n* 继续完善主题,添加自定义背景色\n\n**2019/01/05**\n* 继续完善主题,修复了阅读界面换源不显示的BUG\n\n**2019/01/04**\n* 主题功能实现切换ToolBar颜色\n\n**2019/01/01**\n* 祝大家元旦快乐\n* 修复不显示再按一次退出程序的BUG\n* 支持不写规则用js直接处理网页内容\n\n**2018/12/30**\n* 目录和书签可以滑动切换\n* 修复亮度调节的问题\n\n**2018/12/29**\n* 更换颜色选择器,可以直接输入颜色值\n* 细节优化\n\n**2018/12/28**\n* 修复菜单图标消失的bug\n\n**2018/12/27**\n* 项目迁移到androidX\n* 修复一些bug\n\n**2018/12/26**\n* 发现页长按书源名称增加弹出菜单,编辑 置顶 删除\n* json书源规则添加|| && %%分隔符,和原书源规则一样,如@JSon:$.minorCate||$.cat\n* 其它一些优化\n\n**2018/12/24**\n* 发现页支持样式切换，新样式卡顿可切换老样式\n* 修复4.4报错的问题\n\n**2018/12/23**\n* 细节优化\n\n**2018/12/21**\n* 发现页面改版\n\n**2018/12/19**\n* jsonPath获取字符支持此种写法xxx{$._id}yyy{$.chapter}zzz\n\n**2018/12/18**\n* 书源编辑界面添加常用工具条,可快速输入常用字符\n\n**2018/12/17**\n* 书源导入添加从二维码导入,可以是书源和网址,网址和从网络导入一个效果\n* 阅读界面的菜单可以直接领红包,高级功能失效时显示.\n* 修复删除粒子动画\n* 修复一些BUG\n\n**2018/12/15**\n* 添加书源调试,检查书源更方便\n\n**2018/12/14**\n* 修复优化背景图读取引起滚动背景不对的bug\n\n**2018/12/13**\n* 优化背景图读取,防止oom\n* 编辑书源界面改成中文\n\n**2018/12/12**\n* 添加登陆功能,可以阅读一些需要登陆的网站,需要配置一下书源的登陆地址,可以在书源菜单里登陆,也可以在阅读菜单里登陆\n* 将当前网址传到了js里,变量名为baseUrl\n\n**2018/12/11**\n* 合并**refgd**提交的代码,优化规则解析,使用LinkedHashSet去重提升速度\n* 一些BUG修复\n\n**2018/12/10**\n* 修复一些书源不对会引起崩溃的bug\n* 添加一个进入书架的快捷方式,防止直接进入最后阅读反复崩溃的问题 \n* 减小安装包体积\n* 感谢**沚水**给书源规则网站做了美化\n\n**2018/12/09**\n* 修复一些BUG\n* 需要js处理时再加载js引擎,解决了刷新速度变慢的问题\n* 给目录添加了去重处理,保留后面,比如有些源,最前面有最新章节会和后面重复,导致bug\n\n**2018/12/08**\n* 支持XPath语法,以@XPath:开头,语法见 http://www.w3school.com.cn/xpath/index.asp\n* 支持JSonPath语法,以@JSon:开头,语法见 https://blog.csdn.net/koflance/article/details/63262484\n* 支持用js处理结果,以@js:开头,结果变量为result 如 \"@JSon:$.link@js:\"http://chapterup.zhuishushenqi.com/chapter/\" + encodeURIComponent(result)\"\n* **注意** #替换规则在新语法下无法使用,新的语法用js处理结果,原有的规则不变\n* 去掉了隐藏书源,隐藏书源已经可以用JSonPath语法写出来\n* 新增的语法作为高级功能需要领红包才能开启\n\n**2018/12/07**\n* 修复一下朗读时手动翻页可能会翻两页的情况,概率没那么大了\n\n**2018/12/05**\n* 修复简介显示不全又不能滚动的bug\n* 合并M17764017422提交的代码,TTS初始化失败自动打开TTS设置\n\n**2018/12/04**\n* 为防止侵权,去掉了默认书源,需要书源请自行想办法\n\n**2018/12/03**\n* 修复本地txt文件丢失第一章节的BUG\n\n**2018/12/02**\n* 正在朗读的文字颜色变化\n\n**2018/12/01**\n* 下载通知合并为一个\n* 其它一些bug修复\n\n**2018/11/30**\n* 修复书籍详情页面在横屏显示不全时简介不能滚动的问题\n* 修复其它一些BUG\n* 合并atbest提交的代码\n* tag和class的位置可以使用负数，表示倒数\n* 添加正则表达式替换：split(\"#\") 第3项\n* lastRule添加ownText：只显示Element自己的文字，不显示child的文字\n* 添加Element过滤规则：用\">\"分隔，只选择含有特定class、id、tag或者text的Element\n* 判断搜索结果是否重定向到书籍页面，如果是的话就书籍规则获取内容\n\n**2018/11/29**\n* 修复一些BUG\n* 书籍详情页面在横屏显示不全时可滚动\n* 修复一个length=0;index=-1的报错\n\n**2018/11/24**\n* 修复一个背景问题导致的崩溃bug\n* %不再代表列表的最后一个,列表排除后面的用负值表示,-1表示倒数第一个,-2表示倒数第2个\n* 用%分隔列表规则,这些列表规则将会依次取数,如三个列表,先取列表1的第一个,再取列表2的第一个,再取列表3的第一个,再取列表1的第2个\n\n**2018/11/23**\n* 修复一些BUG\n* 发现页长按分组可编辑书源\n* 书籍详情菜单添加编辑书源\n* 搜索规则|char=escape 会模拟js escape方法进行编码\n\n**2018/11/21**\n* 按章节朗读,解决分页朗读时翻页处朗读不畅的问题\n* 语音引擎如果支持朗读进度播报,则能及时翻页,如不支持会等朗读完一段再判断是否翻页\n\n**2018/11/20**\n* 修复因添加规则引起的一些问题\n* 支持内容分页全是下一章的内容获取\n\n**2018/11/19**\n* 下一章节URL支持同时获取多个,如果有多个会依次获取,如果只有一个同以前\n* 搜索记录最多显示50条,可滚动\n* 书籍详情页面添加菜单按钮,不常用功能收集到菜单\n\n**2018/11/18**\n* 修复一个closed的bug\n* 书架不再显示进度,改为显示未读章节数,红色有更新,灰色无更新\n\n**2018/11/17**\n* 优化界面\n* 合并 atbest 的代码,书架界面修改\n* 合并 Invinciblelee 的代码,下载\n\n**2018/11/15**\n* 书源管理界面搜索的字调小了一些,可以显示全书源数量\n* 更换一些图标,感谢**爱发猫表情的新奥尔良烤鲟鱼堡**\n* 修复EPUB内容与章节不对应的问题\n* 修复一些BUG\n\n**2018/11/14**\n* 书架添加全部书籍分类\n* 更换一些图标,由**爱发猫表情的新奥尔良烤鲟鱼堡**制作\n* 添加了一个导航栏变色开关,有些手机不好适配,麻烦\n* 关于里添加了一个分享软件的按钮,可以二维码分享\n* 合并 atbest 代码,添加EPUB封面\n\n**2018/11/13**\n* 修复换源搜索所有源的问题\n* 换源下拉刷新还是会重新搜索\n* 合并 atbest 代码,修复拼音排序\n* 解决一些导致崩溃的BUG\n* 修复换源里搜索不到的问题\n\n**2018/11/12**\n* 合并 atbest 代码\n* 全新的滚动模式\n* 独立本地书架\n* 优化朗读时淡入淡出\n* Epub滚动模式显示封面\n* 修复Epub乱码问题\n* 支持使用高清背景图片\n* 其他\n\n**2018/11/11**\n* 换源时刷新不会删除已经搜索到的源,只会更新并搜索没有搜索到的书源\n* 换源添加长按菜单\n* 修复亮度问题\n\n**2018/11/10**\n* 一些优化,和bug修复\n* 书源添加排序选项,可选择手动排序,智能排序,拼音排序\n* 目录拷贝 Invinciblelee 的代码,修改了界面,添加倒序\n* 版本号改用日期,首段大版本号,中间年份,尾段日期,精确到小时\n* 封面全采用圆角,调整颜色\n\n**2018/11/7**\n* 添加后台自动更新换源里的最新章节\n\n**2018/11/6**\n* 修复音量键翻页没有动画的bug\n* 修复缓存路径自动变为默认的bug\n\n**2018/11/5**\n* 本地书籍加入编码设置,如遇乱码可自行设置编码\n* 优化EPUB目录读取,可以读取多级目录了\n* 解决本地TXT章节丢失的问题\n* 其它一些优化\n\n**2018/11/4**\n* 修复设置有时弹不出来的bug\n* 修改TXT目录正则后自动刷新目录\n\n**2018/11/3**\n* 解决一些BUG\n* 加入自定义TXT目录正则\n* 拷贝https://github.com/Invinciblelee的代码增加EPUB文件支持\n* 优化阅读设置,解决隐藏虚拟操作栏下面显示空白的问题\n\n**2018/11/2**\n* 解决一些BUG\n* 如何查看书籍详情？\n* 按阅读时间或更新时间排序的，长按书籍；手动排序的，列表视图单击或长按书籍封面，网格视图单击书籍名称。\n* 阅读界面菜单添加更新目录\n\n**2018/11/1**\n* 解决替换影响速度的问题\n\n**2018/10/31**\n* 解析章节放到后台,进一步提升翻页流畅度\n* 合并 https://github.com/atbest 提交的代码\n* 净化替换规则可选是否为正则表达式\n* 书籍详情界面显示剩余章节数\n* 恢复备份以后不重复显示更新日志\n* 朗读时淡入淡出\n* 仿真翻页背景虚化\n* 其他Bug修复及优化\n\n**2018/10/26**\n* 优化翻页，更流畅\n\n**2018/10/23**\n* 合并 https://github.com/atbest 提交的代码, 修复恢复设置不能恢复阅读背景之类的\n* 添加记住书架分组,重新打开时自动进入退出时的分组\n* 添加显示所有发现的设置\n* 优化书籍详情,采用自适应布局\n* 其它一些优化\n\n**2018/10/20**\n* 修复点击返回按钮闪退的BUG\n* 禁用预解析下一章内容,看还有没有翻页时窜章的,如果有请反馈,我好确认问题所在\n* 修复从发现搜索时不直接显示搜索结果的问题\n* 添加滚动时也可以点击翻页\n* 修复目录已下载加粗关闭重开会消失的问题\n* 其它一些优化\n\n**2018/10/19**\n* 升级SDK\n* 修复升级SDK引起的一些问题\n* 优化分区切换\n* 关于里添加常见问题\n* 添加自动备份\n* 解决发现滚动不触发动画滚动不到底的问题\n* 合并代码,其它一些优化\n\n**2018/10/17**\n* 优化书架分区切换\n* 合并代码,其它一些优化\n\n**2018/10/16**\n* 合并 大古队员 的代码\n* 界面改版\n\n**2018/10/15**\n* 合并 https://github.com/atbest 提交的代码\n* 缓存文件添加目录,修改缓存文件名,方便需要合并文件的人\n* 添加禁止更新功能\n\n**2018/10/14**\n* 合并 https://github.com/atbest 提交的代码\n* 添加tip是否跟随边距调整的开关\n* 快速滚动条修改\n* 修复目录加粗可能在部分机型上不起作用的BUG\n* 复制了一些 https://github.com/Invinciblelee 的代码\n\n**2018/10/13**\n* 合并 https://github.com/atbest 提交的代码,修复一些BUG,滚动模式背景不随内容滚动\n* 稍微修改一下界面\n* 领支付宝红包彩蛋修改为持续3天\n\n**2018/10/10**\n* 修复可能引起的跳章的问题\n* 电量添加百分比\n\n**2018/10/08**\n* 修复音量翻页开关\n* 合并 https://github.com/atbest 提交的代码\n\n**2018/10/03**\n* 添加朗读时音量翻页开关\n* 关闭滚动模式的惯性滚动\n* 更新前先删除原有目录,看能不能解决双目录问题\n\n**2018/10/02**\n* 添加更新检查,可直接下载最新版本\n* 朗读时自动关闭音量翻页\n* 修复一些BUG\n\n**2018/10/01**\n* 添加一个自动翻页倒计时条\n* 本地导入书源可以选择.json文件\n* 修复一些BUG\n\n**2018/09/29**\n* 修复一些BUG\n\n**2018/09/26**\n* 为当前使用的字体添加标志\n* 阅读界面菜单添加禁用书源\n* 添加自定义缓存路径\n* 其它一些优化\n\n**2018/09/25**\n* 换源时智能选择对应的章节\n* 其它一些优化和BUG修复\n* 感谢 https://github.com/atbest 提交的代码\n\n**2018/09/23**\n* 修复下载时线程过多导致的崩溃\n* 修复无网络一直加载的问题\n\n**2018/09/21**\n* 修复一些BUG\n* 书源管理添加选择分组功能\n\n**2018/09/20**\n* 优化html取值\n\n**2018/09/19**\n* 修复一些BUG\n* 取值规则添加&分隔符,合并所有取值\n* 取值规则添加children,获取所有子标签\n* 取值规则添加html,获取所有文本\n\n**2018/09/17**\n* 解决更换字体时文字有可能不对齐的问题\n\n**2018/09/14**\n* 添加内容分页获取规则\n\n**2018/09/13**\n* 内容可以用TalkBack朗读\n* 修复一个BUG,String input must not be null\n\n**2018/09/12**\n* 替换添加作用于,可填写书名和书源url,不填应用到所有\n\n**2018/09/10**\n* 修复搜索时卡顿\n* 添加直接打开最近阅读设置\n\n**2018/09/07**\n* 添加本地时点路径可以选择SD卡\n\n**2018/09/06**\n* 点击作者会搜索作者\n* 修复切换手动排序长按封面可能不出书籍信息的BUG\n* 其它一些优化\n\n**2018/09/04**\n* 添加自定义封面\n\n**2018/08/31**\n* 添加自动翻页功能\n\n**2018/08/29**\n* 尝试修复选择图片背景闪退问题\n* 尝试修复搜索闪退问题\n\n**2018/08/27**\n* 去除了获取内容时的空格替换\n* 搜索添加停止按钮\n* 修复一些bug\n\n**2018/08/23**\n* 修复了本地书籍双目录的bug\n* 其它一些修复\n\n**2018/08/20**\n* 添加段距调整\n* 修复bug\n* 无动画时手指拿起才会翻页\n\n**2018/08/14**\n* 背景图不再放到缓存,背景图经过处理减少卡顿\n* 备份可以备份设置,备份文件夹改变为YueDu\n\n**2018/08/14**\n* 更改了选择字体的方式\n* 修复一些BUG\n\n**2018/08/13**\n* 优化启动速度,启动时不读取详细目录\n* 修复一些BUG\n\n**2018/08/09**\n* 优化修复bug\n\n**2018/08/09**\n* 修改书架界面\n* 修改本地导入界面\n* 修复BUG\n\n**2018/08/08**\n* 修复BUG,本地书籍乱码,点击翻页选项无效等问题\n\n**2018/08/06**\n* 重写阅读界面代码,实现多种翻页模式\n"
  },
  {
    "path": "app/src/main/assets/web/bookshelf.css",
    "content": "﻿html, body {\n    height: 100%;\n    margin: 0;\n}\n\n.hide {\n    display: none;\n}\n\n.top, .showchapter, .hidebooks {\n    width: 60px;\n    height: 50px;\n    position: absolute;\n    right: 30px;\n    bottom: 30px;\n    color: black;\n    font-size: 28px;\n    background-color: #ddd;\n    opacity: 0.85;\n}\n\n.top {\n    bottom: 150px;\n}\n\n.showchapter {\n    bottom: 90px;\n    bottom: 90px;\n}\n\n.address {\n    width: 270px;\n}\n\n.nav {\n    border-bottom: solid 1px #ccc;\n}\n\ninput, button {\n    width: 110px;\n    line-height: 34px;\n    background-color: #eee;\n    color: #555;\n    border: none;\n    margin: 10px 5px;\n    font-weight: 500;\n    border-radius: 2px;\n    outline: none;\n    cursor: pointer;\n}\n\ninput {\n    padding: 0 10px;\n    cursor: text;\n}\n\n    input:hover, button:hover {\n        border-color: #aaa;\n        background-color: #efefef;\n        color: #222;\n        outline: solid 1px #ccc;\n    }\n\n.allcontent {\n    height: calc(100% - 60px);\n}\n\n.allscreen {\n    height: 100%\n}\n\n.books > div {\n    display: inline-block;\n    margin: 10px;\n    vertical-align: top;\n    border: solid 1px #ddd;\n}\n\n.read > .books {\n    width: 420px;\n    float: left;\n    height: 100%;\n    overflow: auto;\n    border-right: solid 1px #ccc;\n}\n\n    .read > .books > div {\n        margin-right: 0;\n        border-right: none;\n    }\n\n\n.more {\n    overflow-y: auto;\n    height: 100%;\n    display: none;\n}\n\n.read .more {\n    display: block;\n}\n\n.books > div > img {\n    width: 120px;\n    height: 180px;\n    float: left;\n    margin-right: 10px;\n    cursor: pointer;\n}\n\n.info {\n    padding: 10px 20px 0 20px;\n    width: 600px;\n    margin: 0 auto;\n}\n\n    .info > img {\n        width: 600px;\n        height: 900px;\n    }\n\n    .info p {\n        line-height: 1.5;\n        text-align: justify;\n        margin: 0;\n    }\n\n.books tr:nth-child(n+2) td {\n    border-top: solid 1px #999;\n}\n\n.books td:nth-child(1) {\n    vertical-align: top;\n    width: 50px;\n}\n\n.books td:nth-child(2) {\n    vertical-align: top;\n    width: 200px;\n}\n\n.clear {\n    clear: both;\n}\n\n.chapter {\n    margin: 10px;\n    max-height: 500px;\n    overflow-y: auto;\n    border-top: solid 1px #333;\n    border-bottom: solid 1px #333;\n}\n\n    .chapter button {\n        width: 230px;\n        text-align: left;\n        text-indent: 14px;\n        margin: 10px 4px;\n    }\n\n\n.content {\n    padding: 20px;\n    text-align: justify;\n    min-height: 1000px;\n    padding-bottom: 200px;\n}\n\n    .content h2 {\n        font-family: \"Microsoft YaHei\",微软雅黑,\"MicrosoftJhengHei\",华文细黑,STHeiti,MingLiu;\n        font-weight: 500;\n        text-align: center;\n        line-height: 100px;\n        font-size: 40px;\n        margin: 0;\n    }\n"
  },
  {
    "path": "app/src/main/assets/web/bookshelf.html",
    "content": "﻿<!DOCTYPE html>\n<html>\n\n<head>\n\t<meta charset=\"utf-8\" />\n\t<title>阅读书架</title>\n\t<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n\t<link href=\"bookshelf.css\" rel=\"stylesheet\" />\n</head>\n\n<body>\n\t<button id=\"top\" class=\"top\">↑</button>\n\t<button id=\"showchapter\" class=\"showchapter\">≡</button>\n\t<button id=\"hidebooks\" class=\"hidebooks\">❑</button>\n\t<div class=\"nav\">\n\t\t<button id=\"back\">返回</button>\n\t\t<button id=\"type\">所有书籍 ▼</button>\n\t\t<button id=\"sort\">手动排序 ▼</button>\n\t\t<button id=\"setting\">阅读设置</button>\n\t\t<input type=\"text\" class=\"address\" id=\"address\" title=\"阅读APP地址或IP\" value=\"\" />\n\t\t<button id=\"refresh\">重新加载</button>\n\t</div>\n\n\t<div class=\"allcontent\" id=\"allcontent\">\n\t\t<div id=\"books\" class=\"books\"></div>\n\t\t<div id=\"more\" class=\"more\">\n\t\t\t<div id=\"info\" class=\"info\"></div>\n            <div class=\"clear\"></div>\n            <div id=\"chapter\" class=\"chapter\"></div>\n            <div id=\"content\" class=\"content\"></div>\n            <div id=\"page\" class=\"button\">\n                <center><button id='up'>上一章</button><button id='down'>下一章</button></center>\n            </div>\n        </div>\n    </div>\n    <script src=\"bookshelf.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "app/src/main/assets/web/bookshelf.js",
    "content": "﻿var $ = document.querySelector.bind(document)\n    , $$ = document.querySelectorAll.bind(document)\n    , $c = document.createElement.bind(document)\n    , randomImg = \"http://api.mtyqx.cn/api/random.php\"\n    , randomImg2 = \"http://img.xjh.me/random_img.php\"\n    , books\n    ;\n\nvar now_chapter = -1;\nvar sum_chapter = 0;\n\nvar formatTime = value => {\n    return new Date(value).toLocaleString('zh-CN', {\n        hour12: false, year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\"\n    }).replace(/\\//g, \"-\");\n};\n\nvar apiMap = {\n    \"getBookshelf\": \"/getBookshelf\",\n    \"getChapterList\": \"/getChapterList\",\n    \"getBookContent\": \"/getBookContent\",\n    \"saveBook\": \"/saveBook\"\n};\n\nvar apiAddress = (apiName, url) => {\n    let address = $('#address').value || window.location.host;\n    if (!(/^http|^\\/\\//).test(address)) {\n        address = \"//\" + address;\n    }\n    if (!(/:\\d{4,}/).test(address.split(\"//\")[1].split(\"/\")[0])) {\n        address += \":1122\";\n    }\n    localStorage.setItem('address', address);\n    return address + apiMap[apiName] + (url ? \"?url=\" + encodeURIComponent(url) : \"\");\n};\n\nvar init = () => {\n    $('#allcontent').classList.remove(\"read\");\n    $('#books').innerHTML = \"\";\n    fetch(apiAddress(\"getBookshelf\"), { mode: \"cors\" })\n        .then(res => res.json())\n        .then(data => {\n            if (!data.isSuccess) {\n                alert(getBookshelf.errorMsg);\n                return;\n            }\n            books = data.data.sort((book1, book2) => book1.serialNumber - book2.serialNumber);\n            books.forEach((book, i) => {\n                let bookDiv = $c(\"div\");\n                let img = $c(\"img\");\n                img.src = book.bookInfoBean.coverUrl || randomImg;\n                img.setAttribute(\"data-series-num\", i);\n                bookDiv.appendChild(img);\n                bookDiv.innerHTML += `<table><tbody>\n                                <tr><td>书名：</td><td>${book.bookInfoBean.name}</td></tr>\n                                <tr><td>作者：</td><td>${book.bookInfoBean.author}</td></tr>\n                                <tr><td>阅读：</td><td>${book.durChapterName}<br>${formatTime(book.finalDate)}</td></tr>\n                                <tr><td>更新：</td><td>${book.lastChapterName}<br>${formatTime(book.finalRefreshData)}</td></tr>\n                                <tr><td>来源：</td><td>${book.bookInfoBean.origin}</td></tr>\n                                </tbody></table>`;\n                $('#books').appendChild(bookDiv);\n            });\n            $$('#books img').forEach(bookImg =>\n                bookImg.addEventListener(\"click\", () => {\n                    now_chapter = -1;\n                    sum_chapter = 0;\n                    $('#allcontent').classList.add(\"read\");\n                    var book = books[bookImg.getAttribute(\"data-series-num\")];\n                    $(\"#info\").innerHTML = `<img src=\"${bookImg.src}\">\n                                        <p>　　来源：${book.bookInfoBean.origin}</p>\n                                        <p>　　书名：${book.bookInfoBean.name}</p>\n                                        <p>　　作者：${book.bookInfoBean.author}</p>\n                                        <p>阅读章节：${book.durChapterName}</p>\n                                        <p>阅读时间：${formatTime(book.finalDate)}</p>\n                                        <p>最新章节：${book.lastChapterName}</p>\n                                        <p>检查时间：${formatTime(book.finalRefreshData)}</p>\n                                        <p>　　简介：${book.bookInfoBean.introduce.trim().replace(/\\n/g, \"<br>\")}</p>`;\n                    window.location.hash = \"\";\n                    window.location.hash = \"#info\";\n                    $(\"#content\").innerHTML = \"章节列表加载中...\";\n                    $(\"#chapter\").innerHTML = \"\";\n                    fetch(apiAddress(\"getChapterList\", book.noteUrl), { mode: \"cors\" })\n                        .then(res => res.json())\n                        .then(data => {\n                            if (!data.isSuccess) {\n                                alert(data.errorMsg);\n                                $(\"#content\").innerHTML = \"章节列表加载失败！\";\n                                return;\n                            }\n\n                            data.data.forEach(chapter => {\n                                let ch = $c(\"button\");\n                                ch.setAttribute(\"data-url\", chapter.durChapterUrl);\n                                ch.setAttribute(\"data-index\", chapter.durChapterIndex);\n                                ch.setAttribute(\"title\", chapter.durChapterName);\n                                ch.innerHTML = chapter.durChapterName.length > 15 ? chapter.durChapterName.substring(0, 14) + \"...\" : chapter.durChapterName;\n                                $(\"#chapter\").appendChild(ch);\n                            });\n                            sum_chapter = data.data.length;\n                            $('#chapter').scrollTop = 0;\n                            $(\"#content\").innerHTML = \"章节列表加载完成！\";\n                        });\n\n                }));\n        });\n};\n\n$(\"#back\").addEventListener(\"click\", () => {\n    if (window.location.hash === \"#content\") {\n        window.location.hash = \"#chapter\";\n    } else if (window.location.hash === \"#chapter\") {\n        window.location.hash = \"#info\";\n    } else {\n        $('#allcontent').classList.remove(\"read\");\n    }\n});\n\n$(\"#refresh\").addEventListener(\"click\", init);\n\n$('#hidebooks').addEventListener(\"click\", () => {\n    $(\"#books\").classList.toggle(\"hide\");\n    $(\".nav\").classList.toggle(\"hide\");\n    $(\"#allcontent\").classList.toggle(\"allscreen\");\n});\n\n$('#top').addEventListener(\"click\", () => {\n    window.location.hash = \"\";\n    window.location.hash = \"#info\";\n});\n\n$('#showchapter').addEventListener(\"click\", () => {\n    window.location.hash = \"\";\n    window.location.hash = \"#chapter\";\n});\n\n$('#up').addEventListener('click', e => {\n    if (now_chapter > 0) {\n        now_chapter--;\n        let clickEvent = document.createEvent('MouseEvents');\n        clickEvent.initEvent(\"click\", true, false);\n        $('[data-index=\"' + now_chapter + '\"]').dispatchEvent(clickEvent);\n    } else if (now_chapter == 0) {\n        alert(\"已经是第一章了^_^!\")\n    } else {\n\n    }\n});\n\n$('#down').addEventListener('click', e => {\n    if (now_chapter > -1) {\n        if (now_chapter < sum_chapter - 1) {\n            now_chapter++;\n            let clickEvent = document.createEvent('MouseEvents');\n            clickEvent.initEvent(\"click\", true, false);\n            $('[data-index=\"' + now_chapter + '\"]').dispatchEvent(clickEvent);\n\n        } else {\n            alert(\"已经是最后一章了^_^!\")\n        }\n    }\n});\n\n$('#chapter').addEventListener(\"click\", (e) => {\n    if (e.target.tagName === \"BUTTON\") {\n        var url = e.target.getAttribute(\"data-url\");\n        var index = e.target.getAttribute(\"data-index\");\n        var name = e.target.getAttribute(\"title\");\n        if (!url) {\n            alert(\"未取得章节地址\");\n        }\n        now_chapter = parseInt(index);\n        $(\"#content\").innerHTML = \"<p>\" + name + \" 加载中...</p>\";\n        fetch(apiAddress(\"getBookContent\", url), { mode: \"cors\" })\n            .then(res => res.json())\n            .then(data => {\n                if (!data.isSuccess) {\n                    alert(data.errorMsg);\n                    $(\"#content\").innerHTML = \"<p>\" + name + \" 加载失败！</p>\";\n                    return;\n                }\n                var content = data.data.trim().split(\"\\n\\n\");\n                if (content.length === 2) {\n                    $(\"#content\").innerHTML = `<h2>${content[0]}</h2>　　（全文 ${content[1].length} 字）<br><br>　　` + content[1].trim().replace(/\\n/g, \"<br><br>\");\n                } else {\n                    $(\"#content\").innerHTML = `<h2>${name || e.target.innerHTML}</h2>　　（全文 ${data.data.length} 字）<br><br>　　` + data.data.trim().replace(/\\n/g, \"<br><br>\");\n                }\n                window.location.hash = \"\";\n                window.location.hash = \"#content\";\n            });\n    }\n});\n\n$('#address').setAttribute(\"placeholder\", \"阅读APP地址或IP：\" + window.location.host);\nif (!$('#address').value && typeof localStorage && localStorage.getItem('address')) {\n    $('#address').value = localStorage.getItem('address');\n}\ninit();\n"
  },
  {
    "path": "app/src/main/assets/web/index.css",
    "content": "body {\n\tmargin: 0;\n}\n.editor {\n\tdisplay: flex;\n\talign-items: stretch;\n}\n.setbox,\n.menu,\n.outbox {\n\tflex: 1;\n\tdisplay: flex;\n\tflex-flow: column;\n\tmax-height: 100vh;\n\toverflow-y: auto;\n}\n.menu {\n\tjustify-content: center;\n\tmax-width: 90px;\n\tmargin: 0 5px;\n}\n.menu .button {\n\twidth: 90px;\n\theight: 30px;\n\tmin-height: 30px;\n\tmargin: 5px 0px;\n\tcursor: pointer;\n}\n@keyframes stroker {\n\t0% {\n\t\tstroke-dashoffset: 0\n\t}\n\t100% {\n\t\tstroke-dashoffset: -240\n\t}\n}\n.button rect {\n\twidth: 100%;\n\theight: 100%;\n\tfill: transparent;\n\tstroke: #666;\n\tstroke-width: 2px;\n}\n.button rect.busy {\n\tstroke: #fD1850;\n\tstroke-dasharray: 30 90;\n\tanimation: stroker 1s linear infinite;\n}\n.button text {\n\ttext-anchor: middle;\n\tdominant-baseline: middle;\n}\n.setbox {\n\tmin-width: 40em;\n}\n.rules,\n.tabbox {\n\tflex: 1;\n\tdisplay: flex;\n\tflex-flow: column;\n}\n.rules>* {\n\tdisplay: flex;\n\tmargin: 2px 0;\n}\n.rules textarea {\n\tflex: 1;\n\tmargin-left: 5px;\n}\n.rules>*,\n.rules>*>div,\n.rules textarea {\n\tmin-height: 1em;\n}\ntextarea {\n\tword-break: break-all;\n}\n.tabtitle {\n\tdisplay: flex;\n\tz-index: 1;\n\tjustify-content: flex-end;\n}\n.tabtitle>div {\n\tcursor: pointer;\n\tpadding: 1px 10px 0 10px;\n\tborder-bottom: 3px solid transparent;\n\tfont-weight: bold;\n}\n.tabtitle>.this {\n\tcolor: #4f9da6;\n\tborder-bottom-color: #4EBBE4;\n}\n.tabbody {\n\tflex: 1;\n\tdisplay: flex;\n\tmargin-top: -1px;\n\tborder: 1px solid #A9A9A9;\n\theight: 0;\n}\n.tabbody>* {\n\tflex: 1;\n\tflex-flow: column;\n\tdisplay: none;\n}\n.tabbody>.this {\n\tdisplay: flex;\n}\n.tabbody>*>.titlebar{\n\tdisplay: flex;\n}\n.tabbody>*>.titlebar>*{\n\tflex: 1;\n\tmargin: 1px 1px 1px 1px;\n}\n.tabbody>*>.context {\n\tflex: 1;\n\tflex-flow: column;\n\tborder: 0;\n\tpadding: 5px;\n\toverflow-y: auto;\n}\n.tabbody>*>.inputbox{\n\tborder: 0;\n\tborder-bottom: #A9A9A9 solid 1px;\n\theight: 15px;\n\ttext-align:center;\n}\n.link>* {\n\tdisplay: flex;\n\tmargin: 5px;\n\tborder-bottom: 1px solid;\n\ttext-decoration: none;\n}\n#RuleList>label>* {\n\tbackground: #eee;\n\tpadding-left: 3px;\n\tmargin: 2px 0;\n\tcursor: pointer;\n}\n#RuleList input[type=radio] {\n\tdisplay: none;\n}\n#RuleList input[type=\"radio\"]:checked+* {\n\tbackground: #15cda8;\n}\n.isError {\n\tcolor: #FF0000;\n}"
  },
  {
    "path": "app/src/main/assets/web/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n\t<meta charset=\"UTF-8\">\n\t<title>书源编辑器v3.8</title>\n\t<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/favicon.ico\" />\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/index.css\" />\n\t<script type=\"text/javascript\" src=\"/index.js\" defer></script>\n</head>\n\n<body>\n\t<div class=\"editor\">\n\t\t<div class=\"setbox\">\n\t\t\t<div class=\"rules\">\n\t\t\t\t<div><b>书源基础信息</b></div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书源名称:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"bookSourceName\" placeholder=\"书源名称(bookSourceName) | 会显示在书源列表\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书源分组:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"bookSourceGroup\" placeholder=\"书源分组(bookSourceGroup) | 描述书源的特征信息\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书源类型:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"bookSourceType\" placeholder=\"书源类型(bookSourceType) | 有声读物(填写AUDIO)或文本读物(留空)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书源域名:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"bookSourceUrl\" placeholder=\"书源URL(bookSourceUrl) | 通常填写网站主页(标头不可省略),例: https://www.qidian.com\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>登录网页:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"loginUrl\" placeholder=\"登录URL(loginUrl) | 填写网站登录网址,仅在需要登录的书源有用\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div><b>书籍发现规则</b></div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>发现菜单:</div>\n\t\t\t\t\t<textarea rows=\"5\" id=\"ruleFindUrl\" placeholder=\"发现分类菜单规则(ruleFindUrl),将显示在发现菜单&#10;每行一条发现分类(网址域名可省略):&#10;名称1::网址(Url)1&#10;名称2::网址(Url)2&#10;...\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>结果列表:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindList\" placeholder=\"发现页列表规则(ruleFindList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍名称:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindName\" placeholder=\"发现页书名规则(ruleFindName) | 选择节点书名 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍作者:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindAuthor\" placeholder=\"发现页作者规则(ruleFindAuthor) | 选择节点作者 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍分类:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindKind\" placeholder=\"发现页分类规则(ruleFindKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>最新章节:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindLastChapter\" placeholder=\"发现页最新章节规则(ruleFindLastChapter) | 选择节点最新章节 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>简介内容:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindIntroduce\" placeholder=\"发现页简介规则(ruleFindIntroduce) | 选择节点书籍简介 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>封面链接:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindCoverUrl\" placeholder=\"发现页封面规则(ruleFindCoverUrl) | 选择节点书籍封面 (规则结果为Url)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>详情链接:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleFindNoteUrl\" placeholder=\"发现页详情规则(ruleFindNoteUrl) | 选择书籍详情页网址 (规则结果为Url)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div><b>书籍搜索规则</b></div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>搜索网址:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchUrl\" placeholder=\"搜索网址(ruleSearchUrl) | [域名可省略]/search.php@kw=searchKey|char=utf-8\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>结果验证:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleBookUrlPattern\" placeholder=\"搜索页URL验证(ruleBookUrlPattern) | 正则验证URL是否为详情页,成功则跳过搜索页解析\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>结果列表:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchList\" placeholder=\"搜索页列表规则(ruleSearchList) | 选择书籍节点 (规则结果为List&lt;Element&gt;)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍名称:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchName\" placeholder=\"搜索页书名规则(ruleSearchName) | 选择节点书名 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍作者:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchAuthor\" placeholder=\"搜索页作者规则(ruleSearchAuthor) | 选择节点作者 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍分类:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchKind\" placeholder=\"搜索页分类规则(ruleSearchKind) | 选择节点分类信息 (规则结果为List&lt;String&gt;)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>最新章节:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchLastChapter\" placeholder=\"搜索页最新章节规则(ruleSearchLastChapter) | 选择节点最新章节 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>简介内容:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchIntroduce\" placeholder=\"搜索页简介规则(ruleSearchIntroduce) | 选择节点书籍简介 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>封面链接:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchCoverUrl\" placeholder=\"搜索页封面规则(ruleSearchCoverUrl) | 选择节点书籍封面 (规则结果为Url)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>详情链接:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleSearchNoteUrl\" placeholder=\"搜索页详情规则(ruleSearchNoteUrl) | 选择书籍详情页网址 (规则结果为Url)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div><b>书籍详情规则</b></div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>页面处理:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleBookInfoInit\" placeholder=\"详情页信息预处理(ruleBookInfoInit) | 用于加速详情信息检索\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍名称:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleBookName\" placeholder=\"书名规则(ruleBookName) | 选择详情页书名 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍作者:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleBookAuthor\" placeholder=\"作者规则(ruleBookAuthor) | 选择详情页作者 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>书籍分类:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleBookKind\" placeholder=\"分类规则(ruleBookKind) | 选择详情页分类信息 (规则结果为List&lt;String&gt;)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>最新章节:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleBookLastChapter\" placeholder=\"最新章节规则(ruleBookLastChapter) | 选择详情页最新章节 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>简介内容:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleIntroduce\" placeholder=\"简介规则(ruleIntroduce) | 选择详情页书籍简介 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>封面链接:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleCoverUrl\" placeholder=\"封面规则(ruleCoverUrl) | 选择详情页书籍封面 (规则结果为Url)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>目录链接:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleChapterUrl\" placeholder=\"目录URL规则(ruleChapterUrl) | 选择目录页网址 (规则结果为Url, 与详情页相同时可省略)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div><b>目录列表规则</b></div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>目录翻页:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleChapterUrlNext\" placeholder=\"目录下一页规则(ruleChapterUrlNext) | 选择目录下一页链接 (规则结果为List&lt;Url&gt;)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>目录列表:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleChapterList\" placeholder=\"目录列表规则(ruleChapterList) | 选择目录列表的章节节点 (规则结果为List&lt;Element&gt;)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>章节名称:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleChapterName\" placeholder=\"章节名称规则(ruleChapterName) | 选择章节名称 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>章节链接:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleContentUrl\" placeholder=\"章节URL规则(ruleContentUrl) | 选择章节链接 (规则结果为Url)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div><b>正文阅读规则</b></div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>章节正文:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleBookContent\" placeholder=\"正文规则(ruleBookContent) | 选择正文内容 (规则结果为String)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>正文翻页:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"ruleContentUrlNext\" placeholder=\"正文翻页URL规则(ruleContentUrlNext) | 选择下一分页(不是下一章)链接 (规则结果为Url)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div><b>其它规则</b></div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>浏览标识:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"httpUserAgent\" placeholder=\"浏览器UA(HttpUserAgent) | 浏览器标识:User-Agent (可选)\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>排序编号:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"serialNumber\" placeholder=\"整数: 0~N (可选,默认0) | 数字越小越靠前\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>搜索权重:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"weight\" placeholder=\"整数: 0~N (可选,默认0) | 数字越大越靠前\"></textarea>\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div>是否启用:</div>\n\t\t\t\t\t<textarea rows=\"1\" id=\"enable\" placeholder=\"默认启用=true,手动启用=false (可选,默认true)\"></textarea>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t\t<div class=\"menu\">\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">⇈推送书源</text>\n\t\t\t\t<rect id=\"push\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">⇊拉取书源</text>\n\t\t\t\t<rect id=\"pull\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">⋘编辑书源</text>\n\t\t\t\t<rect id=\"editor\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">⋙生成书源</text>\n\t\t\t\t<rect id=\"conver\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">✗清空表单</text>\n\t\t\t\t<rect id=\"initial\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">↶撤销操作</text>\n\t\t\t\t<rect id=\"undo\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">↷重做操作</text>\n\t\t\t\t<rect id=\"redo\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">⇏调试书源</text>\n\t\t\t\t<rect id=\"debug\"></rect>\n\t\t\t</svg>\n\t\t\t<svg class=\"button\">\n\t\t\t\t<text x=\"50%\" y=\"55%\">✓保存书源</text>\n\t\t\t\t<rect id=\"accept\"></rect>\n\t\t\t</svg>\n\t\t</div>\n\t\t<div class=\"outbox\">\n\t\t\t<div class=\"tabbox\">\n\t\t\t\t<div class=\"tabtitle\">\n\t\t\t\t\t<div name=\"编辑书源\" class=\"tab1 this\">编辑书源</div>\n\t\t\t\t\t<div name=\"调试书源\" class=\"tab2\">调试书源</div>\n\t\t\t\t\t<div name=\"书源列表\" class=\"tab3\">书源列表</div>\n\t\t\t\t\t<div name=\"帮助信息\" class=\"tab4\">帮助信息</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"tabbody\">\n\t\t\t\t\t<div class=\"tab1 this\">\n\t\t\t\t\t\t<textarea class=\"context\" id=\"RuleJsonString\" placeholder=\"这里输出序列化的JSON数据,可直接导入'阅读'APP\"></textarea>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"tab2\">\n\t\t\t\t\t\t<input type=\"text\" class=\"inputbox\" id=\"DebugKey\" placeholder=\"我的\">\n\t\t\t\t\t\t<textarea class=\"context\" id=\"DebugConsole\" placeholder=\"这里用于输出调试信息\"></textarea>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"tab3\">\n\t\t\t\t\t\t<div class=\"titlebar\">\n\t\t\t\t\t\t\t<button id=\"Import\">导入书源文件</button>\n\t\t\t\t\t\t\t<button id=\"Export\">导出书源文件</button>\n\t\t\t\t\t\t\t<button id=\"Delete\">删除选中书源</button>\n\t\t\t\t\t\t\t<button id=\"ClrAll\">清空当前列表</button>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"context\" id=\"RuleList\"></div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div class=\"tab4\">\n\t\t\t\t\t\t<div class=\"context link\">\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"https://gedoor.github.io/MyBookshelf/sourcerule.html\">官方书源教程</a>\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"https://zhuanlan.zhihu.com/p/29436838\">Xpath基础教程</a>\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"https://zhuanlan.zhihu.com/p/32187820\">Xpath高级教程</a>\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"https://www.w3cschool.cn/regex_rmjc/?\">正则表达式教程</a>\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"https://regexr.com/\">正则表达式在线验证工具</a>\n\t\t\t\t\t\t\t<div>^$()[]{}.?+*| 这些是Java正则特殊符号,匹配需转义\n\t\t\t\t\t\t\t\t<br>(?s) 前缀表示跨行解析\n\t\t\t\t\t\t\t\t<br>(?m) 前缀表示逐行匹配\n\t\t\t\t\t\t\t\t<br>(?i) 前缀表示忽略大小写\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"https://www.beta.browxy.com/\">代码在线运行工具</a>\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"http://alanskycn.gitee.io/vip/yd-demo\">WEB看书(需联网)</a>\n\t\t\t\t\t\t\t<a target=\"_blank\" href=\"/bookshelf.html\">WEB看书(内置版)</a>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</body>\n\n</html>"
  },
  {
    "path": "app/src/main/assets/web/index.js",
    "content": "// 简化js原生选择器\nfunction $(selector) { return document.querySelector(selector); }\nfunction $$(selector) { return document.querySelectorAll(selector); }\n// 读写Hash值(val未赋值时为读取)\nfunction hashParam(key, val) {\n\tlet hashstr = decodeURIComponent(window.location.hash);\n\tlet regKey = new RegExp(`${key}=([^&]*)`);\n\tlet getVal = regKey.test(hashstr) ? hashstr.match(regKey)[1] : null;\n\tif (val == undefined) return getVal;\n\tif (hashstr == '' || hashstr == '#') {\n\t\twindow.location.hash = `#${key}=${val}`;\n\t}\n\telse {\n\t\tif (getVal) window.location.hash = hashstr.replace(getVal, val);\n\t\telse {\n\t\t\twindow.location.hash = hashstr.indexOf(key) > -1 ? hashstr.replace(regKey, `${key}=${val}`) : `${hashstr}&${key}=${val}`;\n\t\t}\n\t}\n}\n// 创建书源规则容器对象\nconst RuleJSON = (() => {\n\tlet ruleJson = {};\n\t$$('.rules textarea').forEach(item => ruleJson[item.id] = '');\n//\tfor (let item of $$('.rules textarea')) ruleJson[item.id] = '';\n\truleJson.serialNumber = 0;\n\truleJson.weight = 0;\n\truleJson.enable = true;\n\treturn ruleJson;\n})();\n// 选项卡Tab切换事件处理\nfunction showTab(tabName) {\n\t$$('.tabtitle>*').forEach(node => { node.className = node.className.replace(' this', ''); });\n\t$$('.tabbody>*').forEach(node => { node.className = node.className.replace(' this', ''); });\n\t$(`.tabbody>.${$(`.tabtitle>*[name=${tabName}]`).className}`).className += ' this';\n\t$(`.tabtitle>*[name=${tabName}]`).className += ' this';\n\thashParam('tab', tabName);\n}\n// 书源列表列表标签构造函数\nfunction newRule(rule) {\n\treturn `<label for=\"${rule.bookSourceUrl}\"><input type=\"radio\" name=\"rule\" id=\"${rule.bookSourceUrl}\"><div>${rule.bookSourceName}<br>${rule.bookSourceUrl}</div></label>`;\n}\n// 缓存规则列表\nvar RuleSources = [];\nif (localStorage.getItem('RuleSources')) {\n\tRuleSources = JSON.parse(localStorage.getItem('RuleSources'));\n\tlet ruleListArray = [];\n\tRuleSources.forEach(item => { \n\t\truleListArray.push(newRule(item));\n\t});\n\t$('#RuleList').innerHTML += ruleListArray.join('');\n}\n// 页面加载完成事件\nwindow.onload = () => {\n\t$$('.tabtitle>*').forEach(item => {\n\t\titem.addEventListener('click', () => {\n\t\t\tshowTab(item.innerHTML);\n\t\t});\n\t});\n\tif (hashParam('tab')) showTab(hashParam('tab'));\n}\n// 获取数据\nfunction HttpGet(url) {\n\treturn fetch(hashParam('domain') ? hashParam('domain') + url : url)\n\t\t.then(res => res.json()).catch(err => console.error('Error:', err));\n}\n// 提交数据\nfunction HttpPost(url, data) {\n\treturn fetch(hashParam('domain') ? hashParam('domain') + url : url, {\n\t\tbody: JSON.stringify(data),\n\t\tmethod: 'POST',\n\t\tmode: \"cors\",\n\t\theaders: new Headers({\n\t\t\t'Content-Type': 'application/json;charset=utf-8'\n\t\t})\n\t}).then(res => res.json()).catch(err => console.error('Error:', err));\n}\n// 将书源表单转化为书源对象\nfunction rule2json() {\n\tObject.keys(RuleJSON).forEach((key) => RuleJSON[key] = $('#' + key).value);\n\tRuleJSON.bookSourceType = RuleJSON.bookSourceType.toUpperCase();\n\tRuleJSON.serialNumber = RuleJSON.serialNumber == '' ? 0 : parseInt(RuleJSON.serialNumber);\n\tRuleJSON.weight = RuleJSON.weight == '' ? 0 : parseInt(RuleJSON.weight);\n\tRuleJSON.enable = RuleJSON.enable == '' || RuleJSON.enable.toLocaleLowerCase().replace(/^\\s*|\\s*$/g, '') == 'true';\n\tlet TempRules = RuleSources.filter(item => (item['bookSourceUrl'] == RuleJSON['bookSourceUrl'] ? item : null));\n\tif (TempRules.length > 0) {\n\t\tObject.keys(RuleJSON).forEach(key => TempRules[0][key] = RuleJSON[key]);\n\t\treturn TempRules[0];\n\t}\n\treturn RuleJSON;\n}\n// 将书源对象填充到书源表单\nfunction json2rule(RuleEditor) {\n\tObject.keys(RuleJSON).forEach((key) => $(\"#\" + key).value = RuleEditor[key] ? RuleEditor[key] : '');\n}\n// 记录操作过程\nvar course = { \"old\": [], \"now\": {}, \"new\": [] };\nif (localStorage.getItem('course')) {\n\tcourse = JSON.parse(localStorage.getItem('course'));\n\tjson2rule(course.now);\n}\nelse {\n\tcourse.now = rule2json();\n\twindow.localStorage.setItem('course', JSON.stringify(course));\n}\nfunction todo() {\n\tcourse.old.push(Object.assign({}, course.now));\n\tcourse.now = rule2json();\n\tcourse.new = [];\n\tif (course.old.length > 50) course.old.shift(); // 限制历史记录堆栈大小\n\tlocalStorage.setItem('course', JSON.stringify(course));\n}\nfunction undo() {\n\tcourse = JSON.parse(localStorage.getItem('course'));\n\tif (course.old.length > 0) {\n\t\tcourse.new.push(course.now);\n\t\tcourse.now = course.old.pop();\n\t\tlocalStorage.setItem('course', JSON.stringify(course));\n\t\tjson2rule(course.now);\n\t}\n}\nfunction redo() {\n\tcourse = JSON.parse(localStorage.getItem('course'));\n\tif (course.new.length > 0) {\n\t\tcourse.old.push(course.now);\n\t\tcourse.now = course.new.pop();\n\t\tlocalStorage.setItem('course', JSON.stringify(course));\n\t\tjson2rule(course.now);\n\t}\n}\nfunction setRule(editRule) {\n\tlet checkRule = RuleSources.find(x => x.bookSourceUrl == editRule.bookSourceUrl);\n\tif ($(`input[id=\"${editRule.bookSourceUrl}\"]`)) {\n\t\tObject.keys(checkRule).forEach(key => { checkRule[key] = editRule[key]; });\n\t\t$(`input[id=\"${editRule.bookSourceUrl}\"]+*`).innerHTML = `${editRule.bookSourceName}<br>${editRule.bookSourceUrl}`;\n\t} else {\n\t\tRuleSources.push(editRule);\n\t\t$('#RuleList').innerHTML += newRule(editRule);\n\t}\n}\n$$('input').forEach((item) => { item.addEventListener('change', () => { todo() }) });\n$$('textarea').forEach((item) => { item.addEventListener('change', () => { todo() }) });\n// 处理按钮点击事件\n$('.menu').addEventListener('click', e => {\n\tlet thisNode = e.target;\n\tthisNode = thisNode.parentNode.nodeName == 'svg' ? thisNode.parentNode.querySelector('rect') :\n\t\tthisNode.nodeName == 'svg' ? thisNode.querySelector('rect') : null;\n\tif (!thisNode) return;\n\tif (thisNode.getAttribute('class') == 'busy') return;\n\tthisNode.setAttribute('class', 'busy');\n\tswitch (thisNode.id) {\n\t\tcase 'push':\n\t\t\t$$('#RuleList>label>div').forEach(item => { item.className = ''; });\n\t\t\t(async () => {\n\t\t\t\tawait HttpPost(`/saveSources`, RuleSources).then(json => {\n\t\t\t\t\tif (json.isSuccess) {\n\t\t\t\t\t\tconsole.log('批量推送书源:', RuleSources);\n\t\t\t\t\t\tlet okData = json.data;\n\t\t\t\t\t\tif (Array.isArray(okData)) {\n\t\t\t\t\t\t\tlet failMsg = ``;\n\t\t\t\t\t\t\tif (RuleSources.length > okData.length) {\n\t\t\t\t\t\t\t\tRuleSources.forEach(item => {\n\t\t\t\t\t\t\t\t\tif (okData.find(x => x.bookSourceUrl == item.bookSourceUrl)) { }\n\t\t\t\t\t\t\t\t\telse { $(`#RuleList #${item.bookSourceUrl}+*`).className += 'isError'; }\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tfailMsg = '\\n推送失败的书源将用红色字体标注!';\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\talert(`批量推送书源到「阅读APP」\\n共计: ${RuleSources.length} 条\\n成功: ${okData.length} 条\\n失败: ${RuleSources.length - okData.length} 条${failMsg}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\talert(`批量推送书源到「阅读APP」成功!\\n共计: ${RuleSources.length} 条`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\talert(`批量推送书源失败!\\nErrorMsg: ${json.errorMsg}`);\n\t\t\t\t\t}\n\t\t\t\t}).catch(err => { alert(`批量推送书源失败,无法连接到「阅读APP」!\\n${err}`); });\n\t\t\t\tthisNode.setAttribute('class', '');\n\t\t\t})();\n\t\t\treturn;\n\t\tcase 'pull':\n\t\t\tshowTab('书源列表');\n\t\t\t(async () => {\n\t\t\t\tawait HttpGet(`/getSources`).then(json => {\n\t\t\t\t\tif (json.isSuccess) {\n\t\t\t\t\t\tconsole.log('批量拉取书源:', RuleSources);\n\t\t\t\t\t\t$('#RuleList').innerHTML = ''\n\t\t\t\t\t\tlocalStorage.setItem('RuleSources', JSON.stringify(RuleSources = json.data));\n\t\t\t\t\t\tlet ruleListArray = [];\n\t\t\t\t\t\tRuleSources.forEach(item => {\n\t\t\t\t\t\t\truleListArray.push(newRule(item));\n\t\t\t\t\t\t});\n\t\t\t\t\t\t$('#RuleList').innerHTML += ruleListArray.join('');\n\t\t\t\t\t\talert(`成功拉取 ${RuleSources.length} 条书源`);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\talert(`批量拉取书源失败!\\nErrorMsg: ${json.errorMsg}`);\n\t\t\t\t\t}\n\t\t\t\t}).catch(err => { alert(`批量拉取书源失败,无法连接到「阅读APP」!\\n${err}`); });\n\t\t\t\tthisNode.setAttribute('class', '');\n\t\t\t})();\n\t\t\treturn;\n\t\tcase 'editor':\n\t\t\tif ($('#RuleJsonString').value == '') break;\n\t\t\ttry {\n\t\t\t\tjson2rule(JSON.parse($('#RuleJsonString').value));\n\t\t\t\ttodo();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.log(error);\n\t\t\t\talert(error);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 'conver':\n\t\t\tshowTab('编辑书源');\n\t\t\t$('#RuleJsonString').value = JSON.stringify(rule2json(), null, 4);\n\t\t\tbreak;\n\t\tcase 'initial':\n\t\t\t$$('.rules textarea').forEach(item => { item.value = '' });\n\t\t\ttodo();\n\t\t\tbreak;\n\t\tcase 'undo':\n\t\t\tundo()\n\t\t\tbreak;\n\t\tcase 'redo':\n\t\t\tredo()\n\t\t\tbreak;\n\t\tcase 'debug':\n\t\t\tshowTab('调试书源');\n\t\t\tlet wsOrigin = (hashParam('domain') || location.origin).replace(/^.*?:/, 'ws:').replace(/\\d+$/, (port) => (parseInt(port) + 1));\n\t\t\tlet DebugInfos = $('#DebugConsole');\n\t\t\tfunction DebugPrint(msg) { DebugInfos.value += `\\n${msg}`; DebugInfos.scrollTop = DebugInfos.scrollHeight; }\n\t\t\tlet saveRule = [rule2json()];\n\t\t\tHttpPost(`/saveSources`, saveRule).then(sResult => {\n\t\t\t\tif (sResult.isSuccess) {\n\t\t\t\t\tlet sKey = DebugKey.value ? DebugKey.value : '我的';\n\t\t\t\t\t$('#DebugConsole').value = `书源《${saveRule[0].bookSourceName}》保存成功！使用搜索关键字“${sKey}”开始调试...`;\n\t\t\t\t\tlet ws = new WebSocket(`${wsOrigin}/sourceDebug`);\n\t\t\t\t\tws.onopen = () => {\n\t\t\t\t\t\tws.send(`{\"tag\":\"${saveRule[0].bookSourceUrl}\", \"key\":\"${sKey}\"}`);\n\t\t\t\t\t};\n\t\t\t\t\tws.onmessage = (msg) => {\n\t\t\t\t\t\tDebugPrint(msg.data == 'finish' ? `\\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data);\n\t\t\t\t\t\tif (msg.data == 'finish') setRule(saveRule[0]);\n\t\t\t\t\t};\n\t\t\t\t\tws.onerror = (err) => {\n\t\t\t\t\t\tthrow `${err.data}`;\n\t\t\t\t\t}\n\t\t\t\t\tws.onclose = () => {\n\t\t\t\t\t\tthisNode.setAttribute('class', '');\n\t\t\t\t\t\tDebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`);\n\t\t\t\t\t}\n\t\t\t\t} else throw `${sResult.errorMsg}`;\n\t\t\t}).catch(err => {\n\t\t\t\tDebugPrint(`调试过程意外中止，以下是详细错误信息:\\n${err}`);\n\t\t\t\tthisNode.setAttribute('class', '');\n\t\t\t});\n\t\t\treturn;\n\t\tcase 'accept':\n\t\t\t(async () => {\n\t\t\t\tlet saveRule = [rule2json()];\n\t\t\t\tawait HttpPost(`/saveSources`, saveRule).then(json => {\n\t\t\t\t\talert(json.isSuccess ? `书源《${saveRule[0].bookSourceName}》已成功保存到「阅读APP」` : `书源《${saveRule[0].bookSourceName}》保存失败!\\nErrorMsg: ${json.errorMsg}`);\n\t\t\t\t\tsetRule(saveRule[0]);\n\t\t\t\t}).catch(err => { alert(`保存书源失败,无法连接到「阅读APP」!\\n${err}`); });\n\t\t\t\tthisNode.setAttribute('class', '');\n\t\t\t})();\n\t\t\treturn;\n\t\tdefault:\n\t}\n\tsetTimeout(() => { thisNode.setAttribute('class', ''); }, 500);\n});\n$('#DebugKey').addEventListener('keydown', e => {\n\tif (e.keyCode == 13) {\n\t\tlet clickEvent = document.createEvent('MouseEvents');\n\t\tclickEvent.initEvent(\"click\", true, false);\n\t\t$('#debug').dispatchEvent(clickEvent);\n\t}\n});\n\n// 列表规则更改事件\n$('#RuleList').addEventListener('click', e => {\n\tlet editRule = null;\n\tif (e.target && e.target.getAttribute('name') == 'rule') {\n\t\teditRule = rule2json();\n\t\tjson2rule(RuleSources.find(x => x.bookSourceUrl == e.target.id));\n\t} else return;\n\tif (editRule.bookSourceUrl == '') return;\n\tif (editRule.bookSourceName == '') editRule.bookSourceName = editRule.bookSourceUrl.replace(/.*?\\/\\/|\\/.*/g, '');\n\tsetRule(editRule);\n\tlocalStorage.setItem('RuleSources', JSON.stringify(RuleSources));\n});\n// 处理列表按钮事件\n$('.tab3>.titlebar').addEventListener('click', e => {\n\tlet thisNode = e.target;\n\tif (thisNode.nodeName != 'BUTTON') return;\n\tswitch (thisNode.id) {\n\t\tcase 'Import':\n\t\t\tlet fileImport = document.createElement('input');\n\t\t\tfileImport.type = 'file';\n\t\t\tfileImport.accept = '.json';\n\t\t\tfileImport.addEventListener('change', () => {\n\t\t\t\tlet file = fileImport.files[0];\n\t\t\t\tlet reader = new FileReader();\n\t\t\t\treader.onloadend = function (evt) {\n\t\t\t\t\tif (evt.target.readyState == FileReader.DONE) {\n\t\t\t\t\t\tlet fileText = evt.target.result;\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tlet fileJson = JSON.parse(fileText);\n\t\t\t\t\t\t\tlet newSources = [];\n\t\t\t\t\t\t\tnewSources.push(...fileJson);\n\t\t\t\t\t\t\tif (window.confirm(`如何处理导入的书源?\\n\"确定\": 覆盖当前列表(不会删除APP源)\\n\"取消\": 插入列表尾部(自动忽略重复源)`)) {\n\t\t\t\t\t\t\t\tlocalStorage.setItem('RuleSources', JSON.stringify(RuleSources = newSources));\n\t\t\t\t\t\t\t\t$('#RuleList').innerHTML = '';\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tnewSources = newSources.filter(item => !JSON.stringify(RuleSources).includes(item.bookSourceUrl));\n\t\t\t\t\t\t\t\tRuleSources.push(...newSources);\n\t\t\t\t\t\t\t\tlocalStorage.setItem('RuleSources', JSON.stringify(RuleSources));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tlet ruleListArray = [];\n\t\t\t\t\t\t\tRuleSources.forEach(item => {\n\t\t\t\t\t\t\t\truleListArray.push(newRule(item));\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t$('#RuleList').innerHTML += ruleListArray.join('');\n\t\t\t\t\t\t\talert(`成功导入 ${newSources.length} 条书源`);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcatch (err) {\n\t\t\t\t\t\t\talert(`导入书源文件失败!\\n${err}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\treader.readAsText(file);\n\t\t\t}, false);\n\t\t\tfileImport.click();\n\t\t\tbreak;\n\t\tcase 'Export':\n\t\t\tlet fileExport = document.createElement('a');\n\t\t\tfileExport.download = `Rules${Date().replace(/.*?\\s(\\d+)\\s(\\d+)\\s(\\d+:\\d+:\\d+).*/, '$2$1$3').replace(/:/g, '')}.json`;\n\t\t\tlet myBlob = new Blob([JSON.stringify(RuleSources, null, 4)], { type: \"application/json\" });\n\t\t\tfileExport.href = window.URL.createObjectURL(myBlob);\n\t\t\tfileExport.click();\n\t\t\tbreak;\n\t\tcase 'Delete':\n\t\t\tlet selectRule = $('#RuleList input:checked');\n\t\t\tif (!selectRule) {\n\t\t\t\talert(`没有书源被选中!`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (confirm(`确定要删除选定书源吗?\\n(同时删除APP内书源)`)) {\n\t\t\t\tlet selectRuleUrl = selectRule.id;\n\t\t\t\tlet deleteSources = RuleSources.filter(item => item.bookSourceUrl == selectRuleUrl); // 提取待删除的书源\n\t\t\t\tlet laveSources = RuleSources.filter(item => !(item.bookSourceUrl == selectRuleUrl));  // 提取待留下的书源\n\t\t\t\tHttpPost(`/deleteSources`, deleteSources).then(json => {\n\t\t\t\t\tif (json.isSuccess) {\n\t\t\t\t\t\tlet selectNode = document.getElementById(selectRuleUrl).parentNode;\n\t\t\t\t\t\tselectNode.parentNode.removeChild(selectNode);\n\t\t\t\t\t\tlocalStorage.setItem('RuleSources', JSON.stringify(RuleSources = laveSources));\n\t\t\t\t\t\tif ($('#bookSourceUrl').value == selectRuleUrl) {\n\t\t\t\t\t\t\t$$('.rules textarea').forEach(item => { item.value = '' });\n\t\t\t\t\t\t\ttodo();\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconsole.log(deleteSources);\n\t\t\t\t\t\tconsole.log(`以上书源已删除!`)\n\t\t\t\t\t}\n\t\t\t\t}).catch(err => { alert(`删除书源失败,无法连接到「阅读APP」!\\n${err}`); });\n\t\t\t}\n\t\t\tbreak;\n\t\tcase 'ClrAll':\n\t\t\tif (confirm(`确定要清空当前书源列表吗?\\n(不会删除APP内书源)`)) {\n\t\t\t\tlocalStorage.setItem('RuleSources', JSON.stringify(RuleSources = []));\n\t\t\t\t$('#RuleList').innerHTML = ''\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault:\n\t}\n});"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/DbHelper.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf;\n\nimport android.content.Context;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport com.github.yuweiguocn.library.greendao.MigrationHelper;\nimport com.kunfei.bookshelf.dao.BookChapterBeanDao;\nimport com.kunfei.bookshelf.dao.BookInfoBeanDao;\nimport com.kunfei.bookshelf.dao.BookShelfBeanDao;\nimport com.kunfei.bookshelf.dao.BookSourceBeanDao;\nimport com.kunfei.bookshelf.dao.BookmarkBeanDao;\nimport com.kunfei.bookshelf.dao.CookieBeanDao;\nimport com.kunfei.bookshelf.dao.DaoMaster;\nimport com.kunfei.bookshelf.dao.DaoSession;\nimport com.kunfei.bookshelf.dao.ReplaceRuleBeanDao;\nimport com.kunfei.bookshelf.dao.SearchHistoryBeanDao;\nimport com.kunfei.bookshelf.dao.TxtChapterRuleBeanDao;\n\nimport org.greenrobot.greendao.database.Database;\n\nimport java.util.Locale;\n\npublic class DbHelper {\n    private static DbHelper instance;\n    private final SQLiteDatabase db;\n    private final DaoSession mDaoSession;\n\n    private DbHelper() {\n        DaoOpenHelper mHelper = new DaoOpenHelper(MApplication.getInstance(), \"monkebook_db\", null);\n        db = mHelper.getWritableDatabase();\n        db.setLocale(Locale.CHINESE);\n        // 注意：该数据库连接属于 DaoMaster，所以多个 Session 指的是相同的数据库连接。\n        DaoMaster mDaoMaster = new DaoMaster(db);\n        mDaoSession = mDaoMaster.newSession();\n    }\n\n    public static DbHelper getInstance() {\n        if (null == instance) {\n            synchronized (DbHelper.class) {\n                if (null == instance) {\n                    instance = new DbHelper();\n                }\n            }\n        }\n        return instance;\n    }\n\n    public static DaoSession getDaoSession() {\n        return getInstance().mDaoSession;\n    }\n\n    public static SQLiteDatabase getDb() {\n        return getInstance().db;\n    }\n\n    public static class DaoOpenHelper extends DaoMaster.OpenHelper {\n        DaoOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {\n            super(context, name, factory);\n        }\n\n        @Override\n        @SuppressWarnings(\"unchecked\")\n        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n            MigrationHelper.migrate(db,\n                    new MigrationHelper.ReCreateAllTableListener() {\n                        @Override\n                        public void onCreateAllTables(Database db, boolean ifNotExists) {\n                            DaoMaster.createAllTables(db, ifNotExists);\n                        }\n\n                        @Override\n                        public void onDropAllTables(Database db, boolean ifExists) {\n                            DaoMaster.dropAllTables(db, ifExists);\n                        }\n                    },\n                    BookShelfBeanDao.class, BookInfoBeanDao.class, BookChapterBeanDao.class,\n                    SearchHistoryBeanDao.class, BookSourceBeanDao.class,\n                    ReplaceRuleBeanDao.class, BookmarkBeanDao.class, CookieBeanDao.class,\n                    TxtChapterRuleBeanDao.class\n            );\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/MApplication.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf;\n\nimport android.app.Application;\nimport android.app.NotificationChannel;\nimport android.app.NotificationManager;\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.content.pm.PackageManager;\nimport android.content.res.Resources;\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport androidx.annotation.RequiresApi;\nimport androidx.appcompat.app.AppCompatDelegate;\nimport androidx.multidex.MultiDex;\n\nimport com.kunfei.bookshelf.constant.AppConstant;\nimport com.kunfei.bookshelf.help.AppFrontBackHelper;\nimport com.kunfei.bookshelf.help.CrashHandler;\nimport com.kunfei.bookshelf.help.FileHelp;\nimport com.kunfei.bookshelf.model.UpLastChapterModel;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.Objects;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.internal.functions.Functions;\nimport io.reactivex.plugins.RxJavaPlugins;\nimport timber.log.Timber;\n\npublic class MApplication extends Application {\n    public final static String channelIdDownload = \"channel_download\";\n    public final static String channelIdReadAloud = \"channel_read_aloud\";\n    public final static String channelIdWeb = \"channel_web\";\n    public static String downloadPath;\n    public static boolean isEInkMode;\n    public static String SEARCH_GROUP = null;\n    private static MApplication instance;\n    private static String versionName;\n    private static int versionCode;\n    private SharedPreferences configPreferences;\n    private boolean donateHb;\n\n    public static MApplication getInstance() {\n        return instance;\n    }\n\n    public static int getVersionCode() {\n        return versionCode;\n    }\n\n    public static String getVersionName() {\n        return versionName;\n    }\n\n    public static Resources getAppResources() {\n        return getInstance().getResources();\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        instance = this;\n        CrashHandler.getInstance().init(this);\n        Timber.plant(new Timber.DebugTree());\n        RxJavaPlugins.setErrorHandler(Functions.emptyConsumer());\n        try {\n            versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;\n            versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;\n        } catch (PackageManager.NameNotFoundException e) {\n            versionCode = 0;\n            versionName = \"0.0.0\";\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            createChannelId();\n        }\n        configPreferences = getSharedPreferences(\"CONFIG\", 0);\n        downloadPath = configPreferences.getString(getString(R.string.pk_download_path), \"\");\n        if (TextUtils.isEmpty(downloadPath) | Objects.equals(downloadPath, FileHelp.getCachePath())) {\n            setDownloadPath(null);\n        }\n        initNightTheme();\n        if (!ThemeStore.isConfigured(this, versionCode)) {\n            upThemeStore();\n        }\n        AppFrontBackHelper.getInstance().register(this, new AppFrontBackHelper.OnAppStatusListener() {\n            @Override\n            public void onFront() {\n                donateHb = System.currentTimeMillis() - configPreferences.getLong(\"DonateHb\", 0) <= TimeUnit.DAYS.toMillis(30);\n            }\n\n            @Override\n            public void onBack() {\n                UpLastChapterModel.destroy();\n            }\n        });\n        upEInkMode();\n    }\n\n    @Override\n    protected void attachBaseContext(Context base) {\n        super.attachBaseContext(base);\n        MultiDex.install(this);\n    }\n\n    public void initNightTheme() {\n        if (isNightTheme()) {\n            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);\n        } else {\n            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);\n        }\n    }\n\n    /**\n     * 初始化主题\n     */\n    public void upThemeStore() {\n        if (isNightTheme()) {\n            ThemeStore.editTheme(this)\n                    .primaryColor(configPreferences.getInt(\"colorPrimaryNight\", getResources().getColor(R.color.md_grey_800)))\n                    .accentColor(configPreferences.getInt(\"colorAccentNight\", getResources().getColor(R.color.md_pink_800)))\n                    .backgroundColor(configPreferences.getInt(\"colorBackgroundNight\", getResources().getColor(R.color.md_grey_800)))\n                    .apply();\n        } else {\n            ThemeStore.editTheme(this)\n                    .primaryColor(configPreferences.getInt(\"colorPrimary\", getResources().getColor(R.color.md_grey_100)))\n                    .accentColor(configPreferences.getInt(\"colorAccent\", getResources().getColor(R.color.md_pink_600)))\n                    .backgroundColor(configPreferences.getInt(\"colorBackground\", getResources().getColor(R.color.md_grey_100)))\n                    .apply();\n        }\n    }\n\n    public boolean isNightTheme() {\n        return configPreferences.getBoolean(\"nightTheme\", false);\n    }\n\n    /**\n     * 设置下载地址\n     */\n    public void setDownloadPath(String path) {\n        if (TextUtils.isEmpty(path)) {\n            downloadPath = FileHelp.getFilesPath();\n        } else {\n            downloadPath = path;\n        }\n        AppConstant.BOOK_CACHE_PATH = downloadPath + File.separator + \"book_cache\" + File.separator;\n        configPreferences.edit()\n                .putString(getString(R.string.pk_download_path), path)\n                .apply();\n    }\n\n    public static SharedPreferences getConfigPreferences() {\n        return getInstance().configPreferences;\n    }\n\n    public boolean getDonateHb() {\n        return donateHb || BuildConfig.DEBUG;\n    }\n\n    public void upDonateHb() {\n        configPreferences.edit()\n                .putLong(\"DonateHb\", System.currentTimeMillis())\n                .apply();\n        donateHb = true;\n    }\n\n    public void upEInkMode() {\n        MApplication.isEInkMode = configPreferences.getBoolean(\"E-InkMode\", false);\n    }\n\n    /**\n     * 创建通知ID\n     */\n    @RequiresApi(Build.VERSION_CODES.O)\n    private void createChannelId() {\n        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);\n        //用唯一的ID创建渠道对象\n        NotificationChannel downloadChannel = new NotificationChannel(channelIdDownload,\n                getString(R.string.download_offline),\n                NotificationManager.IMPORTANCE_LOW);\n        //初始化channel\n        downloadChannel.enableLights(false);\n        downloadChannel.enableVibration(false);\n        downloadChannel.setSound(null, null);\n\n        //用唯一的ID创建渠道对象\n        NotificationChannel readAloudChannel = new NotificationChannel(channelIdReadAloud,\n                getString(R.string.read_aloud),\n                NotificationManager.IMPORTANCE_LOW);\n        //初始化channel\n        readAloudChannel.enableLights(false);\n        readAloudChannel.enableVibration(false);\n        readAloudChannel.setSound(null, null);\n\n        //用唯一的ID创建渠道对象\n        NotificationChannel webChannel = new NotificationChannel(channelIdWeb,\n                getString(R.string.web_service),\n                NotificationManager.IMPORTANCE_LOW);\n        //初始化channel\n        webChannel.enableLights(false);\n        webChannel.enableVibration(false);\n        webChannel.setSound(null, null);\n\n        //向notification manager 提交channel\n        if (notificationManager != null) {\n            notificationManager.createNotificationChannels(Arrays.asList(downloadChannel, readAloudChannel, webChannel));\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/BaseDialogFragment.kt",
    "content": "package com.kunfei.bookshelf.base\n\nimport android.os.Bundle\nimport android.view.View\nimport androidx.annotation.LayoutRes\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.FragmentManager\nimport com.kunfei.bookshelf.utils.theme.ThemeStore\nimport io.legado.app.help.coroutine.Coroutine\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlin.coroutines.CoroutineContext\n\n\nabstract class BaseDialogFragment(@LayoutRes layoutID: Int) : DialogFragment(layoutID),\n    CoroutineScope by MainScope() {\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        view.setBackgroundColor(ThemeStore.backgroundColor(requireContext()))\n        onFragmentCreated(view, savedInstanceState)\n        observeLiveBus()\n    }\n\n    abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)\n\n    override fun show(manager: FragmentManager, tag: String?) {\n        kotlin.runCatching {\n            //在每个add事务前增加一个remove事务，防止连续的add\n            manager.beginTransaction().remove(this).commit()\n            super.show(manager, tag)\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        cancel()\n    }\n\n    fun <T> execute(\n        scope: CoroutineScope = this,\n        context: CoroutineContext = Dispatchers.IO,\n        block: suspend CoroutineScope.() -> T\n    ) = Coroutine.async(scope, context) { block() }\n\n    open fun observeLiveBus() {\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/BaseFragment.kt",
    "content": "package com.kunfei.bookshelf.base\n\nimport android.annotation.SuppressLint\nimport android.content.res.Configuration\nimport android.os.Bundle\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport android.view.View\nimport androidx.annotation.LayoutRes\nimport androidx.appcompat.view.SupportMenuInflater\nimport androidx.appcompat.widget.Toolbar\nimport androidx.fragment.app.Fragment\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\n\n@Suppress(\"MemberVisibilityCanBePrivate\")\nabstract class BaseFragment(@LayoutRes layoutID: Int) : Fragment(layoutID),\n    CoroutineScope by MainScope() {\n\n    var supportToolbar: Toolbar? = null\n        private set\n\n    val menuInflater: MenuInflater\n        @SuppressLint(\"RestrictedApi\")\n        get() = SupportMenuInflater(requireContext())\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        onMultiWindowModeChanged()\n        onFragmentCreated(view, savedInstanceState)\n        observeLiveBus()\n    }\n\n    abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)\n\n    override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) {\n        super.onMultiWindowModeChanged(isInMultiWindowMode)\n        onMultiWindowModeChanged()\n    }\n\n    override fun onConfigurationChanged(newConfig: Configuration) {\n        super.onConfigurationChanged(newConfig)\n        onMultiWindowModeChanged()\n    }\n\n    private fun onMultiWindowModeChanged() {\n\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        cancel()\n    }\n\n    open fun observeLiveBus() {\n    }\n\n    open fun onCompatCreateOptionsMenu(menu: Menu) {\n    }\n\n    open fun onCompatOptionsItemSelected(item: MenuItem) {\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/BaseModelImpl.java",
    "content": "package com.kunfei.bookshelf.base;\n\nimport android.annotation.SuppressLint;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.text.TextUtils;\nimport android.webkit.CookieManager;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.bean.CookieBean;\nimport com.kunfei.bookshelf.help.EncodeConverter;\nimport com.kunfei.bookshelf.help.SSLSocketClient;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl;\nimport com.kunfei.bookshelf.model.impl.IHttpGetApi;\nimport com.kunfei.bookshelf.model.impl.IHttpPostApi;\n\nimport org.apache.commons.lang3.StringEscapeUtils;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\nimport okhttp3.ConnectionSpec;\nimport okhttp3.Interceptor;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Protocol;\nimport okhttp3.Request;\nimport retrofit2.Response;\nimport retrofit2.Retrofit;\nimport retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;\n\npublic class BaseModelImpl {\n    private static OkHttpClient httpClient;\n\n    public static BaseModelImpl getInstance() {\n        return new BaseModelImpl();\n    }\n\n    public Observable<Response<String>> getResponseO(AnalyzeUrl analyzeUrl) {\n        switch (analyzeUrl.getUrlMode()) {\n            case POST:\n                if (analyzeUrl.getJsonBody() != null) {\n                    return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode())\n                            .create(IHttpPostApi.class)\n                            .postJson(analyzeUrl.getPath(),\n                                    analyzeUrl.getPostBody(),\n                                    analyzeUrl.getHeaderMap());\n                } else {\n                    return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode())\n                            .create(IHttpPostApi.class)\n                            .postMap(analyzeUrl.getPath(),\n                                    analyzeUrl.getQueryMap(),\n                                    analyzeUrl.getHeaderMap());\n                }\n            case GET:\n                return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode())\n                        .create(IHttpGetApi.class)\n                        .getMap(analyzeUrl.getPath(),\n                                analyzeUrl.getQueryMap(),\n                                analyzeUrl.getHeaderMap());\n            default:\n                return getRetrofitString(analyzeUrl.getHost(), analyzeUrl.getCharCode())\n                        .create(IHttpGetApi.class)\n                        .get(analyzeUrl.getPath(),\n                                analyzeUrl.getHeaderMap());\n        }\n    }\n\n    public Retrofit getRetrofitString(String url) {\n        return new Retrofit.Builder().baseUrl(url)\n                //增加返回值为字符串的支持(以实体类返回)\n                .addConverterFactory(EncodeConverter.create())\n                //增加返回值为Observable<T>的支持\n                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())\n                .client(getClient())\n                .build();\n    }\n\n    public Retrofit getRetrofitString(String url, String encode) {\n        return new Retrofit.Builder().baseUrl(url)\n                //增加返回值为字符串的支持(以实体类返回)\n                .addConverterFactory(EncodeConverter.create(encode))\n                //增加返回值为Observable<T>的支持\n                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())\n                .client(getClient())\n                .build();\n    }\n\n    synchronized public static OkHttpClient getClient() {\n        if (httpClient == null) {\n            ArrayList<ConnectionSpec> specs = new ArrayList<>();\n            specs.add(ConnectionSpec.MODERN_TLS);\n            specs.add(ConnectionSpec.COMPATIBLE_TLS);\n            specs.add(ConnectionSpec.CLEARTEXT);\n            httpClient = new OkHttpClient.Builder()\n                    .connectTimeout(15, TimeUnit.SECONDS)\n                    .writeTimeout(15, TimeUnit.SECONDS)\n                    .readTimeout(15, TimeUnit.SECONDS)\n                    .retryOnConnectionFailure(true)\n                    .sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.createTrustAllManager())\n                    .hostnameVerifier(SSLSocketClient.getHostnameVerifier())\n                    .followRedirects(true)\n                    .followSslRedirects(true)\n                    .connectionSpecs(specs)\n                    .protocols(Collections.singletonList(Protocol.HTTP_1_1))\n                    .addInterceptor(getHeaderInterceptor())\n                    .build();\n        }\n        return httpClient;\n    }\n\n    private static Interceptor getHeaderInterceptor() {\n        return chain -> {\n            Request request = chain.request()\n                    .newBuilder()\n                    .addHeader(\"Keep-Alive\", \"300\")\n                    .addHeader(\"Connection\", \"Keep-Alive\")\n                    .addHeader(\"Cache-Control\", \"no-cache\")\n                    .build();\n            return chain.proceed(request);\n        };\n    }\n\n    protected Observable<Response<String>> setCookie(Response<String> response, String tag) {\n        return Observable.create(e -> {\n            if (!response.raw().headers(\"Set-Cookie\").isEmpty()) {\n                StringBuilder cookieBuilder = new StringBuilder();\n                for (String s : response.raw().headers(\"Set-Cookie\")) {\n                    String[] x = s.split(\";\");\n                    for (String y : x) {\n                        if (!TextUtils.isEmpty(y)) {\n                            cookieBuilder.append(y).append(\";\");\n                        }\n                    }\n                }\n                String cookie = cookieBuilder.toString();\n                if (!TextUtils.isEmpty(cookie)) {\n                    DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(tag, cookie));\n                }\n            }\n            e.onNext(response);\n            e.onComplete();\n        });\n    }\n\n    @SuppressLint({\"AddJavascriptInterface\", \"SetJavaScriptEnabled\"})\n    protected Observable<String> getAjaxString(AnalyzeUrl analyzeUrl, String tag, String js) {\n        final Web web = new Web(\"加载超时\");\n        if (!TextUtils.isEmpty(js)) {\n            web.js = js;\n        }\n        return Observable.create(e -> {\n            Handler handler = new Handler(Looper.getMainLooper());\n            handler.post(() -> {\n                Runnable timeoutRunnable;\n                WebView webView = new WebView(MApplication.getInstance());\n                webView.getSettings().setJavaScriptEnabled(true);\n                webView.getSettings().setUserAgentString(analyzeUrl.getHeaderMap().get(\"User-Agent\"));\n                CookieManager cookieManager = CookieManager.getInstance();\n                Runnable retryRunnable = new Runnable() {\n                    @Override\n                    public void run() {\n                        webView.evaluateJavascript(web.js, value -> {\n                            if (!TextUtils.isEmpty(value)) {\n                                web.content = StringEscapeUtils.unescapeJson(value);\n                                e.onNext(web.content);\n                                e.onComplete();\n                                webView.destroy();\n                                handler.removeCallbacks(this);\n                            } else {\n                                handler.postDelayed(this, 1000);\n                            }\n                        });\n                    }\n                };\n                timeoutRunnable = () -> {\n                    if (!e.isDisposed()) {\n                        handler.removeCallbacks(retryRunnable);\n                        e.onNext(web.content);\n                        e.onComplete();\n                        webView.destroy();\n                    }\n                };\n                handler.postDelayed(timeoutRunnable, 30000);\n                webView.setWebViewClient(new WebViewClient() {\n                    @Override\n                    public void onPageFinished(WebView view, String url) {\n                        DbHelper.getDaoSession().getCookieBeanDao()\n                                .insertOrReplace(new CookieBean(tag, cookieManager.getCookie(webView.getUrl())));\n                        handler.postDelayed(retryRunnable, 1000);\n                    }\n                });\n                switch (analyzeUrl.getUrlMode()) {\n                    case POST:\n                        webView.postUrl(analyzeUrl.getUrl(), analyzeUrl.getPostData());\n                        break;\n                    case GET:\n                        webView.loadUrl(String.format(\"%s?%s\", analyzeUrl.getUrl(), analyzeUrl.getQueryStr()), analyzeUrl.getHeaderMap());\n                        break;\n                    default:\n                        webView.loadUrl(analyzeUrl.getUrl(), analyzeUrl.getHeaderMap());\n                }\n            });\n        });\n    }\n\n    private static class Web {\n        private String content;\n        private String js = \"document.documentElement.outerHTML\";\n\n        Web(String content) {\n            this.content = content;\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/BaseService.kt",
    "content": "package com.kunfei.bookshelf.base\n\nimport android.app.Service\nimport android.content.Intent\nimport android.os.IBinder\nimport androidx.annotation.CallSuper\nimport io.legado.app.help.coroutine.Coroutine\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlin.coroutines.CoroutineContext\n\nabstract class BaseService : Service(), CoroutineScope by MainScope() {\n\n    fun <T> execute(\n        scope: CoroutineScope = this,\n        context: CoroutineContext = Dispatchers.IO,\n        block: suspend CoroutineScope.() -> T\n    ) = Coroutine.async(scope, context) { block() }\n\n    @CallSuper\n    override fun onCreate() {\n        super.onCreate()\n    }\n\n    @CallSuper\n    override fun onTaskRemoved(rootIntent: Intent?) {\n        super.onTaskRemoved(rootIntent)\n        stopSelf()\n    }\n\n    override fun onBind(intent: Intent?): IBinder? {\n        return null\n    }\n\n    @CallSuper\n    override fun onDestroy() {\n        super.onDestroy()\n        cancel()\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/BaseTabActivity.java",
    "content": "package com.kunfei.bookshelf.base;\n\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.app.FragmentManager;\nimport androidx.fragment.app.FragmentPagerAdapter;\nimport androidx.viewpager.widget.ViewPager;\n\nimport com.google.android.material.tabs.TabLayout;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\n\nimport java.util.List;\n\n/**\n * Created by newbiechen on 17-4-24.\n */\n\npublic abstract class BaseTabActivity<T extends IPresenter> extends MBaseActivity<T> {\n    /**************View***************/\n    protected TabLayout mTlIndicator;\n    protected ViewPager mVp;\n    /**************Adapter***************/\n    protected TabFragmentPageAdapter tabFragmentPageAdapter;\n    /************Params*******************/\n    protected List<Fragment> mFragmentList;\n    private List<String> mTitleList;\n\n    /**************abstract***********/\n    protected abstract List<Fragment> createTabFragments();\n\n    protected abstract List<String> createTabTitles();\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        mTlIndicator = findViewById(R.id.tab_tl_indicator);\n        mVp = findViewById(R.id.tab_vp);\n        setUpTabLayout();\n    }\n\n    /*****************rewrite method***************************/\n\n\n    private void setUpTabLayout() {\n        mFragmentList = createTabFragments();\n        mTitleList = createTabTitles();\n\n        checkParamsIsRight();\n\n        tabFragmentPageAdapter = new TabFragmentPageAdapter(getSupportFragmentManager());\n        mVp.setAdapter(tabFragmentPageAdapter);\n        mVp.setOffscreenPageLimit(3);\n        mTlIndicator.setupWithViewPager(mVp);\n    }\n\n    /**\n     * 检查输入的参数是否正确。即Fragment和title是成对的。\n     */\n    private void checkParamsIsRight() {\n        if (mFragmentList == null || mTitleList == null) {\n            throw new IllegalArgumentException(\"fragmentList or titleList doesn't have null\");\n        }\n\n        if (mFragmentList.size() != mTitleList.size())\n            throw new IllegalArgumentException(\"fragment and title size must equal\");\n    }\n\n    /******************inner class*****************/\n    public class TabFragmentPageAdapter extends FragmentPagerAdapter {\n\n        TabFragmentPageAdapter(FragmentManager fm) {\n            super(fm);\n        }\n\n        @NonNull\n        @Override\n        public Fragment getItem(int position) {\n            return mFragmentList.get(position);\n        }\n\n        @Override\n        public int getCount() {\n            return mFragmentList.size();\n        }\n\n        @Override\n        public CharSequence getPageTitle(int position) {\n            return mTitleList.get(position);\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/BaseViewModel.kt",
    "content": "package com.kunfei.bookshelf.base\n\nimport android.app.Application\nimport android.content.Context\nimport androidx.lifecycle.AndroidViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.kunfei.bookshelf.MApplication\nimport io.legado.app.help.coroutine.Coroutine\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Deferred\nimport kotlinx.coroutines.Dispatchers\nimport kotlin.coroutines.CoroutineContext\n\n@Suppress(\"unused\")\nopen class BaseViewModel(application: Application) : AndroidViewModel(application) {\n\n    val context: Context by lazy { this.getApplication<MApplication>() }\n\n    fun <T> execute(\n        scope: CoroutineScope = viewModelScope,\n        context: CoroutineContext = Dispatchers.IO,\n        block: suspend CoroutineScope.() -> T\n    ): Coroutine<T> {\n        return Coroutine.async(scope, context) { block() }\n    }\n\n    fun <R> submit(\n        scope: CoroutineScope = viewModelScope,\n        context: CoroutineContext = Dispatchers.IO,\n        block: suspend CoroutineScope.() -> Deferred<R>\n    ): Coroutine<R> {\n        return Coroutine.async(scope, context) { block().await() }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/MBaseActivity.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.base;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.content.pm.ActivityInfo;\nimport android.content.res.Configuration;\nimport android.graphics.PorterDuff;\nimport android.graphics.drawable.Drawable;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BaseActivity;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.utils.ActivityExtensionsKt;\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\nimport com.kunfei.bookshelf.utils.theme.MaterialValueHelper;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\n\npublic abstract class MBaseActivity<T extends IPresenter> extends BaseActivity<T> {\n    private static final String TAG = MBaseActivity.class.getSimpleName();\n    public final SharedPreferences preferences = MApplication.getConfigPreferences();\n    private Snackbar snackbar;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        initTheme();\n        super.onCreate(savedInstanceState);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            getWindow().getDecorView().setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS);\n        }\n        initImmersionBar();\n    }\n\n    @SuppressWarnings(\"NullableProblems\")\n    @Override\n    public void onConfigurationChanged(Configuration newConfig) {\n        super.onConfigurationChanged(newConfig);\n\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n    }\n\n    @Override\n    public void setSupportActionBar(@Nullable androidx.appcompat.widget.Toolbar toolbar) {\n        if (toolbar != null) {\n            toolbar.setBackgroundColor(ThemeStore.primaryColor(this));\n        }\n        super.setSupportActionBar(toolbar);\n    }\n\n    /**\n     * 设置MENU图标颜色\n     */\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        int primaryTextColor = MaterialValueHelper.getPrimaryTextColor(this, ColorUtils.isColorLight(ThemeStore.primaryColor(this)));\n        for (int i = 0; i < menu.size(); i++) {\n            Drawable drawable = menu.getItem(i).getIcon();\n            if (drawable != null) {\n                drawable.mutate();\n                drawable.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_ATOP);\n            }\n        }\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    @SuppressLint(\"PrivateApi\")\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public boolean onMenuOpened(int featureId, Menu menu) {\n        if (menu != null) {\n            //展开菜单显示图标\n            if (menu.getClass().getSimpleName().equalsIgnoreCase(\"MenuBuilder\")) {\n                try {\n                    Method method = menu.getClass().getDeclaredMethod(\"setOptionalIconsVisible\", Boolean.TYPE);\n                    method.setAccessible(true);\n                    method.invoke(menu, true);\n                    method = menu.getClass().getDeclaredMethod(\"getNonActionItems\");\n                    ArrayList<MenuItem> menuItems = (ArrayList<MenuItem>) method.invoke(menu);\n                    if (!menuItems.isEmpty()) {\n                        for (MenuItem menuItem : menuItems) {\n                            Drawable drawable = menuItem.getIcon();\n                            if (drawable != null) {\n                                drawable.mutate();\n                                drawable.setColorFilter(getResources().getColor(R.color.tv_text_default), PorterDuff.Mode.SRC_ATOP);\n                            }\n                        }\n                    }\n                } catch (Exception ignored) {\n                }\n            }\n\n        }\n        return super.onMenuOpened(featureId, menu);\n    }\n\n    /**\n     * 沉浸状态栏\n     */\n    protected void initImmersionBar() {\n        try {\n            View actionBar = findViewById(R.id.action_bar);\n            ActivityExtensionsKt.fullScreen(this);\n            if (isImmersionBarEnabled()) {\n                boolean isTransparent = getSupportActionBar() != null && actionBar != null && actionBar.getVisibility() == View.VISIBLE;\n                ActivityExtensionsKt.setStatusBarColorAuto(this, ThemeStore.primaryColor(this), isTransparent, isTransparent);\n            } else {\n                if (getSupportActionBar() != null && actionBar != null && actionBar.getVisibility() == View.VISIBLE) {\n                    ActivityExtensionsKt.setStatusBarColorAuto(this, ThemeStore.statusBarColor(this), false, false);\n                } else {\n                    ActivityExtensionsKt.setStatusBarColorAuto(this, getResources().getColor(R.color.status_bar_bag), false, false);\n                }\n            }\n        } catch (Exception ignored) {\n        }\n        try {\n            if (!preferences.getBoolean(\"navigationBarColorChange\", false)) {\n                ActivityExtensionsKt.setNavigationBarColorAuto(this, getResources().getColor(R.color.black));\n            } else {\n                ActivityExtensionsKt.setNavigationBarColorAuto(this, ThemeStore.primaryColorDark(this));\n            }\n        } catch (Exception ignored) {\n        }\n    }\n\n    /**\n     * @return 是否沉浸\n     */\n    protected boolean isImmersionBarEnabled() {\n        return preferences.getBoolean(\"immersionStatusBar\", false);\n    }\n\n    /**\n     * 设置屏幕方向\n     */\n    @SuppressLint(\"SourceLockedOrientationActivity\")\n    public void setOrientation(int screenDirection) {\n        switch (screenDirection) {\n            case 0:\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);\n                break;\n            case 1:\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n                break;\n            case 2:\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);\n                break;\n            case 3:\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);\n                break;\n        }\n    }\n\n    /**\n     * @return 是否夜间模式\n     */\n    public boolean isNightTheme() {\n        return MApplication.getInstance().isNightTheme();\n    }\n\n    protected void setNightTheme(boolean isNightTheme) {\n        preferences.edit()\n                .putBoolean(\"nightTheme\", isNightTheme)\n                .apply();\n        MApplication.getInstance().initNightTheme();\n        MApplication.getInstance().upThemeStore();\n        RxBus.get().post(RxBusTag.RECREATE, true);\n    }\n\n    protected void initTheme() {\n        if (ColorUtils.isColorLight(ThemeStore.primaryColor(this))) {\n            setTheme(R.style.CAppTheme);\n        } else {\n            setTheme(R.style.CAppThemeBarDark);\n        }\n    }\n\n    public void showSnackBar(View view, String msg) {\n        showSnackBar(view, msg, Snackbar.LENGTH_SHORT);\n    }\n\n    public void showSnackBar(View view, String msg, int length) {\n        if (snackbar == null) {\n            snackbar = Snackbar.make(view, msg, length);\n        } else {\n            snackbar.setText(msg);\n            snackbar.setDuration(length);\n        }\n        snackbar.show();\n    }\n\n    public void hideSnackBar() {\n        if (snackbar != null) {\n            snackbar.dismiss();\n        }\n    }\n\n    @Override\n    public void startActivity(Intent intent) {\n        super.startActivity(intent);\n        if (MApplication.isEInkMode) {\n            overridePendingTransition(R.anim.anim_none, R.anim.anim_none);\n        }\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    @Override\n    public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {\n        super.startActivityForResult(intent, requestCode, options);\n        if (MApplication.isEInkMode) {\n            overridePendingTransition(R.anim.anim_none, R.anim.anim_none);\n        }\n    }\n\n    @Override\n    public void finish() {\n        SoftInputUtil.hideIMM(getCurrentFocus());\n        super.finish();\n        if (MApplication.isEInkMode) {\n            overridePendingTransition(R.anim.anim_none, R.anim.anim_none);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/MBaseFragment.java",
    "content": "package com.kunfei.bookshelf.base;\n\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\n\nimport com.kunfei.basemvplib.BaseFragment;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.MApplication;\n\npublic abstract class MBaseFragment<T extends IPresenter> extends BaseFragment<T> implements IView {\n    public final SharedPreferences preferences = MApplication.getConfigPreferences();\n    protected T mPresenter;\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        mPresenter = initInjector();\n        attachView();\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        detachView();\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    protected abstract T initInjector();\n\n    /**\n     * P层绑定V层\n     */\n    private void attachView() {\n        if (null != mPresenter) {\n            mPresenter.attachView(this);\n        }\n    }\n\n    /**\n     * P层解绑V层\n     */\n    private void detachView() {\n        if (null != mPresenter) {\n            mPresenter.detachView();\n        }\n    }\n\n    public void toast(String msg) {\n        Toast.makeText(this.getActivity(), msg, Toast.LENGTH_SHORT).show();\n    }\n\n    public void toast(int id) {\n        Toast.makeText(this.getActivity(), getString(id), Toast.LENGTH_SHORT).show();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/README.md",
    "content": "# 基类"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/VMBaseFragment.kt",
    "content": "package com.kunfei.bookshelf.base\n\nimport androidx.lifecycle.ViewModel\n\nabstract class VMBaseFragment<VM : ViewModel>(layoutID: Int) : BaseFragment(layoutID) {\n\n    protected abstract val viewModel: VM\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/DiffRecyclerAdapter.kt",
    "content": "package com.kunfei.bookshelf.base.adapter\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.AsyncListDiffer\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.GridLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.viewbinding.ViewBinding\n\n/**\n * Created by Invincible on 2017/12/15.\n */\n@Suppress(\"unused\", \"MemberVisibilityCanBePrivate\")\nabstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Context) :\n        RecyclerView.Adapter<ItemViewHolder>() {\n\n    val inflater: LayoutInflater = LayoutInflater.from(context)\n\n    private val asyncListDiffer: AsyncListDiffer<ITEM> by lazy {\n        AsyncListDiffer(this, diffItemCallback).apply {\n            addListListener { _, _ ->\n                onCurrentListChanged()\n            }\n        }\n    }\n\n    private val lock = Object()\n\n    private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null\n    private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null\n\n    var itemAnimation: ItemAnimation? = null\n\n    abstract val diffItemCallback: DiffUtil.ItemCallback<ITEM>\n\n    fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {\n        itemClickListener = listener\n    }\n\n    fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {\n        itemLongClickListener = listener\n    }\n\n    fun bindToRecyclerView(recyclerView: RecyclerView) {\n        recyclerView.adapter = this\n    }\n\n    fun setItems(items: List<ITEM>?) {\n        synchronized(lock) {\n            asyncListDiffer.submitList(items)\n        }\n    }\n\n    fun setItem(position: Int, item: ITEM) {\n        synchronized(lock) {\n            val list = ArrayList(asyncListDiffer.currentList)\n            list[position] = item\n            asyncListDiffer.submitList(list)\n        }\n    }\n\n    fun updateItem(item: ITEM) =\n            synchronized(lock) {\n                val index = asyncListDiffer.currentList.indexOf(item)\n                if (index >= 0) {\n                    asyncListDiffer.currentList[index] = item\n                    notifyItemChanged(index)\n                }\n            }\n\n    fun updateItem(position: Int, payload: Any) =\n            synchronized(lock) {\n                val size = itemCount\n                if (position in 0 until size) {\n                    notifyItemChanged(position, payload)\n                }\n            }\n\n    fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) =\n            synchronized(lock) {\n                val size = itemCount\n                if (fromPosition in 0 until size && toPosition in 0 until size) {\n                    notifyItemRangeChanged(\n                            fromPosition,\n                            toPosition - fromPosition + 1,\n                            payloads\n                    )\n                }\n            }\n\n    fun isEmpty() = asyncListDiffer.currentList.isEmpty()\n\n    fun isNotEmpty() = asyncListDiffer.currentList.isNotEmpty()\n\n    fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position)\n\n    fun getItems(): List<ITEM> = asyncListDiffer.currentList\n\n    /**\n     * grid 模式下使用\n     */\n    protected open fun getSpanSize(viewType: Int, position: Int) = 1\n\n    final override fun getItemCount() = getItems().size\n\n    final override fun getItemViewType(position: Int): Int {\n        return 0\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {\n        val holder = ItemViewHolder(getViewBinding(parent))\n\n        @Suppress(\"UNCHECKED_CAST\")\n        registerListener(holder, (holder.binding as VB))\n\n        if (itemClickListener != null) {\n            holder.itemView.setOnClickListener {\n                getItem(holder.layoutPosition)?.let {\n                    itemClickListener?.invoke(holder, it)\n                }\n            }\n        }\n\n        if (itemLongClickListener != null) {\n            holder.itemView.setOnLongClickListener {\n                getItem(holder.layoutPosition)?.let {\n                    itemLongClickListener?.invoke(holder, it) ?: true\n                } ?: true\n            }\n        }\n\n        return holder\n    }\n\n    protected abstract fun getViewBinding(parent: ViewGroup): VB\n\n    final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {}\n\n    open fun onCurrentListChanged() {\n\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    final override fun onBindViewHolder(\n            holder: ItemViewHolder,\n            position: Int,\n            payloads: MutableList<Any>\n    ) {\n        getItem(holder.layoutPosition)?.let {\n            convert(holder, (holder.binding as VB), it, payloads)\n        }\n    }\n\n    override fun onViewAttachedToWindow(holder: ItemViewHolder) {\n        super.onViewAttachedToWindow(holder)\n        addAnimation(holder)\n    }\n\n    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {\n        super.onAttachedToRecyclerView(recyclerView)\n        val manager = recyclerView.layoutManager\n        if (manager is GridLayoutManager) {\n            manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {\n                override fun getSpanSize(position: Int): Int {\n                    return getSpanSize(getItemViewType(position), position)\n                }\n            }\n        }\n    }\n\n    private fun addAnimation(holder: ItemViewHolder) {\n        itemAnimation?.let {\n            if (it.itemAnimEnabled) {\n                if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {\n                    startAnimation(holder, it)\n                    it.itemAnimStartPosition = holder.layoutPosition\n                }\n            }\n        }\n    }\n\n    protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {\n        item.itemAnimation?.let {\n            for (anim in it.getAnimators(holder.itemView)) {\n                anim.setDuration(item.itemAnimDuration).start()\n                anim.interpolator = item.itemAnimInterpolator\n            }\n        }\n    }\n\n    /**\n     * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,\n     * 使用getItem(holder.layoutPosition)来获取item\n     */\n    abstract fun convert(\n            holder: ItemViewHolder,\n            binding: VB,\n            item: ITEM,\n            payloads: MutableList<Any>\n    )\n\n    /**\n     * 注册事件\n     */\n    abstract fun registerListener(holder: ItemViewHolder, binding: VB)\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/ItemAnimation.kt",
    "content": "package com.kunfei.bookshelf.base.adapter\n\nimport android.view.animation.Interpolator\nimport android.view.animation.LinearInterpolator\nimport com.kunfei.bookshelf.base.adapter.animations.*\n\n/**\n * Created by Invincible on 2017/12/15.\n */\n@Suppress(\"unused\")\nclass ItemAnimation private constructor() {\n\n    var itemAnimEnabled = false\n    var itemAnimFirstOnly = true\n    var itemAnimation: BaseAnimation? = null\n    var itemAnimInterpolator: Interpolator = LinearInterpolator()\n    var itemAnimDuration: Long = 300L\n    var itemAnimStartPosition: Int = -1\n\n    fun interpolator(interpolator: Interpolator) = apply {\n        itemAnimInterpolator = interpolator\n    }\n\n    fun duration(duration: Long) = apply {\n        itemAnimDuration = duration\n    }\n\n    fun startPosition(startPos: Int) = apply {\n        itemAnimStartPosition = startPos\n    }\n\n    fun animation(animationType: Int = NONE, animation: BaseAnimation? = null) = apply {\n        if (animation != null) {\n            itemAnimation = animation\n        } else {\n            when (animationType) {\n                FADE_IN -> itemAnimation = AlphaInAnimation()\n                SCALE_IN -> itemAnimation = ScaleInAnimation()\n                BOTTOM_SLIDE_IN -> itemAnimation = SlideInBottomAnimation()\n                LEFT_SLIDE_IN -> itemAnimation = SlideInLeftAnimation()\n                RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation()\n            }\n        }\n    }\n\n    fun enabled(enabled: Boolean) = apply {\n        itemAnimEnabled = enabled\n    }\n\n    fun firstOnly(firstOnly: Boolean) = apply {\n        itemAnimFirstOnly = firstOnly\n    }\n\n    companion object {\n        const val NONE: Int = 0x00000000\n\n        /**\n         * Use with [.openLoadAnimation]\n         */\n        const val FADE_IN: Int = 0x00000001\n        /**\n         * Use with [.openLoadAnimation]\n         */\n        const val SCALE_IN: Int = 0x00000002\n        /**\n         * Use with [.openLoadAnimation]\n         */\n        const val BOTTOM_SLIDE_IN: Int = 0x00000003\n\n        /**\n         * Use with [.openLoadAnimation]\n         */\n        const val LEFT_SLIDE_IN: Int = 0x00000004\n\n        /**\n         * Use with [.openLoadAnimation]\n         */\n        const val RIGHT_SLIDE_IN: Int = 0x00000005\n\n        fun create() = ItemAnimation()\n\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/ItemViewHolder.kt",
    "content": "package com.kunfei.bookshelf.base.adapter\n\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.viewbinding.ViewBinding\n\n/**\n * Created by Invincible on 2017/11/28.\n */\n@Suppress(\"MemberVisibilityCanBePrivate\")\nclass ItemViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root)"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/RecyclerAdapter.kt",
    "content": "package com.kunfei.bookshelf.base.adapter\n\nimport android.content.Context\nimport android.util.SparseArray\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.GridLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.viewbinding.ViewBinding\nimport java.util.*\n\n/**\n * Created by Invincible on 2017/11/24.\n *\n * 通用的adapter 可添加header，footer，以及不同类型item\n */\n@Suppress(\"unused\", \"MemberVisibilityCanBePrivate\")\nabstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Context) :\n        RecyclerView.Adapter<ItemViewHolder>() {\n\n    val inflater: LayoutInflater = LayoutInflater.from(context)\n\n    private val headerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() }\n    private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() }\n\n    private val items: MutableList<ITEM> = mutableListOf()\n\n    private val lock = Object()\n\n    private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null\n    private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null\n\n    var itemAnimation: ItemAnimation? = null\n\n    fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {\n        itemClickListener = listener\n    }\n\n    fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {\n        itemLongClickListener = listener\n    }\n\n    fun bindToRecyclerView(recyclerView: RecyclerView) {\n        recyclerView.adapter = this\n    }\n\n    fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {\n        synchronized(lock) {\n            val index = headerItems.size()\n            headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header)\n            notifyItemInserted(index)\n        }\n    }\n\n    fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) =\n            synchronized(lock) {\n                val index = getActualItemCount() + footerItems.size()\n                footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer)\n                notifyItemInserted(index)\n            }\n\n\n    fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) =\n            synchronized(lock) {\n                val index = headerItems.indexOfValue(header)\n                if (index >= 0) {\n                    headerItems.remove(index)\n                    notifyItemRemoved(index)\n                }\n            }\n\n    fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) =\n            synchronized(lock) {\n                val index = footerItems.indexOfValue(footer)\n                if (index >= 0) {\n                    footerItems.remove(index)\n                    notifyItemRemoved(getActualItemCount() + index - 2)\n                }\n            }\n\n    fun setItems(items: List<ITEM>?) {\n        synchronized(lock) {\n            if (this.items.isNotEmpty()) {\n                this.items.clear()\n            }\n            if (items != null) {\n                this.items.addAll(items)\n            }\n            notifyDataSetChanged()\n            onCurrentListChanged()\n        }\n    }\n\n    fun setItems(items: List<ITEM>?, diffResult: DiffUtil.DiffResult) {\n        synchronized(lock) {\n            if (this.items.isNotEmpty()) {\n                this.items.clear()\n            }\n            if (items != null) {\n                this.items.addAll(items)\n            }\n            diffResult.dispatchUpdatesTo(this)\n            onCurrentListChanged()\n        }\n    }\n\n    fun setItem(position: Int, item: ITEM) {\n        synchronized(lock) {\n            val oldSize = getActualItemCount()\n            if (position in 0 until oldSize) {\n                this.items[position] = item\n                notifyItemChanged(position + getHeaderCount())\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun addItem(item: ITEM) {\n        synchronized(lock) {\n            val oldSize = getActualItemCount()\n            if (this.items.add(item)) {\n                notifyItemInserted(oldSize + getHeaderCount())\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun addItems(position: Int, newItems: List<ITEM>) {\n        synchronized(lock) {\n            if (this.items.addAll(position, newItems)) {\n                notifyItemRangeInserted(position + getHeaderCount(), newItems.size)\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun addItems(newItems: List<ITEM>) {\n        synchronized(lock) {\n            val oldSize = getActualItemCount()\n            if (this.items.addAll(newItems)) {\n                if (oldSize == 0 && getHeaderCount() == 0) {\n                    notifyDataSetChanged()\n                } else {\n                    notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size)\n                }\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun removeItem(position: Int) {\n        synchronized(lock) {\n            if (this.items.removeAt(position) != null) {\n                notifyItemRemoved(position + getHeaderCount())\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun removeItem(item: ITEM) {\n        synchronized(lock) {\n            if (this.items.remove(item)) {\n                notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun removeItems(items: List<ITEM>) {\n        synchronized(lock) {\n            if (this.items.removeAll(items)) {\n                notifyDataSetChanged()\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun swapItem(oldPosition: Int, newPosition: Int) {\n        synchronized(lock) {\n            val size = getActualItemCount()\n            if (oldPosition in 0 until size && newPosition in 0 until size) {\n                val srcPosition = oldPosition + getHeaderCount()\n                val targetPosition = newPosition + getHeaderCount()\n                Collections.swap(this.items, srcPosition, targetPosition)\n                notifyItemMoved(srcPosition, targetPosition)\n            }\n            onCurrentListChanged()\n        }\n    }\n\n    fun updateItem(item: ITEM) =\n            synchronized(lock) {\n                val index = this.items.indexOf(item)\n                if (index >= 0) {\n                    this.items[index] = item\n                    notifyItemChanged(index)\n                }\n                onCurrentListChanged()\n            }\n\n    fun updateItem(position: Int, payload: Any) =\n            synchronized(lock) {\n                val size = getActualItemCount()\n                if (position in 0 until size) {\n                    notifyItemChanged(position + getHeaderCount(), payload)\n                }\n            }\n\n    fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) =\n            synchronized(lock) {\n                val size = getActualItemCount()\n                if (fromPosition in 0 until size && toPosition in 0 until size) {\n                    notifyItemRangeChanged(\n                            fromPosition + getHeaderCount(),\n                            toPosition - fromPosition + 1,\n                            payloads\n                    )\n                }\n            }\n\n    fun clearItems() = synchronized(lock) {\n        this.items.clear()\n        notifyDataSetChanged()\n        onCurrentListChanged()\n    }\n\n    fun isEmpty() = items.isEmpty()\n\n    fun isNotEmpty() = items.isNotEmpty()\n\n    /**\n     * 除去header和footer\n     */\n    fun getActualItemCount() = items.size\n\n\n    fun getHeaderCount() = headerItems.size()\n\n\n    fun getFooterCount() = footerItems.size()\n\n    fun getItem(position: Int): ITEM? = items.getOrNull(position)\n\n    fun getItemByLayoutPosition(position: Int) = items.getOrNull(position - getHeaderCount())\n\n    fun getItems(): List<ITEM> = items\n\n    protected open fun getItemViewType(item: ITEM, position: Int) = 0\n\n    /**\n     * grid 模式下使用\n     */\n    protected open fun getSpanSize(viewType: Int, position: Int) = 1\n\n    final override fun getItemCount() = getActualItemCount() + getHeaderCount() + getFooterCount()\n\n    final override fun getItemViewType(position: Int) = when {\n        isHeader(position) -> TYPE_HEADER_VIEW + position\n        isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()\n        else -> getItem(getActualPosition(position))?.let {\n            getItemViewType(it, getActualPosition(position))\n        } ?: 0\n    }\n\n    open fun onCurrentListChanged() {\n\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when {\n        viewType < TYPE_HEADER_VIEW + getHeaderCount() -> {\n            ItemViewHolder(headerItems.get(viewType).invoke(parent))\n        }\n\n        viewType >= TYPE_FOOTER_VIEW -> {\n            ItemViewHolder(footerItems.get(viewType).invoke(parent))\n        }\n\n        else -> {\n            val holder = ItemViewHolder(getViewBinding(parent))\n\n            @Suppress(\"UNCHECKED_CAST\")\n            registerListener(holder, (holder.binding as VB))\n\n            if (itemClickListener != null) {\n                holder.itemView.setOnClickListener {\n                    getItem(holder.layoutPosition)?.let {\n                        itemClickListener?.invoke(holder, it)\n                    }\n                }\n            }\n\n            if (itemLongClickListener != null) {\n                holder.itemView.setOnLongClickListener {\n                    getItem(holder.layoutPosition)?.let {\n                        itemLongClickListener?.invoke(holder, it) ?: true\n                    } ?: true\n                }\n            }\n\n            holder\n        }\n    }\n\n    protected abstract fun getViewBinding(parent: ViewGroup): VB\n\n    final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {}\n\n    @Suppress(\"UNCHECKED_CAST\")\n    final override fun onBindViewHolder(\n            holder: ItemViewHolder,\n            position: Int,\n            payloads: MutableList<Any>\n    ) {\n        if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {\n            getItem(holder.layoutPosition - getHeaderCount())?.let {\n                convert(holder, (holder.binding as VB), it, payloads)\n            }\n        }\n    }\n\n    override fun onViewAttachedToWindow(holder: ItemViewHolder) {\n        super.onViewAttachedToWindow(holder)\n        if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {\n            addAnimation(holder)\n        }\n    }\n\n    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {\n        super.onAttachedToRecyclerView(recyclerView)\n        val manager = recyclerView.layoutManager\n        if (manager is GridLayoutManager) {\n            manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {\n                override fun getSpanSize(position: Int): Int {\n                    return getSpanSize(getItemViewType(position), position)\n                }\n            }\n        }\n    }\n\n    private fun isHeader(position: Int) = position < getHeaderCount()\n\n    private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount()\n\n    private fun getActualPosition(position: Int) = position - getHeaderCount()\n\n    private fun addAnimation(holder: ItemViewHolder) {\n        itemAnimation?.let {\n            if (it.itemAnimEnabled) {\n                if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {\n                    startAnimation(holder, it)\n                    it.itemAnimStartPosition = holder.layoutPosition\n                }\n            }\n        }\n    }\n\n    protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {\n        item.itemAnimation?.let {\n            for (anim in it.getAnimators(holder.itemView)) {\n                anim.setDuration(item.itemAnimDuration).start()\n                anim.interpolator = item.itemAnimInterpolator\n            }\n        }\n    }\n\n    /**\n     * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,\n     * 使用getItem(holder.layoutPosition)来获取item\n     */\n    abstract fun convert(\n            holder: ItemViewHolder,\n            binding: VB,\n            item: ITEM,\n            payloads: MutableList<Any>\n    )\n\n    /**\n     * 注册事件\n     */\n    abstract fun registerListener(holder: ItemViewHolder, binding: VB)\n\n    companion object {\n        private const val TYPE_HEADER_VIEW = Int.MIN_VALUE\n        private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999\n    }\n\n}\n\n\n\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/AlphaInAnimation.kt",
    "content": "package com.kunfei.bookshelf.base.adapter.animations\n\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.view.View\n\n\nclass AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : BaseAnimation {\n\n    override fun getAnimators(view: View): Array<Animator> =\n            arrayOf(ObjectAnimator.ofFloat(view, \"alpha\", mFrom, 1f))\n\n    companion object {\n\n        private const val DEFAULT_ALPHA_FROM = 0f\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/BaseAnimation.kt",
    "content": "package com.kunfei.bookshelf.base.adapter.animations\n\nimport android.animation.Animator\nimport android.view.View\n\n/**\n * adapter item 动画\n */\ninterface BaseAnimation {\n\n    fun getAnimators(view: View): Array<Animator>\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/ScaleInAnimation.kt",
    "content": "package com.kunfei.bookshelf.base.adapter.animations\n\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.view.View\n\n\nclass ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : BaseAnimation {\n\n    override fun getAnimators(view: View): Array<Animator> {\n        val scaleX = ObjectAnimator.ofFloat(view, \"scaleX\", mFrom, 1f)\n        val scaleY = ObjectAnimator.ofFloat(view, \"scaleY\", mFrom, 1f)\n        return arrayOf(scaleX, scaleY)\n    }\n\n    companion object {\n\n        private const val DEFAULT_SCALE_FROM = .5f\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/SlideInBottomAnimation.kt",
    "content": "package com.kunfei.bookshelf.base.adapter.animations\n\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.view.View\n\nclass SlideInBottomAnimation : BaseAnimation {\n\n\n    override fun getAnimators(view: View): Array<Animator> =\n            arrayOf(ObjectAnimator.ofFloat(view, \"translationY\", view.measuredHeight.toFloat(), 0f))\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/SlideInLeftAnimation.kt",
    "content": "package com.kunfei.bookshelf.base.adapter.animations\n\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.view.View\n\n\nclass SlideInLeftAnimation : BaseAnimation {\n\n\n    override fun getAnimators(view: View): Array<Animator> =\n            arrayOf(ObjectAnimator.ofFloat(view, \"translationX\", -view.rootView.width.toFloat(), 0f))\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/adapter/animations/SlideInRightAnimation.kt",
    "content": "package com.kunfei.bookshelf.base.adapter.animations\n\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.view.View\n\n\nclass SlideInRightAnimation : BaseAnimation {\n\n\n    override fun getAnimators(view: View): Array<Animator> =\n            arrayOf(ObjectAnimator.ofFloat(view, \"translationX\", view.rootView.width.toFloat(), 0f))\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/observer/MyObserver.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.base.observer;\n\nimport io.reactivex.Observer;\nimport io.reactivex.disposables.Disposable;\n\npublic abstract class MyObserver<T> implements Observer<T> {\n\n    @Override\n    public void onSubscribe(Disposable d) {\n\n    }\n\n    @Override\n    public void onError(Throwable e) {\n\n    }\n\n    @Override\n    public void onComplete() {\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/base/observer/MySingleObserver.java",
    "content": "package com.kunfei.bookshelf.base.observer;\n\nimport io.reactivex.SingleObserver;\nimport io.reactivex.disposables.Disposable;\n\npublic abstract class MySingleObserver<T> implements SingleObserver<T> {\n\n    @Override\n    public void onSubscribe(Disposable d) {\n\n    }\n\n    @Override\n    public void onError(Throwable e) {\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BaseBookBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport java.util.Map;\n\npublic interface BaseBookBean {\n\n    String getTag();\n\n    String getNoteUrl();\n\n    void setNoteUrl(String noteUrl);\n\n    String getVariable();\n\n    void setVariable(String variable);\n\n    void putVariable(String key, String value);\n\n    Map<String, String> getVariableMap();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BaseChapterBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\npublic interface BaseChapterBean {\n\n    String getTag();\n\n    String getDurChapterUrl();\n\n    int getDurChapterIndex();\n\n    String getNoteUrl();\n\n    String getDurChapterName();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookChapterBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\nimport android.content.Context;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\n\nimport java.util.Objects;\n\n/**\n * 章节列表\n */\n@Entity\npublic class BookChapterBean implements Cloneable, BaseChapterBean {\n    private String tag;\n    private String noteUrl; //对应BookInfoBean noteUrl;\n\n    private int durChapterIndex;  //当前章节数\n    @Id\n    private String durChapterUrl;  //当前章节对应的文章地址\n    private String durChapterName;  //当前章节名称\n    private boolean isVip;\n    private boolean isPay;\n    //章节内容在文章中的起始位置(本地)\n    private Long start;\n    //章节内容在文章中的终止位置(本地)\n    private Long end;\n\n    public BookChapterBean() {\n    }\n\n    @Generated(hash = 922679906)\n    public BookChapterBean(String tag, String noteUrl, int durChapterIndex, String durChapterUrl, String durChapterName,\n                           boolean isVip, boolean isPay, Long start, Long end) {\n        this.tag = tag;\n        this.noteUrl = noteUrl;\n        this.durChapterIndex = durChapterIndex;\n        this.durChapterUrl = durChapterUrl;\n        this.durChapterName = durChapterName;\n        this.isVip = isVip;\n        this.isPay = isPay;\n        this.start = start;\n        this.end = end;\n    }\n\n    public BookChapterBean(String tag, String durChapterName, String durChapterUrl) {\n        this.tag = tag;\n        this.durChapterName = durChapterName;\n        this.durChapterUrl = durChapterUrl;\n    }\n\n    public BookChapterBean(String tag, String durChapterName, String durChapterUrl, boolean isVip, boolean isPay) {\n        this.tag = tag;\n        this.durChapterName = durChapterName;\n        this.durChapterUrl = durChapterUrl;\n        this.isVip = isVip;\n        this.isPay = isPay;\n    }\n\n    @Override\n    protected Object clone() {\n        try {\n            Gson gson = new Gson();\n            String json = gson.toJson(this);\n            return gson.fromJson(json, BookChapterBean.class);\n        } catch (Exception ignored) {\n        }\n        return this;\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj instanceof BookChapterBean) {\n            BookChapterBean bookChapterBean = (BookChapterBean) obj;\n            return Objects.equals(bookChapterBean.durChapterUrl, durChapterUrl);\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public int hashCode() {\n        if (durChapterUrl == null) {\n            return 0;\n        }\n        return durChapterUrl.hashCode();\n    }\n\n    @Override\n    public String getTag() {\n        return this.tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    @Override\n    public String getDurChapterName() {\n        return this.durChapterName;\n    }\n\n    public void setDurChapterName(String durChapterName) {\n        this.durChapterName = durChapterName;\n    }\n\n    @Override\n    public String getDurChapterUrl() {\n        return this.durChapterUrl;\n    }\n\n    public void setDurChapterUrl(String durChapterUrl) {\n        this.durChapterUrl = durChapterUrl;\n    }\n\n    @Override\n    public int getDurChapterIndex() {\n        return this.durChapterIndex;\n    }\n\n    public void setDurChapterIndex(int durChapterIndex) {\n        this.durChapterIndex = durChapterIndex;\n    }\n\n    @Override\n    public String getNoteUrl() {\n        return this.noteUrl;\n    }\n\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    public Long getStart() {\n        return this.start;\n    }\n\n    public void setStart(Long start) {\n        this.start = start;\n    }\n\n    public Long getEnd() {\n        return this.end;\n    }\n\n    public void setEnd(Long end) {\n        this.end = end;\n    }\n\n    public Boolean getHasCache(BookInfoBean bookInfoBean) {\n        return BookshelfHelp.isChapterCached(bookInfoBean.getName(), tag, this, bookInfoBean.isAudio());\n    }\n\n    public boolean getIsVip() {\n        return this.isVip;\n    }\n\n    public void setIsVip(boolean isVip) {\n        this.isVip = isVip;\n    }\n\n    public boolean getIsPay() {\n        return this.isPay;\n    }\n\n    public void setIsPay(boolean isPay) {\n        this.isPay = isPay;\n    }\n\n    public String getDisplayTitle(Context context) {\n        if (!isVip) {\n            return durChapterName;\n        }\n        if (isPay) {\n            return context.getString(R.string.payed_title, durChapterName);\n        }\n        return context.getString(R.string.vip_title, durChapterName);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookContentBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\n\n/**\n * 书本缓存内容\n */\n@Entity\npublic class BookContentBean {\n    private String noteUrl; //对应BookInfoBean noteUrl;\n    @Id\n    private String durChapterUrl;\n\n    private Integer durChapterIndex;   //当前章节  （包括番外）\n\n    private String durChapterContent; //当前章节内容\n\n    private String tag;   //来源  某个网站/本地\n\n    private Long timeMillis;\n\n    public BookContentBean() {\n\n    }\n\n    @Generated(hash = 695554675)\n    public BookContentBean(String noteUrl, String durChapterUrl,\n                           Integer durChapterIndex, String durChapterContent, String tag,\n                           Long timeMillis) {\n        this.noteUrl = noteUrl;\n        this.durChapterUrl = durChapterUrl;\n        this.durChapterIndex = durChapterIndex;\n        this.durChapterContent = durChapterContent;\n        this.tag = tag;\n        this.timeMillis = timeMillis;\n    }\n\n    public String getDurChapterUrl() {\n        return durChapterUrl;\n    }\n\n    public void setDurChapterUrl(String durChapterUrl) {\n        this.durChapterUrl = durChapterUrl;\n    }\n\n    public int getDurChapterIndex() {\n        return durChapterIndex;\n    }\n\n    public void setDurChapterIndex(int durChapterIndex) {\n        this.durChapterIndex = durChapterIndex;\n    }\n\n    public String getDurChapterContent() {\n        return durChapterContent;\n    }\n\n    public void setDurChapterContent(String durChapterContent) {\n        this.durChapterContent = durChapterContent;\n    }\n\n    public String getTag() {\n        return tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    public String getNoteUrl() {\n        return this.noteUrl;\n    }\n\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    public void setDurChapterIndex(Integer durChapterIndex) {\n        this.durChapterIndex = durChapterIndex;\n    }\n\n    public Long getTimeMillis() {\n        return this.timeMillis;\n    }\n\n    public void setTimeMillis(Long timeMillis) {\n        this.timeMillis = timeMillis;\n    }\n\n    public boolean outTime() {\n        if (timeMillis == null) {\n            return true;\n        }\n        return timeMillis < System.currentTimeMillis();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookInfoBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.os.AsyncTask;\nimport android.text.TextUtils;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.constant.BookType;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.FileHelp;\nimport com.kunfei.bookshelf.utils.MD5Utils;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.widget.page.PageLoaderEpub;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\nimport org.greenrobot.greendao.annotation.Transient;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.util.Objects;\n\n/**\n * 书本信息\n */\n@Entity\npublic class BookInfoBean implements Cloneable {\n\n    private String name=\"\"; //小说名。初始化为空字符串，以避免由于规则不完善，书名为空时，某些操作造成的一些异常（比如删除书名为空的书）\n    private String tag;\n    @Id\n    private String noteUrl;  //如果是来源网站,则小说根地址,如果是本地则是小说本地MD5\n    private String chapterUrl;  //章节目录地址,本地目录正则\n    private long finalRefreshData;  //章节最后更新时间\n    private String coverUrl; //小说封面\n    private String author=\"\";//作者\n    private String introduce; //简介\n    private String origin; //来源\n    private String charset;//编码\n    private String bookSourceType;\n    @Transient\n    private String bookInfoHtml;\n    @Transient\n    private String chapterListHtml;\n\n    public BookInfoBean() {\n    }\n\n    @Generated(hash = 906814482)\n    public BookInfoBean(String name, String tag, String noteUrl, String chapterUrl, long finalRefreshData, String coverUrl, String author, String introduce,\n            String origin, String charset, String bookSourceType) {\n        this.name = name;\n        this.tag = tag;\n        this.noteUrl = noteUrl;\n        this.chapterUrl = chapterUrl;\n        this.finalRefreshData = finalRefreshData;\n        this.coverUrl = coverUrl;\n        this.author = author;\n        this.introduce = introduce;\n        this.origin = origin;\n        this.charset = charset;\n        this.bookSourceType = bookSourceType;\n    }\n\n    @Override\n    protected Object clone() {\n        try {\n            Gson gson = new Gson();\n            String json = gson.toJson(this);\n            return gson.fromJson(json, BookInfoBean.class);\n        } catch (Exception ignored) {\n        }\n        return this;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getTag() {\n        return tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    public String getNoteUrl() {\n        return noteUrl;\n    }\n\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    public String getChapterUrl() {\n        return chapterUrl;\n    }\n\n    public void setChapterUrl(String chapterUrl) {\n        this.chapterUrl = chapterUrl;\n    }\n\n    public long getFinalRefreshData() {\n        return finalRefreshData;\n    }\n\n    public void setFinalRefreshData(long finalRefreshData) {\n        this.finalRefreshData = finalRefreshData;\n    }\n\n    public String getCoverUrl() {\n        if (isEpub() && (TextUtils.isEmpty(coverUrl) || !(new File(coverUrl)).exists())) {\n            extractEpubCoverImage();\n            return \"\";\n        }\n        return coverUrl;\n    }\n\n    public void setCoverUrl(String coverUrl) {\n        this.coverUrl = coverUrl;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = BookshelfHelp.formatAuthor(author);\n    }\n\n    public String getIntroduce() {\n        return introduce;\n    }\n\n    public void setIntroduce(String introduce) {\n        this.introduce = introduce;\n    }\n\n    public String getOrigin() {\n        return TextUtils.isEmpty(origin) && tag.equals(BookShelfBean.LOCAL_TAG) ? StringUtils.getString(R.string.local) : origin;\n    }\n\n    public void setOrigin(String origin) {\n        this.origin = origin;\n    }\n\n    public String getCharset() {\n        return this.charset;\n    }\n\n    public void setCharset(String charset) {\n        this.charset = charset;\n    }\n\n    private void extractEpubCoverImage() {\n        AsyncTask.execute(new Runnable() {\n            @Override\n            public void run() {\n                try {\n                    FileHelp.createFolderIfNotExists(coverUrl);\n                    Bitmap cover = BitmapFactory.decodeStream(Objects.requireNonNull(PageLoaderEpub.readBook(new File(noteUrl))).getCoverImage().getInputStream());\n                    String md5Path = FileHelp.getCachePath() + File.separator + \"cover\" + File.separator + MD5Utils.strToMd5By16(noteUrl) + \".jpg\";\n                    FileOutputStream out = new FileOutputStream(new File(md5Path));\n                    cover.compress(Bitmap.CompressFormat.JPEG, 90, out);\n                    out.flush();\n                    out.close();\n                    setCoverUrl(md5Path);\n                    DbHelper.getDaoSession().getBookInfoBeanDao().insertOrReplace(BookInfoBean.this);\n                } catch (Exception ignored) {\n                }\n            }\n        });\n    }\n\n    private boolean isEpub() {\n        return Objects.equals(tag, BookShelfBean.LOCAL_TAG) && noteUrl.toLowerCase().matches(\".*\\\\.epub$\");\n    }\n\n    public String getBookSourceType() {\n        return this.bookSourceType;\n    }\n\n    public void setBookSourceType(String bookSourceType) {\n        this.bookSourceType = bookSourceType;\n    }\n\n    public boolean isAudio() {\n        return Objects.equals(BookType.AUDIO, bookSourceType);\n    }\n\n    public String getBookInfoHtml() {\n        return bookInfoHtml;\n    }\n\n    public void setBookInfoHtml(String bookInfoHtml) {\n        this.bookInfoHtml = bookInfoHtml;\n    }\n\n    public String getChapterListHtml() {\n        return chapterListHtml;\n    }\n\n    public void setChapterListHtml(String chapterListHtml) {\n        this.chapterListHtml = chapterListHtml;\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookKindBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.text.DecimalFormat;\n\npublic class BookKindBean {\n    private String wordsS;\n    private String state;\n    private String kind;\n\n    public BookKindBean(String kindS) {\n        if (TextUtils.isEmpty(kindS)) return;\n        for (String kind : kindS.split(\"[,|\\n]\")) {\n            if (StringUtils.isContainNumber(kind) && TextUtils.isEmpty(wordsS)) {\n                if (StringUtils.isNumeric(kind)) {\n                    int words = Integer.parseInt(kind);\n                    if (words > 0) {\n                        wordsS = words + \"字\";\n                        if (words > 10000) {\n                            DecimalFormat df = new DecimalFormat(\"#.#\");\n                            wordsS = df.format(words * 1.0f / 10000f) + \"万字\";\n                        }\n                    }\n                } else {\n                    wordsS = kind;\n                }\n            } else if (kind.matches(\".*[连载|完结].*\")) {\n                state = kind;\n            } else if (TextUtils.isEmpty(this.kind) && !TextUtils.isEmpty(kind)) {\n                this.kind = kind;\n            } else if (TextUtils.isEmpty(state) && !TextUtils.isEmpty(kind)) {\n                state = kind;\n            } else if (wordsS != null && state != null && this.kind != null) {\n                break;\n            }\n        }\n    }\n\n    public String getWordsS() {\n        return wordsS;\n    }\n\n    public String getState() {\n        return state;\n    }\n\n    public String getKind() {\n        return kind;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookShelfBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.constant.BookType;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\nimport org.greenrobot.greendao.annotation.Transient;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING;\n\n/**\n * 书架item Bean\n */\n\n@Entity\npublic class BookShelfBean implements Cloneable, BaseBookBean {\n    @Transient\n    public static final String LOCAL_TAG = \"loc_book\";\n    @Transient\n    private String errorMsg;\n    @Transient\n    private boolean isLoading;\n\n    @Id\n    private String noteUrl; //对应BookInfoBean noteUrl;\n    private Integer durChapter = 0;   //当前章节 （包括番外）\n    private Integer durChapterPage = 0;  // 当前章节位置   用页码\n    private Long finalDate = System.currentTimeMillis();  //最后阅读时间\n    private Boolean hasUpdate = false;  //是否有更新\n    private Integer newChapters = 0;  //更新章节数\n    private String tag;\n    private Integer serialNumber = 0; //手动排序\n    private Long finalRefreshData = System.currentTimeMillis();  //章节最后更新时间\n    private Integer group = 0;\n    private String durChapterName;\n    private String lastChapterName;\n    private Integer chapterListSize = 0;\n    private String customCoverPath;\n    private Boolean allowUpdate = true;\n    private Boolean useReplaceRule = true;\n    private String variable;\n    private Boolean replaceEnable = MApplication.getConfigPreferences().getBoolean(\"replaceEnableDefault\", true);\n\n    @Transient\n    private Map<String, String> variableMap;\n\n    @Transient\n    private BookInfoBean bookInfoBean;\n\n    public BookShelfBean() {\n\n    }\n\n    @Generated(hash = 451550884)\n    public BookShelfBean(String noteUrl, Integer durChapter, Integer durChapterPage, Long finalDate, Boolean hasUpdate,\n                         Integer newChapters, String tag, Integer serialNumber, Long finalRefreshData, Integer group,\n                         String durChapterName, String lastChapterName, Integer chapterListSize, String customCoverPath,\n                         Boolean allowUpdate, Boolean useReplaceRule, String variable, Boolean replaceEnable) {\n        this.noteUrl = noteUrl;\n        this.durChapter = durChapter;\n        this.durChapterPage = durChapterPage;\n        this.finalDate = finalDate;\n        this.hasUpdate = hasUpdate;\n        this.newChapters = newChapters;\n        this.tag = tag;\n        this.serialNumber = serialNumber;\n        this.finalRefreshData = finalRefreshData;\n        this.group = group;\n        this.durChapterName = durChapterName;\n        this.lastChapterName = lastChapterName;\n        this.chapterListSize = chapterListSize;\n        this.customCoverPath = customCoverPath;\n        this.allowUpdate = allowUpdate;\n        this.useReplaceRule = useReplaceRule;\n        this.variable = variable;\n        this.replaceEnable = replaceEnable;\n    }\n\n    @Override\n    public Object clone() {\n        try {\n            Gson gson = new Gson();\n            String json = gson.toJson(this);\n            return gson.fromJson(json, BookShelfBean.class);\n        } catch (Exception ignored) {\n        }\n        return this;\n    }\n\n    @Override\n    public String getVariable() {\n        return this.variable;\n    }\n\n    @Override\n    public void setVariable(String variable) {\n        this.variable = variable;\n    }\n\n    @Override\n    public void putVariable(String key, String value) {\n        if (variableMap == null) {\n            variableMap = new HashMap<>();\n        }\n        variableMap.put(key, value);\n        variable = new Gson().toJson(variableMap);\n    }\n\n    @Override\n    public Map<String, String> getVariableMap() {\n        if (variableMap == null && !TextUtils.isEmpty(variable)) {\n            variableMap = new Gson().fromJson(variable, MAP_STRING);\n        }\n        return variableMap;\n    }\n\n    @Override\n    public String getNoteUrl() {\n        return noteUrl;\n    }\n\n    @Override\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    public int getDurChapter() {\n        return durChapter < 0 ? 0 : durChapter;\n    }\n\n    public int getDurChapter(int chapterListSize) {\n        if (durChapter < 0 | chapterListSize == 0) {\n            return 0;\n        } else if (durChapter >= chapterListSize) {\n            return chapterListSize - 1;\n        }\n        return durChapter;\n    }\n\n    public int getDurChapterPage() {\n        return durChapterPage < 0 ? 0 : durChapterPage;\n    }\n\n    public long getFinalDate() {\n        return finalDate;\n    }\n\n    @Override\n    public String getTag() {\n        return tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    public BookInfoBean getBookInfoBean() {\n        if (bookInfoBean == null) {\n            bookInfoBean = new BookInfoBean();\n            bookInfoBean.setNoteUrl(noteUrl);\n            bookInfoBean.setTag(tag);\n        }\n        return bookInfoBean;\n    }\n\n    public void setBookInfoBean(BookInfoBean bookInfoBean) {\n        this.bookInfoBean = bookInfoBean;\n    }\n\n    public boolean getHasUpdate() {\n        return hasUpdate && !isAudio();\n    }\n\n    public int getNewChapters() {\n        return newChapters;\n    }\n\n    public String getErrorMsg() {\n        return errorMsg;\n    }\n\n    public void setErrorMsg(String errorMsg) {\n        this.errorMsg = errorMsg;\n    }\n\n    public int getSerialNumber() {\n        return this.serialNumber;\n    }\n\n    public long getFinalRefreshData() {\n        return this.finalRefreshData;\n    }\n\n    public boolean isLoading() {\n        return isLoading;\n    }\n\n    public void setLoading(boolean loading) {\n        isLoading = loading;\n    }\n\n    public int getGroup() {\n        return this.group == null ? 0 : this.group;\n    }\n\n    public void setDurChapter(Integer durChapter) {\n        this.durChapter = durChapter;\n    }\n\n    public void setDurChapterPage(Integer durChapterPage) {\n        this.durChapterPage = durChapterPage;\n    }\n\n    public void setFinalDate(Long finalDate) {\n        this.finalDate = finalDate;\n    }\n\n    public void setHasUpdate(Boolean hasUpdate) {\n        this.hasUpdate = hasUpdate;\n    }\n\n    public void setNewChapters(Integer newChapters) {\n        this.newChapters = newChapters;\n    }\n\n    public void setSerialNumber(Integer serialNumber) {\n        this.serialNumber = serialNumber;\n    }\n\n    public void setFinalRefreshData(Long finalRefreshData) {\n        this.finalRefreshData = finalRefreshData;\n    }\n\n    public void setGroup(Integer group) {\n        this.group = group;\n    }\n\n    public String getDurChapterName() {\n        return this.durChapterName;\n    }\n\n    public void setDurChapterName(String durChapterName) {\n        this.durChapterName = durChapterName;\n    }\n\n    public String getLastChapterName() {\n        return this.lastChapterName;\n    }\n\n    public void setLastChapterName(String lastChapterName) {\n        this.lastChapterName = lastChapterName;\n    }\n\n    public int getUnreadChapterNum() {\n        int num = getChapterListSize() - getDurChapter() - 1;\n        return Math.max(num, 0);\n    }\n\n    public int getChapterListSize() {\n        return this.chapterListSize == null ? 0 : this.chapterListSize;\n    }\n\n    public void setChapterListSize(Integer chapterListSize) {\n        this.chapterListSize = chapterListSize;\n    }\n\n    public String getCustomCoverPath() {\n        return this.customCoverPath;\n    }\n\n    public String getCoverPath() {\n        if (TextUtils.isEmpty(customCoverPath)) {\n            return bookInfoBean.getCoverUrl();\n        } else {\n            return this.customCoverPath;\n        }\n    }\n\n    public void setCustomCoverPath(String customCoverPath) {\n        this.customCoverPath = customCoverPath;\n    }\n\n    public Boolean getAllowUpdate() {\n        return allowUpdate == null ? true : allowUpdate;\n    }\n\n    public void setAllowUpdate(Boolean allowUpdate) {\n        this.allowUpdate = allowUpdate;\n    }\n\n    public Boolean getUseReplaceRule() {\n        return this.useReplaceRule;\n    }\n\n    public void setUseReplaceRule(Boolean useReplaceRule) {\n        this.useReplaceRule = useReplaceRule;\n    }\n\n    public boolean isAudio() {\n        return Objects.equals(bookInfoBean.getBookSourceType(), BookType.AUDIO);\n    }\n\n    public Boolean getReplaceEnable() {\n        return replaceEnable == null ? MApplication.getConfigPreferences().getBoolean(\"replaceEnableDefault\", true) : replaceEnable;\n    }\n\n    public String getName() {\n        return bookInfoBean.getName();\n    }\n\n    public String getAuthor() {\n        return bookInfoBean.getAuthor();\n    }\n\n    public void setReplaceEnable(Boolean replaceEnable) {\n        this.replaceEnable = replaceEnable;\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookSource3Bean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport com.google.gson.Gson;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport kotlin.jvm.Transient;\n\n// 解析阅读3.0的书源规则,toBookSourceBean()方法转换为阅读2.0书源规则\n// https://raw.githubusercontent.com/gedoor/legado/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt\npublic class BookSource3Bean {\n    private String bookSourceName = \"\";                // 名称\n    private String bookSourceGroup;            // 分组\n    private String bookSourceUrl = \"\";            // 地址，包括 http/https\n    private int bookSourceType = 0;    // 类型，0 文本，1 音频\n    private String bookUrlPattern;            // 详情页url正则\n    private int customOrder = 0;                      // 手动排序编号\n    private Boolean enabled = true;                    // 是否启用\n    private Boolean enabledExplore = true;             // 启用发现\n    private String header;                     // 请求头\n    private String loginUrl;                   // 登录地址\n    private String loginUi;\n    private String loginCheckJs;\n    private String bookSourceComment;            // 注释\n    private Long lastUpdateTime = 0L;               // 最后更新时间，用于排序\n    private int weight = 0;                            // 智能排序的权重\n    private String exploreUrl;                // 发现url\n    private ExploreRule ruleExplore;          // 发现规则\n    private String searchUrl;                 // 搜索url\n    private SearchRule ruleSearch;             // 搜索规则\n    private BookInfoRule ruleBookInfo;        // 书籍信息页规则\n    private TocRule ruleToc;                 // 目录页规则\n    private ContentRule ruleContent;            // 正文页规则\n\n    @Transient\n    private String userAgent;\n    @Transient\n    private String RuleSearchUrl;\n\n    class ContentRule {\n        String content;\n        String nextContentUrl;\n        String webJs;\n        String sourceRegex;\n        String replaceRegex;\n        String imageStyle;  //默认大小居中,FULL最大宽度\n        String actions;\n    }\n\n    class TocRule {\n        String chapterList;\n        String chapterName;\n        String chapterUrl;\n        String isVip;\n        String isPay;\n        String updateTime;\n        String nextTocUrl;\n    }\n\n    class BookInfoRule {\n        String init;\n        String name;\n        String author;\n        String intro;\n        String kind;\n        String lastChapter;\n        String updateTime;\n        String coverUrl;\n        String tocUrl;\n        String wordCount;\n    }\n\n    class SearchRule {\n        String bookList;\n        String name;\n        String author;\n        String intro;\n        String kind;\n        String lastChapter;\n        String updateTime;\n        String bookUrl;\n        String coverUrl;\n        String wordCount;\n    }\n\n    class ExploreRule {\n        String bookList;\n        String name;\n        String author;\n        String intro;\n        String kind;\n        String lastChapter;\n        String updateTime;\n        String bookUrl;\n        String coverUrl;\n        String wordCount;\n    }\n\n/*    @Override\n    public Object clone() {\n        try {\n            Gson gson = new Gson();\n            String json = gson.toJson(this);\n            return gson.fromJson(json, BookSource3Bean.class);\n        } catch (Exception ignored) {\n        }\n        return this;\n    }*/\n\n    // 给书源增加一个标签\n    public BookSource3Bean addGroupTag(String tag) {\n        if (this.bookSourceGroup != null) {\n            //为了避免空格、首尾位置的差异造成影响，这里做循环处理\n            String[] tags = (this.bookSourceGroup + \";\" + tag).split(\";\");\n\n            List<String> list = new ArrayList<>();\n            list.add(tag);\n            for (String s : tags) {\n                if (!list.contains(s)) {\n                    list.add(s);\n                }\n            }\n            bookSourceGroup = tag;\n            for (int i = 1; i < list.size(); i++) {\n                bookSourceGroup = bookSourceGroup + \";\" + list.get(i);\n            }\n        }\n        return this;\n    }\n\n    class httpRequest {\n        String method;\n        String body;\n        String headers;\n        String charset;\n    }\n\n    private String searchUrl2RuleSearchUrl(String searchUrl) {\n        String RuleSearchUrl = searchUrl;\n        if (searchUrl != null) {\n            String q = \"\";\n            if (searchUrl.replaceAll(\"\\\\s\", \"\").matches(\"^[^,]+,\\\\{.+\\\\}\")) {\n                // 正常网址不包含逗号\n                String[] strings = searchUrl.split(\",\", 2);\n                try {\n                    Gson gson = new Gson();\n                    httpRequest request = gson.fromJson(strings[1], httpRequest.class);\n                    if (gson.toJson(request).replaceAll(\"\\\\s\", \"\").length() > 0) {\n                        // 阅读2.0没有单独的header，只有useragent\n                        if (request.headers != null) {\n                            if (this.header == null)\n                                this.header = request.headers;\n                            else if (request.headers.trim().length() < 1)\n                                this.header = request.headers;\n                        }\n\n                        if (request.charset != null) {\n                            if (request.charset.trim().length() > 0)\n                                q = q + \"|char=\" + request.charset;\n                        }\n\n                        if (request.body != null) {\n                            q = request.body\n                                    .replace(\"{{key}}\", \"searchKey\")\n                                    .replaceFirst(\"\\\\{\\\\{([^{}]*)page([^{}]*)\\\\}\\\\}\", \"$1searchPage$2\")\n                                    + q;\n\n                            // post请求的关键词一定在第二部分\n                            if (request.method != null) {\n                                if (request.method.toLowerCase().contains(\"post\"))\n                                    q = \"@\" + q;\n                                else\n                                    q = \"?\" + q;\n                            }\n                            return strings[0] + q;\n                        }\n\n                        RuleSearchUrl = strings[0] + q;\n                    } else\n                        RuleSearchUrl = searchUrl;\n\n                } catch (Exception e) {\n                    e.printStackTrace();\n                    RuleSearchUrl = searchUrl;\n                }\n            }\n\n            return RuleSearchUrl.replaceAll(\"\\\\s\", \"\")\n                    .replace(\"{{key}}\", \"searchKey\")\n                    .replaceFirst(\"\\\\{\\\\{([^{}]*)page([^{}]*)\\\\}\\\\}\", \"$1searchPage$2\")\n                    ;\n        }\n        return null;\n    }\n\n    public BookSourceBean toBookSourceBean() {\n        // 带注释的行，表示2.0/3.0书源json的数据命名不同。注释后方为2.0名称\n        String bookSourceType = \"\";\n        if (this.bookSourceType != 0)\n            bookSourceType = \"\" + this.bookSourceType;\n\n        RuleSearchUrl = searchUrl2RuleSearchUrl(searchUrl);\n\n        if (header != null && userAgent == null) {\n            if (header.matches(\"(?!).*(User-Agent).*\"))\n                userAgent = header.replaceFirst(\"(?!).*(User-Agent)[\\\\s:]+\\\"([^\\\"]+)\\\".*\", \"$2\");\n        }\n\n        String ruleFindUrl=null;\n        if(exploreUrl!=null){\n            ruleFindUrl=exploreUrl.replaceAll(\"\\\\{\\\\{page\\\\}\\\\}\",\"searchPage\");\n        }\n\n        // 暂时只给发现和搜索添加了header\n        String header=\"\";\n        if(this.header!=null){\n            if(this.header.trim().length()>0)\n            header=\"@Header:\"+this.header.replaceAll(\"\\\\n\",\" \");\n        }\n\n\n        return new BookSourceBean(\n                bookSourceUrl,\n                bookSourceName,\n                bookSourceGroup,\n                bookSourceType,\n                userAgent, //  httpUserAgent\n                loginUrl,\n                loginUi,\n                loginCheckJs,\n                lastUpdateTime,\n                0, //u  serialNumber,\n                weight,\n                true, //u enable,\n                ruleFindUrl + header,//发现规则 ruleFindUrl,\n                ruleExplore.bookList,  //  列表 ruleFindList,\n                ruleExplore.name,//  ruleFindName,\n                ruleExplore.author,//   ruleFindAuthor,\n                ruleExplore.kind,//  ruleFindKind,\n                ruleExplore.intro,//  ruleFindIntroduce,\n                ruleExplore.lastChapter,//    ruleFindLastChapter,\n                ruleExplore.coverUrl,//   ruleFindCoverUrl,\n                ruleExplore.bookUrl,//???   ruleFindNoteUrl,\n                RuleSearchUrl+header,//   ruleSearchUrl,\n                ruleSearch.bookList,//  ruleSearchList,\n                ruleSearch.name,// ruleSearchName,\n                ruleSearch.author,// ruleSearchAuthor,\n                ruleSearch.kind,// ruleSearchKind,\n                ruleSearch.intro,// ruleSearchIntroduce,\n                ruleSearch.lastChapter,//  ruleSearchLastChapter,\n                ruleSearch.coverUrl,//ruleSearchCoverUrl,\n                ruleSearch.bookUrl,//  ruleSearchNoteUrl,\n                bookUrlPattern, //???  ruleBookUrlPattern,\n                ruleBookInfo.init,//      ruleBookInfoInit,\n                ruleBookInfo.name,//       ruleBookName,\n                ruleBookInfo.author,//       ruleBookAuthor,\n                ruleBookInfo.coverUrl,//   ruleCoverUrl,\n                ruleBookInfo.intro,//   ruleIntroduce,\n                ruleBookInfo.kind,//   ruleBookKind,\n                ruleBookInfo.lastChapter,//    ruleBookLastChapter,\n                ruleBookInfo.tocUrl,//      ruleChapterUrl,\n                ruleToc.nextTocUrl,//       ruleChapterUrlNext,\n                ruleToc.chapterList,//   ruleChapterList,\n                ruleToc.chapterName,//     ruleChapterName,\n                ruleToc.chapterUrl,  // ruleContentUrl,\n                ruleToc.isVip,\n                ruleToc.isPay,\n                ruleContent.nextContentUrl, //ruleContentUrlNext,\n                ruleContent.content, //  ruleBookContent,\n                ruleContent.replaceRegex,//    ruleBookContentReplace,\n                ruleContent.actions\n        );\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookSourceBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport static android.text.TextUtils.isEmpty;\nimport static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING;\nimport static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE;\n\nimport android.text.TextUtils;\nimport android.util.Pair;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.help.JsExtensions;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders;\nimport com.kunfei.bookshelf.utils.ACache;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\nimport org.greenrobot.greendao.annotation.NotNull;\nimport org.greenrobot.greendao.annotation.OrderBy;\nimport org.greenrobot.greendao.annotation.Transient;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport javax.script.SimpleBindings;\n\n/**\n * Created by GKF on 2017/12/14.\n * 书源信息\n */\n@SuppressWarnings(\"unused\")\n@Entity\npublic class BookSourceBean implements Cloneable, JsExtensions {\n    @Id\n    private String bookSourceUrl;\n    private String bookSourceName;\n    private String bookSourceGroup;\n    private String bookSourceType;\n    private String httpUserAgent;\n    private String loginUrl;\n    private String loginUi;\n    private String loginCheckJs;\n    private Long lastUpdateTime;\n    @OrderBy\n    private int serialNumber;\n    @OrderBy\n    @NotNull\n    private int weight = 0;\n    private boolean enable;\n    //发现规则\n    private String ruleFindUrl;\n    private String ruleFindList;\n    private String ruleFindName;\n    private String ruleFindAuthor;\n    private String ruleFindKind;\n    private String ruleFindIntroduce;\n    private String ruleFindLastChapter;\n    private String ruleFindCoverUrl;\n    private String ruleFindNoteUrl;\n    //搜索规则\n    private String ruleSearchUrl;\n    private String ruleSearchList;\n    private String ruleSearchName;\n    private String ruleSearchAuthor;\n    private String ruleSearchKind;\n    private String ruleSearchIntroduce;\n    private String ruleSearchLastChapter;\n    private String ruleSearchCoverUrl;\n    private String ruleSearchNoteUrl;\n    //详情页规则\n    private String ruleBookUrlPattern;\n    private String ruleBookInfoInit;\n    private String ruleBookName;\n    private String ruleBookAuthor;\n    private String ruleCoverUrl;\n    private String ruleIntroduce;\n    private String ruleBookKind;\n    private String ruleBookLastChapter;\n    private String ruleChapterUrl;\n    //目录页规则\n    private String ruleChapterUrlNext;\n    private String ruleChapterList;\n    private String ruleChapterName;\n    private String ruleContentUrl;\n    private String ruleChapterVip;\n    private String ruleChapterPay;\n    //正文页规则\n    private String ruleContentUrlNext;\n    private String ruleBookContent;\n    private String ruleBookContentReplace;\n    private String payAction;\n\n    @Transient\n    private transient ArrayList<String> groupList;\n\n    @Generated(hash = 1152082575)\n    public BookSourceBean(String bookSourceUrl, String bookSourceName, String bookSourceGroup, String bookSourceType, String httpUserAgent,\n                          String loginUrl, String loginUi, String loginCheckJs, Long lastUpdateTime, int serialNumber, int weight, boolean enable, String ruleFindUrl,\n                          String ruleFindList, String ruleFindName, String ruleFindAuthor, String ruleFindKind, String ruleFindIntroduce, String ruleFindLastChapter,\n                          String ruleFindCoverUrl, String ruleFindNoteUrl, String ruleSearchUrl, String ruleSearchList, String ruleSearchName, String ruleSearchAuthor,\n                          String ruleSearchKind, String ruleSearchIntroduce, String ruleSearchLastChapter, String ruleSearchCoverUrl, String ruleSearchNoteUrl,\n                          String ruleBookUrlPattern, String ruleBookInfoInit, String ruleBookName, String ruleBookAuthor, String ruleCoverUrl, String ruleIntroduce,\n                          String ruleBookKind, String ruleBookLastChapter, String ruleChapterUrl, String ruleChapterUrlNext, String ruleChapterList,\n                          String ruleChapterName, String ruleContentUrl, String ruleChapterVip, String ruleChapterPay, String ruleContentUrlNext,\n                          String ruleBookContent, String ruleBookContentReplace, String payAction) {\n        this.bookSourceUrl = bookSourceUrl;\n        this.bookSourceName = bookSourceName;\n        this.bookSourceGroup = bookSourceGroup;\n        this.bookSourceType = bookSourceType;\n        this.httpUserAgent = httpUserAgent;\n        this.loginUrl = loginUrl;\n        this.loginUi = loginUi;\n        this.loginCheckJs = loginCheckJs;\n        this.lastUpdateTime = lastUpdateTime;\n        this.serialNumber = serialNumber;\n        this.weight = weight;\n        this.enable = enable;\n        this.ruleFindUrl = ruleFindUrl;\n        this.ruleFindList = ruleFindList;\n        this.ruleFindName = ruleFindName;\n        this.ruleFindAuthor = ruleFindAuthor;\n        this.ruleFindKind = ruleFindKind;\n        this.ruleFindIntroduce = ruleFindIntroduce;\n        this.ruleFindLastChapter = ruleFindLastChapter;\n        this.ruleFindCoverUrl = ruleFindCoverUrl;\n        this.ruleFindNoteUrl = ruleFindNoteUrl;\n        this.ruleSearchUrl = ruleSearchUrl;\n        this.ruleSearchList = ruleSearchList;\n        this.ruleSearchName = ruleSearchName;\n        this.ruleSearchAuthor = ruleSearchAuthor;\n        this.ruleSearchKind = ruleSearchKind;\n        this.ruleSearchIntroduce = ruleSearchIntroduce;\n        this.ruleSearchLastChapter = ruleSearchLastChapter;\n        this.ruleSearchCoverUrl = ruleSearchCoverUrl;\n        this.ruleSearchNoteUrl = ruleSearchNoteUrl;\n        this.ruleBookUrlPattern = ruleBookUrlPattern;\n        this.ruleBookInfoInit = ruleBookInfoInit;\n        this.ruleBookName = ruleBookName;\n        this.ruleBookAuthor = ruleBookAuthor;\n        this.ruleCoverUrl = ruleCoverUrl;\n        this.ruleIntroduce = ruleIntroduce;\n        this.ruleBookKind = ruleBookKind;\n        this.ruleBookLastChapter = ruleBookLastChapter;\n        this.ruleChapterUrl = ruleChapterUrl;\n        this.ruleChapterUrlNext = ruleChapterUrlNext;\n        this.ruleChapterList = ruleChapterList;\n        this.ruleChapterName = ruleChapterName;\n        this.ruleContentUrl = ruleContentUrl;\n        this.ruleChapterVip = ruleChapterVip;\n        this.ruleChapterPay = ruleChapterPay;\n        this.ruleContentUrlNext = ruleContentUrlNext;\n        this.ruleBookContent = ruleBookContent;\n        this.ruleBookContentReplace = ruleBookContentReplace;\n        this.payAction = payAction;\n    }\n\n    public BookSourceBean(BookSourceBean sourceBean) {\n        this.bookSourceUrl = sourceBean.bookSourceUrl;\n        this.bookSourceName = sourceBean.bookSourceName;\n        this.bookSourceGroup = sourceBean.bookSourceGroup;\n        this.bookSourceType = sourceBean.bookSourceType;\n        this.loginUrl = sourceBean.loginUrl;\n        this.lastUpdateTime = sourceBean.lastUpdateTime;\n        this.serialNumber = sourceBean.serialNumber;\n        this.weight = sourceBean.weight;\n        this.enable = sourceBean.enable;\n        this.ruleFindUrl = sourceBean.ruleFindUrl;\n        this.ruleFindList = sourceBean.ruleFindList;\n        this.ruleFindName = sourceBean.ruleFindName;\n        this.ruleFindAuthor = sourceBean.ruleFindAuthor;\n        this.ruleFindKind = sourceBean.ruleFindKind;\n        this.ruleFindIntroduce = sourceBean.ruleFindIntroduce;\n        this.ruleFindLastChapter = sourceBean.ruleFindLastChapter;\n        this.ruleFindCoverUrl = sourceBean.ruleFindCoverUrl;\n        this.ruleFindNoteUrl = sourceBean.ruleFindNoteUrl;\n        this.ruleSearchUrl = sourceBean.ruleSearchUrl;\n        this.ruleSearchList = sourceBean.ruleSearchList;\n        this.ruleSearchName = sourceBean.ruleSearchName;\n        this.ruleSearchAuthor = sourceBean.ruleSearchAuthor;\n        this.ruleSearchKind = sourceBean.ruleSearchKind;\n        this.ruleSearchIntroduce = sourceBean.ruleSearchIntroduce;\n        this.ruleSearchLastChapter = sourceBean.ruleSearchLastChapter;\n        this.ruleSearchCoverUrl = sourceBean.ruleSearchCoverUrl;\n        this.ruleSearchNoteUrl = sourceBean.ruleSearchNoteUrl;\n        this.ruleBookUrlPattern = sourceBean.ruleBookUrlPattern;\n        this.ruleBookInfoInit = sourceBean.ruleBookInfoInit;\n        this.ruleBookName = sourceBean.ruleBookName;\n        this.ruleBookAuthor = sourceBean.ruleBookAuthor;\n        this.ruleCoverUrl = sourceBean.ruleCoverUrl;\n        this.ruleIntroduce = sourceBean.ruleIntroduce;\n        this.ruleBookKind = sourceBean.ruleBookKind;\n        this.ruleBookLastChapter = sourceBean.ruleBookLastChapter;\n        this.ruleChapterUrl = sourceBean.ruleChapterUrl;\n        this.ruleChapterUrlNext = sourceBean.ruleChapterUrlNext;\n        this.ruleChapterList = sourceBean.ruleChapterList;\n        this.ruleChapterName = sourceBean.ruleChapterName;\n        this.ruleContentUrl = sourceBean.ruleContentUrl;\n        this.ruleContentUrlNext = sourceBean.ruleContentUrlNext;\n        this.ruleBookContent = sourceBean.ruleBookContent;\n        this.ruleBookContentReplace = sourceBean.ruleBookContentReplace;\n        this.httpUserAgent = sourceBean.httpUserAgent;\n    }\n\n    public BookSourceBean() {\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n        if (obj instanceof BookSourceBean) {\n            BookSourceBean bs = (BookSourceBean) obj;\n            return stringEquals(bookSourceUrl, bs.bookSourceUrl)\n                    && stringEquals(bookSourceName, bs.bookSourceName)\n                    && stringEquals(bookSourceType, bs.bookSourceType)\n                    && stringEquals(loginUrl, bs.loginUrl)\n                    && stringEquals(bookSourceGroup, bs.bookSourceGroup)\n                    && stringEquals(ruleBookName, bs.ruleBookName)\n                    && stringEquals(ruleBookAuthor, bs.ruleBookAuthor)\n                    && stringEquals(ruleChapterUrl, bs.ruleChapterUrl)\n                    && stringEquals(ruleChapterUrlNext, ruleChapterUrlNext)\n                    && stringEquals(ruleCoverUrl, bs.ruleCoverUrl)\n                    && stringEquals(ruleIntroduce, bs.ruleIntroduce)\n                    && stringEquals(ruleChapterList, bs.ruleChapterList)\n                    && stringEquals(ruleChapterName, bs.ruleChapterName)\n                    && stringEquals(ruleContentUrl, bs.ruleContentUrl)\n                    && stringEquals(ruleContentUrlNext, bs.ruleContentUrlNext)\n                    && stringEquals(ruleBookContent, bs.ruleBookContent)\n                    && stringEquals(ruleSearchUrl, bs.ruleSearchUrl)\n                    && stringEquals(ruleSearchList, bs.ruleSearchList)\n                    && stringEquals(ruleSearchName, bs.ruleSearchName)\n                    && stringEquals(ruleSearchAuthor, bs.ruleSearchAuthor)\n                    && stringEquals(ruleSearchKind, bs.ruleSearchKind)\n                    && stringEquals(ruleSearchLastChapter, bs.ruleSearchLastChapter)\n                    && stringEquals(ruleSearchCoverUrl, bs.ruleSearchCoverUrl)\n                    && stringEquals(ruleSearchNoteUrl, bs.ruleSearchNoteUrl)\n                    && stringEquals(httpUserAgent, bs.httpUserAgent)\n                    && stringEquals(ruleBookKind, bs.ruleBookKind)\n                    && stringEquals(ruleBookLastChapter, bs.ruleBookLastChapter)\n                    && stringEquals(ruleBookUrlPattern, bs.ruleBookUrlPattern)\n                    && stringEquals(ruleBookContentReplace, bs.ruleBookContentReplace);\n        }\n        return false;\n    }\n\n    private Boolean stringEquals(String str1, String str2) {\n        return Objects.equals(str1, str2) || (isEmpty(str1) && isEmpty(str2));\n    }\n\n    @Override\n    public Object clone() {\n        try {\n            Gson gson = new Gson();\n            String json = gson.toJson(this);\n            return gson.fromJson(json, BookSourceBean.class);\n        } catch (Exception ignored) {\n        }\n        return this;\n    }\n\n    public String getBookSourceName() {\n        return bookSourceName;\n    }\n\n    public void setBookSourceName(String bookSourceName) {\n        this.bookSourceName = bookSourceName;\n    }\n\n    public String getBookSourceUrl() {\n        return bookSourceUrl;\n    }\n\n    public void setBookSourceUrl(String bookSourceUrl) {\n        this.bookSourceUrl = bookSourceUrl;\n    }\n\n    public int getSerialNumber() {\n        return this.serialNumber;\n    }\n\n    public void setSerialNumber(int serialNumber) {\n        this.serialNumber = serialNumber;\n    }\n\n    public int getWeight() {\n        return this.weight;\n    }\n\n    public void setWeight(int weight) {\n        this.weight = weight;\n    }\n\n    // 换源时选择的源权重+500\n    public void increaseWeightBySelection() {\n        this.weight += 500;\n    }\n\n    public void increaseWeight(int increase) {\n        this.weight += increase;\n    }\n\n    public boolean getEnable() {\n        return this.enable;\n    }\n\n    public void setEnable(boolean enable) {\n        this.enable = enable;\n    }\n\n    public String getRuleBookName() {\n        return this.ruleBookName;\n    }\n\n    public void setRuleBookName(String ruleBookName) {\n        this.ruleBookName = ruleBookName;\n    }\n\n    public String getRuleBookAuthor() {\n        return this.ruleBookAuthor;\n    }\n\n    public void setRuleBookAuthor(String ruleBookAuthor) {\n        this.ruleBookAuthor = ruleBookAuthor;\n    }\n\n    public String getRuleChapterUrl() {\n        return this.ruleChapterUrl;\n    }\n\n    public void setRuleChapterUrl(String ruleChapterUrl) {\n        this.ruleChapterUrl = ruleChapterUrl;\n    }\n\n    public String getRuleCoverUrl() {\n        return this.ruleCoverUrl;\n    }\n\n    public void setRuleCoverUrl(String ruleCoverUrl) {\n        this.ruleCoverUrl = ruleCoverUrl;\n    }\n\n    public String getRuleIntroduce() {\n        return this.ruleIntroduce;\n    }\n\n    public void setRuleIntroduce(String ruleIntroduce) {\n        this.ruleIntroduce = ruleIntroduce;\n    }\n\n    public String getRuleBookContent() {\n        return this.ruleBookContent;\n    }\n\n    public void setRuleBookContent(String ruleBookContent) {\n        this.ruleBookContent = ruleBookContent;\n    }\n\n    public String getRuleSearchUrl() {\n        return this.ruleSearchUrl;\n    }\n\n    public void setRuleSearchUrl(String ruleSearchUrl) {\n        this.ruleSearchUrl = ruleSearchUrl;\n    }\n\n    public String getRuleContentUrl() {\n        return this.ruleContentUrl;\n    }\n\n    public void setRuleContentUrl(String ruleContentUrl) {\n        this.ruleContentUrl = ruleContentUrl;\n    }\n\n    public String getRuleSearchName() {\n        return this.ruleSearchName;\n    }\n\n    public void setRuleSearchName(String ruleSearchName) {\n        this.ruleSearchName = ruleSearchName;\n    }\n\n    public String getRuleSearchAuthor() {\n        return this.ruleSearchAuthor;\n    }\n\n    public void setRuleSearchAuthor(String ruleSearchAuthor) {\n        this.ruleSearchAuthor = ruleSearchAuthor;\n    }\n\n    public String getRuleSearchKind() {\n        return this.ruleSearchKind;\n    }\n\n    public void setRuleSearchKind(String ruleSearchKind) {\n        this.ruleSearchKind = ruleSearchKind;\n    }\n\n    public String getRuleSearchLastChapter() {\n        return this.ruleSearchLastChapter;\n    }\n\n    public void setRuleSearchLastChapter(String ruleSearchLastChapter) {\n        this.ruleSearchLastChapter = ruleSearchLastChapter;\n    }\n\n    public String getRuleSearchCoverUrl() {\n        return this.ruleSearchCoverUrl;\n    }\n\n    public void setRuleSearchCoverUrl(String ruleSearchCoverUrl) {\n        this.ruleSearchCoverUrl = ruleSearchCoverUrl;\n    }\n\n    public String getRuleSearchNoteUrl() {\n        return this.ruleSearchNoteUrl;\n    }\n\n    public void setRuleSearchNoteUrl(String ruleSearchNoteUrl) {\n        this.ruleSearchNoteUrl = ruleSearchNoteUrl;\n    }\n\n    public String getRuleSearchList() {\n        return this.ruleSearchList;\n    }\n\n    public void setRuleSearchList(String ruleSearchList) {\n        this.ruleSearchList = ruleSearchList;\n    }\n\n    public String getRuleChapterList() {\n        return this.ruleChapterList;\n    }\n\n    public void setRuleChapterList(String ruleChapterList) {\n        this.ruleChapterList = ruleChapterList;\n    }\n\n    public String getRuleChapterName() {\n        return this.ruleChapterName;\n    }\n\n    public void setRuleChapterName(String ruleChapterName) {\n        this.ruleChapterName = ruleChapterName;\n    }\n\n    public String getHttpUserAgent() {\n        return this.httpUserAgent;\n    }\n\n    public void setHttpUserAgent(String httpHeaders) {\n        this.httpUserAgent = httpHeaders;\n    }\n\n    public String getRuleFindUrl() {\n        return this.ruleFindUrl;\n    }\n\n    public void setRuleFindUrl(String ruleFindUrl) {\n        this.ruleFindUrl = ruleFindUrl;\n    }\n\n    public String getBookSourceGroup() {\n        return this.bookSourceGroup;\n    }\n\n    public void setBookSourceGroup(String bookSourceGroup) {\n        this.bookSourceGroup = bookSourceGroup;\n        upGroupList();\n        this.bookSourceGroup = TextUtils.join(\"; \", groupList);\n    }\n\n    public String getRuleChapterUrlNext() {\n        return this.ruleChapterUrlNext;\n    }\n\n    public void setRuleChapterUrlNext(String ruleChapterUrlNext) {\n        this.ruleChapterUrlNext = ruleChapterUrlNext;\n    }\n\n    public String getRuleContentUrlNext() {\n        return this.ruleContentUrlNext;\n    }\n\n    public void setRuleContentUrlNext(String ruleContentUrlNext) {\n        this.ruleContentUrlNext = ruleContentUrlNext;\n    }\n\n    public String getRuleBookUrlPattern() {\n        return ruleBookUrlPattern;\n    }\n\n    public void setRuleBookUrlPattern(String ruleBookUrlPattern) {\n        this.ruleBookUrlPattern = ruleBookUrlPattern;\n    }\n\n    public String getRuleBookKind() {\n        return ruleBookKind;\n    }\n\n    public void setRuleBookKind(String ruleBookKind) {\n        this.ruleBookKind = ruleBookKind;\n    }\n\n    public String getRuleBookLastChapter() {\n        return ruleBookLastChapter;\n    }\n\n    public void setRuleBookLastChapter(String ruleBookLastChapter) {\n        this.ruleBookLastChapter = ruleBookLastChapter;\n    }\n\n    private void upGroupList() {\n        if (groupList == null)\n            groupList = new ArrayList<>();\n        else\n            groupList.clear();\n        if (!TextUtils.isEmpty(bookSourceGroup)) {\n            for (String group : bookSourceGroup.split(\"\\\\s*[,;，；]\\\\s*\")) {\n                group = group.trim();\n                if (TextUtils.isEmpty(group) || groupList.contains(group)) continue;\n                groupList.add(group);\n            }\n        }\n    }\n\n    public void addGroup(String group) {\n        if (groupList == null)\n            upGroupList();\n        if (!groupList.contains(group)) {\n            groupList.add(group);\n            updateModTime();\n            bookSourceGroup = TextUtils.join(\"; \", groupList);\n        }\n    }\n\n    public void removeGroup(String group) {\n        if (groupList == null)\n            upGroupList();\n        if (groupList.contains(group)) {\n            groupList.remove(group);\n            updateModTime();\n            bookSourceGroup = TextUtils.join(\"; \", groupList);\n        }\n    }\n\n    public boolean containsGroup(String group) {\n        if (groupList == null) {\n            upGroupList();\n        }\n        return groupList.contains(group);\n    }\n\n    public Long getLastUpdateTime() {\n        return lastUpdateTime;\n    }\n\n    public void setLastUpdateTime(Long lastUpdateTime) {\n        this.lastUpdateTime = lastUpdateTime;\n    }\n\n    public void updateModTime() {\n        this.lastUpdateTime = System.currentTimeMillis();\n    }\n\n    public String getLoginUrl() {\n        return this.loginUrl;\n    }\n\n    public void setLoginUrl(String loginUrl) {\n        this.loginUrl = loginUrl;\n    }\n\n    public String getBookSourceType() {\n        return bookSourceType == null ? \"\" : bookSourceType;\n    }\n\n    public void setBookSourceType(String bookSourceType) {\n        this.bookSourceType = bookSourceType;\n    }\n\n    public String getRuleSearchIntroduce() {\n        return this.ruleSearchIntroduce;\n    }\n\n    public void setRuleSearchIntroduce(String ruleSearchIntroduce) {\n        this.ruleSearchIntroduce = ruleSearchIntroduce;\n    }\n\n    public String getRuleFindList() {\n        return this.ruleFindList;\n    }\n\n    public void setRuleFindList(String ruleFindList) {\n        this.ruleFindList = ruleFindList;\n    }\n\n    public String getRuleFindName() {\n        return this.ruleFindName;\n    }\n\n    public void setRuleFindName(String ruleFindName) {\n        this.ruleFindName = ruleFindName;\n    }\n\n    public String getRuleFindAuthor() {\n        return this.ruleFindAuthor;\n    }\n\n    public void setRuleFindAuthor(String ruleFindAuthor) {\n        this.ruleFindAuthor = ruleFindAuthor;\n    }\n\n    public String getRuleFindKind() {\n        return this.ruleFindKind;\n    }\n\n    public void setRuleFindKind(String ruleFindKind) {\n        this.ruleFindKind = ruleFindKind;\n    }\n\n    public String getRuleFindIntroduce() {\n        return this.ruleFindIntroduce;\n    }\n\n    public void setRuleFindIntroduce(String ruleFindIntroduce) {\n        this.ruleFindIntroduce = ruleFindIntroduce;\n    }\n\n    public String getRuleFindLastChapter() {\n        return this.ruleFindLastChapter;\n    }\n\n    public void setRuleFindLastChapter(String ruleFindLastChapter) {\n        this.ruleFindLastChapter = ruleFindLastChapter;\n    }\n\n    public String getRuleFindCoverUrl() {\n        return this.ruleFindCoverUrl;\n    }\n\n    public void setRuleFindCoverUrl(String ruleFindCoverUrl) {\n        this.ruleFindCoverUrl = ruleFindCoverUrl;\n    }\n\n    public String getRuleFindNoteUrl() {\n        return this.ruleFindNoteUrl;\n    }\n\n    public void setRuleFindNoteUrl(String ruleFindNoteUrl) {\n        this.ruleFindNoteUrl = ruleFindNoteUrl;\n    }\n\n    public String getRuleBookInfoInit() {\n        return this.ruleBookInfoInit;\n    }\n\n    public void setRuleBookInfoInit(String ruleBookInfoInit) {\n        this.ruleBookInfoInit = ruleBookInfoInit;\n    }\n\n    public String getRuleBookContentReplace() {\n        return ruleBookContentReplace;\n    }\n\n    public void setRuleBookContentReplace(String ruleBookContentReplace) {\n        this.ruleBookContentReplace = ruleBookContentReplace;\n    }\n\n    public String getJson(int maxLength) {\n        try {\n            String source = getMinJson(false);\n            // 优先直接输出\n            if (source.getBytes(\"utf-8\").length <= maxLength)\n                return source;\n            source = StringUtils.zipString(source\n                    .replaceFirst(\"^\\\\{\", \"\")\n                    // 保留末位的括号，用于解压缩后的验证\n                    .replaceFirst(\"(\\\\s|\\n)*\\\\}$\", \"}\")\n                    .trim());\n            // 优先输出带发现的书源\n            if (source.getBytes(\"utf-8\").length < maxLength)\n                return \"{\" + source;\n            // 去除发现\n            return \"{\" + StringUtils.zipString(getMinJson(true)\n                    .replaceFirst(\"^\\\\{\", \"\")\n                    // 保留末位的括号，用于解压缩后的验证\n                    .replaceFirst(\"(\\\\s|\\n)*\\\\}$\", \"}\")\n                    .trim());\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return \"\";\n    }\n\n    /**\n     * 把书源bean转为json，并去除不必要的信息。\n     *\n     * @param removeFind false，去除空信息和排序、可用性信息。true，额外去除发现规则\n     * @return\n     */\n    public String getMinJson(Boolean removeFind) {\n        BookSourceBean bean = new BookSourceBean(this);\n        String json;\n\n        if (removeFind) {\n            bean.setRuleFindUrl(null);\n            bean.setRuleFindList(null);\n            bean.setRuleFindName(null);\n            bean.setRuleFindAuthor(null);\n            bean.setRuleFindKind(null);\n            bean.setRuleFindIntroduce(null);\n            bean.setRuleFindLastChapter(null);\n            bean.setRuleFindCoverUrl(null);\n            bean.setRuleFindNoteUrl(null);\n        }\n\n        try {\n            Gson gson = new Gson();\n            json = gson.toJson(bean);\n\n            return json.replaceFirst(\"\\n\\\\s*\\\"enable\\\":\\\\s*\\\\S+(,)?\\\\s*\", \"\\n\")\n                    .replaceFirst(\"\\n\\\\s*\\\"serialNumber\\\":\\\\s*\\\\d+(,)?\\\\s*\", \"\\n\")\n                    .replaceFirst(\"\\n\\\\s*\\\"\\\"weight\\\":\\\\s*\\\\d+(,)?\\\\s*\", \"\\n\")\n                    .replaceAll(\"\\n\\\\s*\\\"[a-zA-Z]+\\\"(:\\\"\\\"|: \\\"\\\"| :\\\"\\\"| : \\\"\\\")\\\\s*,\\\\s*\\n\", \"\\n\")\n                    .replaceAll(\"\\\\s*\\n\\\\s*\", \"\")\n                    ;\n        } catch (Exception ignored) {\n        }\n        return \"\";\n    }\n\n    public String getLoginUi() {\n        return loginUi;\n    }\n\n    public void setLoginUi(String loginUi) {\n        this.loginUi = loginUi;\n    }\n\n    public String getLoginCheckJs() {\n        return loginCheckJs;\n    }\n\n    public void setLoginCheckJs(String loginCheckJs) {\n        this.loginCheckJs = loginCheckJs;\n    }\n\n\n    /**\n     * 执行JS\n     */\n    public Object evalJS(String jsStr) throws Exception {\n        try {\n            SimpleBindings bindings = new SimpleBindings();\n            bindings.put(\"java\", this);\n            bindings.put(\"source\", this);\n            bindings.put(\"baseUrl\", bookSourceUrl);\n            return SCRIPT_ENGINE.eval(jsStr, bindings);\n        } catch (Exception e) {\n            e.printStackTrace();\n            return e.getLocalizedMessage();\n        }\n    }\n\n    public Map<String, String> getHeaderMap(Boolean hasLoginHeader) {\n        Map<String, String> headerMap = new HashMap<>();\n        String headers = getHttpUserAgent();\n        if (!isEmpty(headers)) {\n            if (StringUtils.isJsonObject(headers)) {\n                Map<String, String> map = new Gson().fromJson(headers, MAP_STRING);\n                headerMap.putAll(map);\n            } else {\n                headerMap.put(\"User-Agent\", headers);\n            }\n        } else {\n            headerMap.put(\"User-Agent\", AnalyzeHeaders.getDefaultUserAgent());\n        }\n        CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load(bookSourceUrl);\n        if (cookie != null) {\n            headerMap.put(\"Cookie\", cookie.getCookie());\n        }\n        if (hasLoginHeader) {\n            headerMap.putAll(getLoginHeaderMap());\n        }\n        return headerMap;\n    }\n\n    /**\n     * @return 登录头, Map格式\n     */\n    public Map<String, String> getLoginHeaderMap() {\n        Map<String, String> headerMap = new HashMap<>();\n        String header = getLoginHeader();\n        if (header != null) {\n            Map<String, String> map = new Gson().fromJson(header, MAP_STRING);\n            if (map != null) {\n                headerMap.putAll(map);\n            }\n        }\n        return headerMap;\n    }\n\n    public String getLoginHeader() {\n        CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load(\"loginHeader_\" + bookSourceUrl);\n        if (cookie == null) {\n            return null;\n        }\n        return cookie.getCookie();\n    }\n\n    public void putLoginHeader(String value) {\n        CookieBean cookie = new CookieBean(\"loginHeader_\" + bookSourceUrl, value);\n        DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(cookie);\n    }\n\n    public String getLoginInfo() {\n        CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load(\"loginInfo_\" + bookSourceUrl);\n        if (cookie == null) {\n            return null;\n        }\n        return cookie.getCookie();\n    }\n\n    /**\n     * @return 用户登录信息\n     */\n    public Map<String, String> getLoginInfoMap() {\n        String info = getLoginInfo();\n        if (info != null) {\n            return new Gson().fromJson(info, MAP_STRING);\n        }\n        return null;\n    }\n\n    public void putLoginInfo(Map<String, String> info) {\n        String json = new Gson().toJson(info);\n        CookieBean cookieBean = new CookieBean(\"loginInfo_\" + bookSourceUrl, json);\n        DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(cookieBean);\n    }\n\n    public Pair<FindKindGroupBean, List<FindKindBean>> getFindList() {\n        String findError = \"发现规则语法错误\";\n        ACache aCache = ACache.get(MApplication.getInstance(), \"findCache\");\n        try {\n            String[] kindA;\n            String findRule;\n            if (!TextUtils.isEmpty(getRuleFindUrl()) && !containsGroup(findError)) {\n                boolean isJsAndCache = getRuleFindUrl().startsWith(\"<js>\") || getRuleFindUrl().startsWith(\"@js:\");\n                if (isJsAndCache) {\n                    findRule = aCache.getAsString(getBookSourceUrl());\n                    if (TextUtils.isEmpty(findRule)) {\n                        String jsStr;\n                        if (getRuleFindUrl().startsWith(\"<js>\")) {\n                            jsStr = getRuleFindUrl().substring(4, getRuleFindUrl().lastIndexOf(\"<\"));\n                        } else {\n                            jsStr = getRuleFindUrl().substring(4);\n                        }\n                        findRule = evalJS(jsStr).toString();\n                    } else {\n                        isJsAndCache = false;\n                    }\n                } else {\n                    findRule = getRuleFindUrl();\n                }\n                kindA = findRule.split(\"(&&|\\n)+\");\n                List<FindKindBean> children = new ArrayList<>();\n                for (String kindB : kindA) {\n                    if (kindB.trim().isEmpty()) continue;\n                    String[] kind = kindB.split(\"::\");\n                    FindKindBean findKindBean = new FindKindBean();\n                    findKindBean.setGroup(getBookSourceName());\n                    findKindBean.setTag(getBookSourceUrl());\n                    findKindBean.setKindName(kind[0]);\n                    findKindBean.setKindUrl(kind[1]);\n                    children.add(findKindBean);\n                }\n                if (isJsAndCache) {\n                    aCache.put(getBookSourceUrl(), findRule);\n                }\n                FindKindGroupBean groupBean = new FindKindGroupBean();\n                groupBean.setGroupName(getBookSourceName());\n                groupBean.setGroupTag(getBookSourceUrl());\n                return new Pair<>(groupBean, children);\n            }\n        } catch (Exception exception) {\n            exception.printStackTrace();\n            addGroup(findError);\n            BookSourceManager.addBookSource(this);\n        }\n        return null;\n    }\n\n    public String getPayAction() {\n        return this.payAction;\n    }\n\n    public void setPayAction(String payAction) {\n        this.payAction = payAction;\n    }\n\n    public String getRuleChapterVip() {\n        return this.ruleChapterVip;\n    }\n\n    public void setRuleChapterVip(String ruleChapterVip) {\n        this.ruleChapterVip = ruleChapterVip;\n    }\n\n    public String getRuleChapterPay() {\n        return this.ruleChapterPay;\n    }\n\n    public void setRuleChapterPay(String ruleChapterPay) {\n        this.ruleChapterPay = ruleChapterPay;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/BookmarkBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\n\n@Entity\npublic class BookmarkBean implements Cloneable {\n\n    @Id\n    private Long id = System.currentTimeMillis();\n    private String noteUrl;\n    private String bookName;\n    private String chapterName;\n    private Integer chapterIndex;\n    private Integer pageIndex;\n    private String content;\n\n\n    @Generated(hash = 1176037419)\n    public BookmarkBean(Long id, String noteUrl, String bookName, String chapterName,\n                        Integer chapterIndex, Integer pageIndex, String content) {\n        this.id = id;\n        this.noteUrl = noteUrl;\n        this.bookName = bookName;\n        this.chapterName = chapterName;\n        this.chapterIndex = chapterIndex;\n        this.pageIndex = pageIndex;\n        this.content = content;\n    }\n\n    @Generated(hash = 1612540172)\n    public BookmarkBean() {\n    }\n\n    @Override\n    protected Object clone() throws CloneNotSupportedException {\n        BookmarkBean bookmarkBean = (BookmarkBean) super.clone();\n        bookmarkBean.id = id;\n        bookmarkBean.noteUrl = noteUrl;\n        bookmarkBean.bookName = bookName;\n        bookmarkBean.chapterIndex = chapterIndex;\n        bookmarkBean.chapterName = chapterName;\n        bookmarkBean.pageIndex = pageIndex;\n        bookmarkBean.content = content;\n\n        return bookmarkBean;\n    }\n\n    public String getNoteUrl() {\n        return this.noteUrl;\n    }\n\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    public String getChapterName() {\n        return this.chapterName;\n    }\n\n    public void setChapterName(String chapterName) {\n        this.chapterName = chapterName;\n    }\n\n    public Integer getChapterIndex() {\n        return this.chapterIndex;\n    }\n\n    public void setChapterIndex(Integer chapterIndex) {\n        this.chapterIndex = chapterIndex;\n    }\n\n    public String getContent() {\n        return this.content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public String getBookName() {\n        return this.bookName;\n    }\n\n    public void setBookName(String bookName) {\n        this.bookName = bookName;\n    }\n\n    public Long getId() {\n        return this.id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public Integer getPageIndex() {\n        return this.pageIndex;\n    }\n\n    public void setPageIndex(Integer pageIndex) {\n        this.pageIndex = pageIndex;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/CookieBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\n\n@Entity\npublic class CookieBean implements Parcelable {\n\n    @Id\n    private String url;\n    private String cookie;\n\n    private CookieBean(Parcel in) {\n        url = in.readString();\n        cookie = in.readString();\n    }\n\n    @Generated(hash = 517179762)\n    public CookieBean(String url, String cookie) {\n        this.url = url;\n        this.cookie = cookie;\n    }\n\n    @Generated(hash = 769081142)\n    public CookieBean() {\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeString(url);\n        dest.writeString(cookie);\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    public String getUrl() {\n        return this.url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String getCookie() {\n        return cookie == null ? \"\" : cookie;\n    }\n\n    public void setCookie(String cookie) {\n        this.cookie = cookie;\n    }\n\n    public static final Creator<CookieBean> CREATOR = new Creator<CookieBean>() {\n        @Override\n        public CookieBean createFromParcel(Parcel in) {\n            return new CookieBean(in);\n        }\n\n        @Override\n        public CookieBean[] newArray(int size) {\n            return new CookieBean[size];\n        }\n    };\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/DownloadBookBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.text.TextUtils;\n\nimport io.reactivex.annotations.Nullable;\n\npublic class DownloadBookBean implements Parcelable, Comparable<DownloadBookBean> {\n    private String name; //小说名\n    private String noteUrl;\n    private String coverUrl;\n    private int downloadCount;\n    private int start;\n    private int end;\n    private int successCount;\n    private boolean isValid;\n    private long finalDate;\n\n    public DownloadBookBean() {\n    }\n\n    protected DownloadBookBean(Parcel in) {\n        name = in.readString();\n        noteUrl = in.readString();\n        coverUrl = in.readString();\n        downloadCount = in.readInt();\n        start = in.readInt();\n        end = in.readInt();\n        successCount = in.readInt();\n        isValid = in.readByte() != 0;\n        finalDate = in.readLong();\n    }\n\n    @Override\n    public void writeToParcel(Parcel dest, int flags) {\n        dest.writeString(name);\n        dest.writeString(noteUrl);\n        dest.writeString(coverUrl);\n        dest.writeInt(downloadCount);\n        dest.writeInt(start);\n        dest.writeInt(end);\n        dest.writeInt(successCount);\n        dest.writeByte((byte) (isValid ? 1 : 0));\n        dest.writeLong(finalDate);\n    }\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    public static final Parcelable.Creator<DownloadBookBean> CREATOR = new Parcelable.Creator<DownloadBookBean>() {\n        @Override\n        public DownloadBookBean createFromParcel(Parcel in) {\n            return new DownloadBookBean(in);\n        }\n\n        @Override\n        public DownloadBookBean[] newArray(int size) {\n            return new DownloadBookBean[size];\n        }\n    };\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getNoteUrl() {\n        return noteUrl;\n    }\n\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    public String getCoverUrl() {\n        return coverUrl;\n    }\n\n    public void setCoverUrl(String coverUrl) {\n        this.coverUrl = coverUrl;\n    }\n\n    public int getDownloadCount() {\n        return downloadCount;\n    }\n\n    public void setDownloadCount(int downloadCount) {\n        this.downloadCount = downloadCount;\n        setValid(downloadCount > 0);\n    }\n\n    public int getStart() {\n        return start;\n    }\n\n    public void setStart(int start) {\n        this.start = start;\n    }\n\n    public int getEnd() {\n        return end;\n    }\n\n    public void setEnd(int end) {\n        this.end = end;\n    }\n\n    public int getSuccessCount() {\n        return successCount;\n    }\n\n    public int getWaitingCount() {\n        return this.downloadCount - this.successCount;\n    }\n\n    public synchronized void successCountAdd() {\n        if (this.successCount < this.downloadCount) {\n            this.successCount += 1;\n        }\n    }\n\n    public boolean isValid() {\n        return isValid;\n    }\n\n    public void setValid(boolean valid) {\n        isValid = valid;\n    }\n\n    public long getFinalDate() {\n        return finalDate;\n    }\n\n    public void setFinalDate(long finalDate) {\n        this.finalDate = finalDate;\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n        if (obj instanceof DownloadBookBean) {\n            return TextUtils.equals(((DownloadBookBean) obj).getNoteUrl(), this.noteUrl);\n        }\n        return super.equals(obj);\n    }\n\n    @Override\n    public int compareTo(DownloadBookBean o) {\n        return (int) (this.finalDate - o.finalDate);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/DownloadChapterBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\npublic class DownloadChapterBean implements BaseChapterBean {\n    private String noteUrl;\n\n    private int durChapterIndex;  //当前章节数\n\n    private String durChapterUrl;  //当前章节对应的文章地址\n\n    private String durChapterName;  //当前章节名称\n\n    private String tag;\n\n    private String bookName;\n\n    public DownloadChapterBean() {\n    }\n\n    @Override\n    public String getNoteUrl() {\n        return noteUrl;\n    }\n\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    @Override\n    public int getDurChapterIndex() {\n        return durChapterIndex;\n    }\n\n    public void setDurChapterIndex(int durChapterIndex) {\n        this.durChapterIndex = durChapterIndex;\n    }\n\n    @Override\n    public String getDurChapterUrl() {\n        return durChapterUrl;\n    }\n\n    public void setDurChapterUrl(String durChapterUrl) {\n        this.durChapterUrl = durChapterUrl;\n    }\n\n    @Override\n    public String getDurChapterName() {\n        return durChapterName;\n    }\n\n    public void setDurChapterName(String durChapterName) {\n        this.durChapterName = durChapterName;\n    }\n\n    @Override\n    public String getTag() {\n        return tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    public String getBookName() {\n        return bookName;\n    }\n\n    public void setBookName(String bookName) {\n        this.bookName = bookName;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/FindKindBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\npublic class FindKindBean {\n    private String group;\n    private String tag;\n    private String kindName;\n    private String kindUrl;\n\n    public FindKindBean() {\n\n    }\n\n    public String getKindName() {\n        return kindName;\n    }\n\n    public void setKindName(String kindName) {\n        this.kindName = kindName;\n    }\n\n    public String getKindUrl() {\n        return kindUrl;\n    }\n\n    public void setKindUrl(String kindUrl) {\n        this.kindUrl = kindUrl;\n    }\n\n    public String getTag() {\n        return tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    public String getGroup() {\n        return group;\n    }\n\n    public void setGroup(String group) {\n        this.group = group;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/FindKindGroupBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\npublic class FindKindGroupBean {\n    private String groupName;\n    private String groupTag;\n\n    public String getGroupName() {\n        return groupName;\n    }\n\n    public void setGroupName(String groupName) {\n        this.groupName = groupName;\n    }\n\n    public String getGroupTag() {\n        return groupTag;\n    }\n\n    public void setGroupTag(String groupTag) {\n        this.groupTag = groupTag;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/LocBookShelfBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\npublic class LocBookShelfBean {\n    private Boolean isNew;\n    private BookShelfBean bookShelfBean;\n\n    public LocBookShelfBean(Boolean isNew, BookShelfBean bookShelfBean) {\n        this.isNew = isNew;\n        this.bookShelfBean = bookShelfBean;\n    }\n\n    public Boolean getNew() {\n        return isNew;\n    }\n\n    public void setNew(Boolean aNew) {\n        isNew = aNew;\n    }\n\n    public BookShelfBean getBookShelfBean() {\n        return bookShelfBean;\n    }\n\n    public void setBookShelfBean(BookShelfBean bookShelfBean) {\n        this.bookShelfBean = bookShelfBean;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/OpenChapterBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\npublic class OpenChapterBean {\n    private int chapterIndex;\n    private int pageIndex;\n\n    public OpenChapterBean(int chapterIndex, int pageIndex) {\n        this.chapterIndex = chapterIndex;\n        this.pageIndex = pageIndex;\n    }\n\n    public int getChapterIndex() {\n        return chapterIndex;\n    }\n\n    public int getPageIndex() {\n        return pageIndex;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/ReplaceRuleBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\nimport org.greenrobot.greendao.annotation.OrderBy;\nimport org.greenrobot.greendao.annotation.Transient;\n\nimport java.util.regex.Pattern;\n\n/**\n * Created by GKF on 2018/2/7.\n * 阅读内容替换规则\n */\n@Entity\npublic class ReplaceRuleBean implements Parcelable {\n    @Id(autoincrement = true)\n    private Long id;\n    //描述\n    private String replaceSummary;\n    //替换规则\n    private String regex;\n    //替换为\n    private String replacement;\n    //作用于\n    private String useTo;\n\n    private Boolean enable;\n\n    private Boolean isRegex;\n\n    @OrderBy\n    private int serialNumber;\n\n    private ReplaceRuleBean(Parcel in) {\n        id = in.readLong();\n        regex = in.readString();\n        replacement = in.readString();\n        replaceSummary = in.readString();\n        useTo = in.readString();\n        enable = in.readByte() != 0;\n        serialNumber = in.readInt();\n        isRegex = in.readByte() != 0;\n    }\n\n    @Generated(hash = 1896663649)\n    public ReplaceRuleBean(Long id, String replaceSummary, String regex, String replacement,\n                           String useTo, Boolean enable, Boolean isRegex, int serialNumber) {\n        this.id = id;\n        this.replaceSummary = replaceSummary;\n        this.regex = regex;\n        this.replacement = replacement;\n        this.useTo = useTo;\n        this.enable = enable;\n        this.isRegex = isRegex;\n        this.serialNumber = serialNumber;\n    }\n\n    @Generated(hash = 582692869)\n    public ReplaceRuleBean() {\n    }\n\n    @Override\n    public void writeToParcel(Parcel parcel, int i) {\n        parcel.writeLong(id);\n        parcel.writeString(regex);\n        parcel.writeString(replacement);\n        parcel.writeString(replaceSummary);\n        parcel.writeString(useTo);\n        parcel.writeByte((byte) (enable ? 1 : 0));\n        parcel.writeInt(serialNumber);\n        parcel.writeByte((byte) (isRegex ? 1 : 0));\n    }\n\n    @Transient\n    public static final Creator<ReplaceRuleBean> CREATOR = new Creator<ReplaceRuleBean>() {\n        @Override\n        public ReplaceRuleBean createFromParcel(Parcel in) {\n            return new ReplaceRuleBean(in);\n        }\n\n        @Override\n        public ReplaceRuleBean[] newArray(int size) {\n            return new ReplaceRuleBean[size];\n        }\n    };\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    public String getReplaceSummary() {\n        return this.replaceSummary;\n    }\n\n    public void setReplaceSummary(String replaceSummary) {\n        this.replaceSummary = replaceSummary;\n    }\n\n    public String getRegex() {\n        return this.regex;\n    }\n\n    public String getFixedRegex() {\n        if (getIsRegex())\n            return this.regex;\n        else\n            return Pattern.quote(regex);\n    }\n\n    public void setRegex(String regex) {\n        this.regex = regex;\n    }\n\n    public String getReplacement() {\n        return this.replacement;\n    }\n\n    public void setReplacement(String replacement) {\n        this.replacement = replacement;\n    }\n\n    public Boolean getEnable() {\n        if (enable == null) {\n            return false;\n        }\n        return this.enable;\n    }\n\n    public void setEnable(Boolean enable) {\n        this.enable = enable;\n    }\n\n    public int getSerialNumber() {\n        return this.serialNumber;\n    }\n\n    public void setSerialNumber(int serialNumber) {\n        this.serialNumber = serialNumber;\n    }\n\n    public Long getId() {\n        return this.id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public String getUseTo() {\n        return this.useTo;\n    }\n\n    public void setUseTo(String useTo) {\n        this.useTo = useTo;\n    }\n\n    public Boolean getIsRegex() {\n        return isRegex == null ? true : isRegex;\n    }\n\n    public void setIsRegex(Boolean isRegex) {\n        this.isRegex = isRegex;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/SearchBookBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.BookSourceManager;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\nimport org.greenrobot.greendao.annotation.Transient;\n\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\nimport java.util.Map;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING;\n\n@Entity\npublic class SearchBookBean implements BaseBookBean {\n    @Id\n    private String noteUrl;\n    private String coverUrl;//封面URL\n    private String name;\n    private String author;\n    private String tag;\n    private String kind;//分类\n    private String origin;//来源\n    private String lastChapter;\n    private String introduce; //简介\n    private String chapterUrl;//目录URL\n    private Long addTime = 0L;\n    private Long upTime = 0L;\n    private String variable;\n\n    @Transient\n    private Boolean isCurrentSource = false;\n    @Transient\n    private int originNum = 1;\n    @Transient\n    private int lastChapterNum = -2;\n    @Transient\n    private int searchTime = Integer.MAX_VALUE;\n    @Transient\n    private LinkedHashSet<String> originUrls;\n    @Transient\n    private Map<String, String> variableMap;\n    @Transient\n    private String bookInfoHtml;\n\n    public SearchBookBean() {\n\n    }\n\n    public SearchBookBean(String tag, String origin) {\n        this.tag = tag;\n        this.origin = origin;\n    }\n\n    @Generated(hash = 337890066)\n    public SearchBookBean(String noteUrl, String coverUrl, String name, String author, String tag, String kind,\n                          String origin, String lastChapter, String introduce, String chapterUrl, Long addTime, Long upTime,\n                          String variable) {\n        this.noteUrl = noteUrl;\n        this.coverUrl = coverUrl;\n        this.name = name;\n        this.author = author;\n        this.tag = tag;\n        this.kind = kind;\n        this.origin = origin;\n        this.lastChapter = lastChapter;\n        this.introduce = introduce;\n        this.chapterUrl = chapterUrl;\n        this.addTime = addTime;\n        this.upTime = upTime;\n        this.variable = variable;\n    }\n\n    @Override\n    public String getVariable() {\n        return this.variable;\n    }\n\n    @Override\n    public void setVariable(String variable) {\n        this.variable = variable;\n    }\n\n    @Override\n    public void putVariable(String key, String value) {\n        if (variableMap == null) {\n            variableMap = new HashMap<>();\n        }\n        variableMap.put(key, value);\n        variable = new Gson().toJson(variableMap);\n    }\n\n    @Override\n    public Map<String, String> getVariableMap() {\n        if (variableMap == null) {\n            return new Gson().fromJson(variable, MAP_STRING);\n        }\n        return variableMap;\n    }\n\n    @Override\n    public String getNoteUrl() {\n        return noteUrl;\n    }\n\n    @Override\n    public void setNoteUrl(String noteUrl) {\n        this.noteUrl = noteUrl;\n    }\n\n    public String getCoverUrl() {\n        return coverUrl;\n    }\n\n    public void setCoverUrl(String coverUrl) {\n        this.coverUrl = coverUrl;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name != null ? name.trim().replaceAll(\"　\", \"\") : null;\n    }\n\n    public String getAuthor() {\n        return author;\n    }\n\n    public void setAuthor(String author) {\n        this.author = BookshelfHelp.formatAuthor(author);\n    }\n\n    public String getLastChapter() {\n        return lastChapter == null ? \"\" : lastChapter;\n    }\n\n    public void setLastChapter(String lastChapter) {\n        this.lastChapter = lastChapter;\n\n    }\n\n    public int getLastChapterNum() {\n        if (lastChapterNum == -2)\n            this.lastChapterNum = BookshelfHelp.guessChapterNum(lastChapter);\n        return lastChapterNum;\n    }\n\n    public String getKind() {\n        return kind;\n    }\n\n    public void setKind(String kind) {\n        this.kind = kind;\n    }\n\n    @Override\n    public String getTag() {\n        return tag;\n    }\n\n    public void setTag(String tag) {\n        this.tag = tag;\n    }\n\n    public String getOrigin() {\n        return origin;\n    }\n\n    public void setOrigin(String origin) {\n        this.origin = origin;\n    }\n\n    public Boolean getIsCurrentSource() {\n        return this.isCurrentSource;\n    }\n\n    public void setIsCurrentSource(Boolean isCurrentSource) {\n        this.isCurrentSource = isCurrentSource;\n        if (isCurrentSource)\n            this.addTime = System.currentTimeMillis();\n    }\n\n    public int getOriginNum() {\n        return originNum;\n    }\n\n    public void addOriginUrl(String origin) {\n        if (this.originUrls == null) {\n            this.originUrls = new LinkedHashSet<>();\n        }\n        this.originUrls.add(origin);\n        originNum = this.originUrls.size();\n    }\n\n    public String getIntroduce() {\n        return introduce;\n    }\n\n    public void setIntroduce(String introduce) {\n        this.introduce = introduce;\n    }\n\n    public String getChapterUrl() {\n        return this.chapterUrl;\n    }\n\n    public void setChapterUrl(String chapterUrl) {\n        this.chapterUrl = chapterUrl;\n    }\n\n    public long getAddTime() {\n        return this.addTime;\n    }\n\n    public void setAddTime(Long addTime) {\n        this.addTime = addTime;\n    }\n\n    public int getWeight() {\n        BookSourceBean source = BookSourceManager.getBookSourceByUrl(this.tag);\n        if (source != null)\n            return source.getWeight();\n        else\n            return 0;\n    }\n\n    public int getSearchTime() {\n        return searchTime;\n    }\n\n    public void setSearchTime(int searchTime) {\n        this.searchTime = searchTime;\n    }\n\n    public Long getUpTime() {\n        return this.upTime;\n    }\n\n    public void setUpTime(Long upTime) {\n        this.upTime = upTime;\n    }\n\n    public String getBookInfoHtml() {\n        return bookInfoHtml;\n    }\n\n    public void setBookInfoHtml(String bookInfoHtml) {\n        this.bookInfoHtml = bookInfoHtml;\n    }\n\n    // 一次性存入搜索书籍节点信息\n    public void setSearchInfo(String name, String author, String kind, String lastChapter,\n                              String introduce, String coverUrl, String noteUrl) {\n        this.name = name;\n        this.author = author;\n        this.kind = kind;\n        this.lastChapter = lastChapter;\n        this.introduce = introduce;\n        this.coverUrl = coverUrl;\n        this.noteUrl = noteUrl;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/SearchHistoryBean.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.bean;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\n\n@Entity\npublic class SearchHistoryBean {\n    @Id(autoincrement = true)\n    private Long id = null;\n    private int type;\n    private String content;\n    private long date;\n\n    public long getDate() {\n        return this.date;\n    }\n\n    public void setDate(long date) {\n        this.date = date;\n    }\n\n    public String getContent() {\n        return this.content;\n    }\n\n    public void setContent(String content) {\n        this.content = content;\n    }\n\n    public int getType() {\n        return this.type;\n    }\n\n    public void setType(int type) {\n        this.type = type;\n    }\n\n    public Long getId() {\n        return this.id;\n    }\n\n    public void setId(Long id) {\n        this.id = id;\n    }\n\n    public SearchHistoryBean(int type, String content, long date) {\n        this.type = type;\n        this.content = content;\n        this.date = date;\n    }\n\n    @Generated(hash = 488115752)\n    public SearchHistoryBean(Long id, int type, String content, long date) {\n        this.id = id;\n        this.type = type;\n        this.content = content;\n        this.date = date;\n    }\n\n    @Generated(hash = 1570282321)\n    public SearchHistoryBean() {\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/TwoDataBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\npublic class TwoDataBean<T, S> {\n    private T data1;\n    private S data2;\n\n    public TwoDataBean(T data1, S data2) {\n        this.data1 = data1;\n        this.data2 = data2;\n    }\n\n    public T getData1() {\n        return data1;\n    }\n\n    public S getData2() {\n        return data2;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/TxtChapterRuleBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport androidx.annotation.Nullable;\n\nimport org.greenrobot.greendao.annotation.Entity;\nimport org.greenrobot.greendao.annotation.Generated;\nimport org.greenrobot.greendao.annotation.Id;\nimport org.greenrobot.greendao.annotation.OrderBy;\n\nimport java.util.Objects;\n\n@Entity\npublic class TxtChapterRuleBean {\n\n    @Id\n    private String name;\n    private String rule;\n    @OrderBy\n    private Integer serialNumber;\n    private Boolean enable;\n\n    @Generated(hash = 2018686288)\n    public TxtChapterRuleBean(String name, String rule, Integer serialNumber,\n                              Boolean enable) {\n        this.name = name;\n        this.rule = rule;\n        this.serialNumber = serialNumber;\n        this.enable = enable;\n    }\n\n    @Generated(hash = 382733400)\n    public TxtChapterRuleBean() {\n    }\n\n    @Override\n    public boolean equals(@Nullable Object obj) {\n        if (obj instanceof TxtChapterRuleBean) {\n            return Objects.equals(this.name, ((TxtChapterRuleBean) obj).name);\n        }\n        return false;\n    }\n\n    public TxtChapterRuleBean copy() {\n        TxtChapterRuleBean ruleBean = new TxtChapterRuleBean();\n        ruleBean.setName(name);\n        ruleBean.setRule(rule);\n        ruleBean.setEnable(enable);\n        ruleBean.setSerialNumber(serialNumber);\n        return ruleBean;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getRule() {\n        return rule;\n    }\n\n    public void setRule(String rule) {\n        this.rule = rule;\n    }\n\n    public Integer getSerialNumber() {\n        return serialNumber;\n    }\n\n    public void setSerialNumber(Integer serialNumber) {\n        this.serialNumber = serialNumber;\n    }\n\n    public Boolean getEnable() {\n        return enable == null ? true : enable;\n    }\n\n    public void setEnable(Boolean enable) {\n        this.enable = enable;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/UpdateInfoBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport android.os.Parcel;\nimport android.os.Parcelable;\n\npublic class UpdateInfoBean implements Parcelable {\n    private String lastVersion;\n    private String url;\n    private String detail;\n    private Boolean upDate;\n\n    public UpdateInfoBean() {\n\n    }\n\n    protected UpdateInfoBean(Parcel in) {\n        lastVersion = in.readString();\n        url = in.readString();\n        detail = in.readString();\n        upDate = in.readByte() != 0;\n    }\n\n    public static final Creator<UpdateInfoBean> CREATOR = new Creator<UpdateInfoBean>() {\n        @Override\n        public UpdateInfoBean createFromParcel(Parcel in) {\n            return new UpdateInfoBean(in);\n        }\n\n        @Override\n        public UpdateInfoBean[] newArray(int size) {\n            return new UpdateInfoBean[size];\n        }\n    };\n\n    @Override\n    public int describeContents() {\n        return 0;\n    }\n\n    @Override\n    public void writeToParcel(Parcel parcel, int i) {\n        parcel.writeString(lastVersion);\n        parcel.writeString(url);\n        parcel.writeString(detail);\n        parcel.writeByte((byte) (upDate ? 1 : 0));\n    }\n\n    public String getLastVersion() {\n        return lastVersion;\n    }\n\n    public void setLastVersion(String lastVersion) {\n        this.lastVersion = lastVersion;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public String getDetail() {\n        return detail;\n    }\n\n    public void setDetail(String detail) {\n        this.detail = detail;\n    }\n\n    public Boolean getUpDate() {\n        return upDate;\n    }\n\n    public void setUpDate(Boolean upDate) {\n        this.upDate = upDate;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/bean/WebChapterBean.java",
    "content": "package com.kunfei.bookshelf.bean;\n\nimport java.util.LinkedHashSet;\nimport java.util.List;\n\npublic class WebChapterBean {\n    private String url;\n\n    private List<BookChapterBean> data;\n\n    private LinkedHashSet<String> nextUrlList;\n\n    public WebChapterBean(String url) {\n        this.url = url;\n    }\n\n    public WebChapterBean(List<BookChapterBean> data, LinkedHashSet<String> nextUrlList) {\n        this.data = data;\n        this.nextUrlList = nextUrlList;\n    }\n\n    public List<BookChapterBean> getData() {\n        return data;\n    }\n\n    public void setData(List<BookChapterBean> data) {\n        this.data = data;\n    }\n\n    public LinkedHashSet<String> getNextUrlList() {\n        return nextUrlList;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public boolean noData() {\n        return data == null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/constant/AppConst.kt",
    "content": "package com.kunfei.bookshelf.constant\n\nimport android.annotation.SuppressLint\nimport android.provider.Settings\nimport splitties.init.appCtx\nimport java.text.SimpleDateFormat\nimport javax.script.ScriptEngine\nimport javax.script.ScriptEngineManager\n\n@SuppressLint(\"SimpleDateFormat\")\nobject AppConst {\n\n    const val APP_TAG = \"Legado\"\n\n    val androidId: String by lazy {\n        Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID)\n    }\n\n    const val channelIdDownload = \"channel_download\"\n    const val channelIdReadAloud = \"channel_read_aloud\"\n    const val channelIdWeb = \"channel_web\"\n\n    const val UA_NAME = \"User-Agent\"\n\n    val SCRIPT_ENGINE: ScriptEngine by lazy {\n        ScriptEngineManager().getEngineByName(\"rhino\")\n    }\n\n    val timeFormat: SimpleDateFormat by lazy {\n        SimpleDateFormat(\"HH:mm\")\n    }\n\n    val dateFormat: SimpleDateFormat by lazy {\n        SimpleDateFormat(\"yyyy/MM/dd HH:mm\")\n    }\n\n    val fileNameFormat: SimpleDateFormat by lazy {\n        SimpleDateFormat(\"yy-MM-dd-HH-mm-ss\")\n    }\n\n    val keyboardToolChars: List<String> by lazy {\n        arrayListOf(\n            \"❓\", \"@css:\", \"<js></js>\", \"{{}}\", \"##\", \"&&\", \"%%\", \"||\", \"//\", \"\\\\\", \"$.\",\n            \"@\", \":\", \"class\", \"text\", \"href\", \"textNodes\", \"ownText\", \"all\", \"html\",\n            \"[\", \"]\", \"<\", \">\", \"#\", \"!\", \".\", \"+\", \"-\", \"*\", \"=\", \"{'webView': true}\"\n        )\n    }\n\n    const val bookGroupAllId = -1L\n    const val bookGroupLocalId = -2L\n    const val bookGroupAudioId = -3L\n    const val bookGroupNoneId = -4L\n\n    const val notificationIdRead = 1144771\n    const val notificationIdAudio = 1144772\n    const val notificationIdWeb = 1144773\n    const val notificationIdDownload = 1144774\n\n    val urlOption: String by lazy {\n        \"\"\"\n        ,{\n        'charset': '',\n        'method': 'POST',\n        'body': '',\n        'headers': {\n            'User-Agent': ''\n            }\n        }\n        \"\"\".trimIndent()\n    }\n\n    val menuViewNames = arrayOf(\n        \"com.android.internal.view.menu.ListMenuItemView\",\n        \"androidx.appcompat.view.menu.ListMenuItemView\"\n    )\n\n    val darkWebViewJs by lazy {\n        \"\"\"\n            document.body.style.backgroundColor = \"#222222\";\n            document.getElementsByTagName('body')[0].style.webkitTextFillColor = '#8a8a8a';\n        \"\"\".trimIndent()\n    }\n\n\n    val charsets =\n        arrayListOf(\"UTF-8\", \"GB2312\", \"GB18030\", \"GBK\", \"Unicode\", \"UTF-16\", \"UTF-16LE\", \"ASCII\")\n\n    data class AppInfo(\n        var versionCode: Long = 0L,\n        var versionName: String = \"\"\n    )\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/constant/AppConstant.java",
    "content": "package com.kunfei.bookshelf.constant;\n\nimport android.content.Context;\nimport android.provider.Settings;\n\nimport com.google.gson.reflect.TypeToken;\nimport com.kunfei.bookshelf.BuildConfig;\nimport com.kunfei.bookshelf.MApplication;\n\nimport java.io.File;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineManager;\n\nimport okhttp3.MediaType;\n\npublic class AppConstant {\n\n    public static final String ActionStartService = \"startService\";\n    public static final String ActionDoneService = \"doneService\";\n\n    public static final long TIME_OUT = BuildConfig.DEBUG ? 600 : 180;\n\n    //Book Date Convert Format\n    public static final String FORMAT_TIME = \"HH:mm\";\n    public static final String FORMAT_FILE_DATE = \"yyyy-MM-dd\";\n    //BookCachePath (因为getCachePath引用了Context，所以必须是静态变量，不能够是静态常量)\n    public static String BOOK_CACHE_PATH = MApplication.downloadPath + File.separator + \"book_cache\" + File.separator;\n\n    public static Type MAP_STRING = new TypeToken<Map<String, String>>() {\n    }.getType();\n\n    public static final String DEFAULT_WEB_DAV_URL = \"https://dav.jianguoyun.com/dav/\";\n\n    public static final String DEFAULT_USER_AGENT = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36\";\n\n    public static final Pattern JS_PATTERN = Pattern.compile(\"(<js>[\\\\w\\\\W]*?</js>|@js:[\\\\w\\\\W]*$)\", Pattern.CASE_INSENSITIVE);\n    public static final Pattern EXP_PATTERN = Pattern.compile(\"\\\\{\\\\{([\\\\w\\\\W]*?)\\\\}\\\\}\");\n\n    public static final ScriptEngine SCRIPT_ENGINE = new ScriptEngineManager().getEngineByName(\"rhino\");\n\n    public static final MediaType jsonMediaType = MediaType.parse(\"Content-Type, application/json\");\n\n    static public String androidId(Context context) {\n        return Settings.System.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/constant/BookType.java",
    "content": "package com.kunfei.bookshelf.constant;\n\npublic class BookType {\n    public final static String AUDIO = \"AUDIO\";\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/constant/RxBusTag.java",
    "content": "package com.kunfei.bookshelf.constant;\n\npublic class RxBusTag {\n    public final static String HAD_ADD_BOOK = \"add_book\";\n    public final static String HAD_REMOVE_BOOK = \"remove_book\";\n    public final static String REFRESH_BOOK_LIST = \"reFresh_book\";\n    public final static String UPDATE_GROUP = \"UPDATE_GROUP\";\n    public final static String UPDATE_BOOK_PROGRESS = \"update_book_progress\";\n    public final static String UPDATE_READ = \"update_read\";\n    public final static String CHAPTER_CHANGE = \"chapter_change\";\n    public final static String MEDIA_BUTTON = \"media_button\";\n    public final static String ALOUD_STATE = \"aloud_state\";\n    public final static String ALOUD_TIMER = \"aloud_timer\";\n    public final static String RECREATE = \"recreate\";\n    public final static String CHECK_SOURCE_STATE = \"checkSourceState\";\n    public final static String CHECK_SOURCE_FINISH = \"checkSourceFinish\";\n    public final static String IMMERSION_CHANGE = \"Immersion_Change\";\n    public final static String SEARCH_BOOK = \"search_book\";\n    public final static String UPDATE_APK_STATE = \"updateApkState\";\n    public final static String DOWNLOAD_ALL = \"downloadAll\";\n    public final static String UP_SEARCH_BOOK = \"upSearchBook\";\n    public final static String SKIP_TO_CHAPTER = \"skipToChapter\";\n    public final static String OPEN_BOOK_MARK = \"openBookMark\";\n    public final static String READ_ALOUD_NUMBER = \"readAloudNumber\";\n    public final static String READ_ALOUD_START = \"readAloudStart\";\n    public final static String AUTO_BACKUP = \"autoBackup\";\n    public final static String PRINT_DEBUG_LOG = \"printDebugLog\";\n    public final static String AUDIO_SIZE = \"audioSize\";\n    public final static String AUDIO_DUR = \"audioDur\";\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/constant/TimeConstants.java",
    "content": "package com.kunfei.bookshelf.constant;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\npublic class TimeConstants {\n    public static final int MSEC = 1;\n    public static final int SEC = 1000;\n    public static final int MIN = 60000;\n    public static final int HOUR = 3600000;\n    public static final int DAY = 86400000;\n\n    @IntDef({MSEC, SEC, MIN, HOUR, DAY})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Unit {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/AppFrontBackHelper.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.app.Activity;\nimport android.app.Application;\nimport android.os.Bundle;\n\n\n/**\n * 应用前后台状态监听帮助类，仅在Application中使用\n */\npublic class AppFrontBackHelper {\n    private OnAppStatusListener mOnAppStatusListener;\n\n    public static AppFrontBackHelper getInstance() {\n        return new AppFrontBackHelper();\n    }\n\n    /**\n     * 注册状态监听，仅在Application中使用\n     */\n    public void register(Application application, OnAppStatusListener listener) {\n        mOnAppStatusListener = listener;\n        application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks);\n    }\n\n    private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {\n        //打开的Activity数量统计\n        private int activityStartCount = 0;\n\n        @Override\n        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {\n\n        }\n\n        @Override\n        public void onActivityStarted(Activity activity) {\n            activityStartCount++;\n            //数值从0变到1说明是从后台切到前台\n            if (activityStartCount == 1) {\n                //从后台切到前台\n                if (mOnAppStatusListener != null) {\n                    mOnAppStatusListener.onFront();\n                }\n            }\n        }\n\n        @Override\n        public void onActivityResumed(Activity activity) {\n\n        }\n\n        @Override\n        public void onActivityPaused(Activity activity) {\n\n        }\n\n        @Override\n        public void onActivityStopped(Activity activity) {\n            activityStartCount--;\n            //数值从1到0说明是从前台切到后台\n            if (activityStartCount == 0) {\n                //从前台切到后台\n                if (mOnAppStatusListener != null) {\n                    mOnAppStatusListener.onBack();\n                }\n            }\n        }\n\n        @Override\n        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {\n\n        }\n\n        @Override\n        public void onActivityDestroyed(Activity activity) {\n\n        }\n    };\n\n    public interface OnAppStatusListener {\n        void onFront();\n\n        void onBack();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/BlurTransformation.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.os.Build;\nimport android.renderscript.Allocation;\nimport android.renderscript.Element;\nimport android.renderscript.RenderScript;\nimport android.renderscript.ScriptIntrinsicBlur;\n\nimport androidx.annotation.NonNull;\n\nimport com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;\nimport com.bumptech.glide.load.resource.bitmap.BitmapTransformation;\n\nimport java.security.MessageDigest;\n\npublic class BlurTransformation extends BitmapTransformation {\n    private RenderScript rs;\n    private int radius;\n\n    public BlurTransformation(Context context, int radius) {\n        super();\n        rs = RenderScript.create(context);\n        this.radius = radius;\n    }\n\n    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)\n    @Override\n    protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {\n        Bitmap blurredBitmap = toTransform.copy(Bitmap.Config.ARGB_8888, true);\n\n        // Allocate memory for Renderscript to work with\n        //分配用于渲染脚本的内存\n        Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED);\n        Allocation output = Allocation.createTyped(rs, input.getType());\n\n        // Load up an instance of the specific script that we want to use.\n        //加载我们想要使用的特定脚本的实例。\n        ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));\n        script.setInput(input);\n\n        // Set the blur radius\n        //设置模糊半径\n        script.setRadius(radius);\n\n        // Start the ScriptIntrinsicBlur\n        //启动 ScriptIntrinsicBlur,\n        script.forEach(output);\n\n        // Copy the output to the blurred bitmap\n        //将输出复制到模糊的位图\n        output.copyTo(blurredBitmap);\n\n        return blurredBitmap;\n    }\n\n    @Override\n    public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {\n        messageDigest.update(\"blur transformation\".getBytes());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/BookshelfHelp.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.annotation.SuppressLint;\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.bean.BaseChapterBean;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookmarkBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.constant.AppConstant;\nimport com.kunfei.bookshelf.dao.BookChapterBeanDao;\nimport com.kunfei.bookshelf.dao.BookInfoBeanDao;\nimport com.kunfei.bookshelf.dao.BookShelfBeanDao;\nimport com.kunfei.bookshelf.dao.BookmarkBeanDao;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport net.ricecode.similarity.JaroWinklerStrategy;\nimport net.ricecode.similarity.StringSimilarityService;\nimport net.ricecode.similarity.StringSimilarityServiceImpl;\n\nimport java.io.BufferedWriter;\nimport java.io.File;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.text.DecimalFormat;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * Created by GKF on 2018/1/18.\n * 添加删除Book\n */\n\npublic class BookshelfHelp {\n\n    private static final Pattern chapterNamePattern = Pattern.compile(\"^(.*?第([\\\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟０-９\\\\s]+)[章节篇回集])[、，。　：:.\\\\s]*\");\n\n    public static String getCachePathName(String bookName, String tag) {\n        return formatFolderName(bookName + \"-\" + tag);\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    public static String getCacheFileName(int chapterIndex, String chapterName) {\n        return String.format(\"%05d-%s\", chapterIndex, formatFolderName(chapterName));\n    }\n\n    public static boolean isChapterCached(String bookName, String tag, BaseChapterBean chapter, boolean isAudio) {\n        if (isAudio) {\n            BookContentBean contentBean = DbHelper.getDaoSession().getBookContentBeanDao().load(chapter.getDurChapterUrl());\n            if (contentBean == null) return false;\n            if (contentBean.outTime()) {\n                DbHelper.getDaoSession().getBookContentBeanDao().delete(contentBean);\n                return false;\n            }\n            return !TextUtils.isEmpty(contentBean.getDurChapterContent());\n        }\n        File file = new File(AppConstant.BOOK_CACHE_PATH + getCachePathName(bookName, tag)\n                + File.separator + getCacheFileName(chapter.getDurChapterIndex(), chapter.getDurChapterName()) + FileHelp.SUFFIX_NB);\n        return file.exists();\n    }\n\n    public static String getChapterCache(BookShelfBean bookShelfBean, BookChapterBean chapter) {\n        if (bookShelfBean.isAudio()) {\n            BookContentBean contentBean = DbHelper.getDaoSession().getBookContentBeanDao().load(chapter.getDurChapterUrl());\n            if (contentBean == null) return null;\n            if (contentBean.outTime()) {\n                DbHelper.getDaoSession().getBookContentBeanDao().delete(contentBean);\n                return null;\n            }\n            return contentBean.getDurChapterContent();\n        }\n        File file = new File(AppConstant.BOOK_CACHE_PATH\n                + formatFolderName(BookshelfHelp.getCachePathName(bookShelfBean.getBookInfoBean().getName(), bookShelfBean.getTag()))\n                + File.separator + getCacheFileName(chapter.getDurChapterIndex(), chapter.getDurChapterName()) + FileHelp.SUFFIX_NB);\n        if (!file.exists()) return null;\n\n        byte[] contentByte = DocumentHelper.getBytes(file);\n        return new String(contentByte, StandardCharsets.UTF_8);\n    }\n\n    public static void clearCaches(boolean clearChapterList) {\n        FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH);\n        FileHelp.getFolder(AppConstant.BOOK_CACHE_PATH);\n        if (clearChapterList)\n            DbHelper.getDaoSession().getBookChapterBeanDao().deleteAll();\n    }\n\n    /**\n     * 删除章节文件\n     */\n    public static void delChapter(String folderName, int index, String fileName) {\n        FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH + folderName\n                + File.separator + getCacheFileName(index, fileName) + FileHelp.SUFFIX_NB);\n    }\n\n    /**\n     * 存储章节\n     */\n    public static synchronized boolean saveChapterInfo(String folderName, int index, String fileName, String content) {\n        if (content == null) {\n            return false;\n        }\n        File file = getBookFile(folderName, index, fileName);\n        //获取流并存储\n        try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {\n            writer.write(fileName + \"\\n\\n\");\n            writer.write(content);\n            writer.write(\"\\n\\n\");\n            writer.flush();\n            return true;\n        } catch (IOException e) {\n            e.printStackTrace();\n            return false;\n        }\n    }\n\n    /**\n     * 创建或获取存储文件\n     */\n    public static File getBookFile(String folderName, int index, String fileName) {\n        return FileHelp.createFileIfNotExist(AppConstant.BOOK_CACHE_PATH + formatFolderName(folderName)\n                + File.separator + getCacheFileName(index, fileName) + FileHelp.SUFFIX_NB);\n    }\n\n    private static String formatFolderName(String folderName) {\n        return folderName.replaceAll(\"[\\\\\\\\/:*?\\\"<>|.]\", \"\");\n    }\n\n    /**\n     * 根据目录名获取当前章节\n     */\n    public static int getDurChapter(int oldDurChapterIndex, int oldChapterListSize, String oldDurChapterName, List<BookChapterBean> newChapterList) {\n        if (oldChapterListSize == 0)\n            return 0;\n        int oldChapterNum = getChapterNum(oldDurChapterName);\n        String oldName = getPureChapterName(oldDurChapterName);\n        int newChapterSize = newChapterList.size();\n        int min = Math.max(0, Math.min(oldDurChapterIndex, oldDurChapterIndex - oldChapterListSize + newChapterSize) - 10);\n        int max = Math.min(newChapterSize - 1, Math.max(oldDurChapterIndex, oldDurChapterIndex - oldChapterListSize + newChapterSize) + 10);\n        double nameSim = 0;\n        int newIndex = 0;\n        int newNum = 0;\n        if (!oldName.isEmpty()) {\n            StringSimilarityService service = new StringSimilarityServiceImpl(new JaroWinklerStrategy());\n            for (int i = min; i <= max; i++) {\n                String newName = getPureChapterName(newChapterList.get(i).getDurChapterName());\n                double temp = service.score(oldName, newName);\n                if (temp > nameSim) {\n                    nameSim = temp;\n                    newIndex = i;\n                }\n            }\n        }\n        if (nameSim < 0.96 && oldChapterNum > 0) {\n            for (int i = min; i <= max; i++) {\n                int temp = getChapterNum(newChapterList.get(i).getDurChapterName());\n                if (temp == oldChapterNum) {\n                    newNum = temp;\n                    newIndex = i;\n                    break;\n                } else if (Math.abs(temp - oldChapterNum) < Math.abs(newNum - oldChapterNum)) {\n                    newNum = temp;\n                    newIndex = i;\n                }\n            }\n        }\n        if (nameSim > 0.96 || Math.abs(newNum - oldChapterNum) < 1) {\n            return newIndex;\n        } else {\n            return Math.min(Math.max(0, newChapterList.size() - 1), oldDurChapterIndex);\n        }\n    }\n\n    private static int getChapterNum(String chapterName) {\n        if (chapterName != null) {\n            Matcher matcher = chapterNamePattern.matcher(chapterName);\n            if (matcher.find()) {\n                return StringUtils.stringToInt(matcher.group(2));\n            }\n        }\n        return -1;\n    }\n\n    private static String getPureChapterName(String chapterName) {\n        return chapterName == null ? \"\"\n                : StringUtils.fullToHalf(chapterName).replaceAll(\"\\\\s\", \"\")\n                .replaceAll(\"^第.*?章|[(\\\\[][^()\\\\[\\\\]]{2,}[)\\\\]]$\", \"\")\n                .replaceAll(\"[^\\\\w\\\\u4E00-\\\\u9FEF〇\\\\u3400-\\\\u4DBF\\\\u20000-\\\\u2A6DF\\\\u2A700-\\\\u2EBEF]\", \"\");\n        // 所有非字母数字中日韩文字 CJK区+扩展A-F区\n    }\n\n    /**\n     * 获取所有书籍\n     */\n    public static List<BookShelfBean> getAllBook() {\n        List<BookShelfBean> bookShelfList = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder()\n                .orderDesc(BookShelfBeanDao.Properties.FinalDate).list();\n        for (int i = 0; i < bookShelfList.size(); i++) {\n            BookInfoBean bookInfoBean = DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder()\n                    .where(BookInfoBeanDao.Properties.NoteUrl.eq(bookShelfList.get(i).getNoteUrl())).limit(1).build().unique();\n            if (bookInfoBean != null) {\n                bookShelfList.get(i).setBookInfoBean(bookInfoBean);\n            } else {\n                bookShelfList.remove(i);\n                i--;\n            }\n        }\n        return bookShelfList;\n    }\n\n    /**\n     * 获取书籍按分组\n     */\n    public static List<BookShelfBean> getBooksByGroup(int group) {\n        List<BookShelfBean> bookShelfList = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder()\n                .where(BookShelfBeanDao.Properties.Group.eq(group))\n                .orderDesc(BookShelfBeanDao.Properties.FinalDate).list();\n        for (int i = 0; i < bookShelfList.size(); i++) {\n            BookInfoBean bookInfoBean = DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder()\n                    .where(BookInfoBeanDao.Properties.NoteUrl.eq(bookShelfList.get(i).getNoteUrl())).limit(1).build().unique();\n            if (bookInfoBean != null) {\n                bookShelfList.get(i).setBookInfoBean(bookInfoBean);\n            } else {\n                DbHelper.getDaoSession().getBookShelfBeanDao().delete(bookShelfList.get(i));\n                bookShelfList.remove(i);\n                i--;\n            }\n        }\n        return bookShelfList;\n    }\n\n    /**\n     * 获取书籍按bookUrl\n     */\n    public static BookShelfBean getBook(String bookUrl) {\n        BookShelfBean bookShelfBean = DbHelper.getDaoSession().getBookShelfBeanDao().load(bookUrl);\n        if (bookShelfBean != null) {\n            BookInfoBean bookInfoBean = DbHelper.getDaoSession().getBookInfoBeanDao().load(bookUrl);\n            if (bookInfoBean != null) {\n                bookShelfBean.setBookInfoBean(bookInfoBean);\n                return bookShelfBean;\n            }\n        }\n        return null;\n    }\n\n    public static List<BookInfoBean> searchBookInfo(String key) {\n        return DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder()\n                .where(BookInfoBeanDao.Properties.Name.like(\"%\" + key + \"%\"))\n                .orderAsc(BookInfoBeanDao.Properties.Name)\n                .list();\n    }\n\n    /**\n     * 移除书籍\n     */\n    public static void removeFromBookShelf(BookShelfBean bookShelfBean, boolean keepCaches) {\n        DbHelper.getDaoSession().getBookShelfBeanDao().deleteByKey(bookShelfBean.getNoteUrl());\n        DbHelper.getDaoSession().getBookInfoBeanDao().deleteByKey(bookShelfBean.getBookInfoBean().getNoteUrl());\n        delChapterList(bookShelfBean.getNoteUrl());\n        if (!keepCaches) {\n            String bookName = bookShelfBean.getBookInfoBean().getName();\n            // 如果书架上有其他同名书籍，只删除本书源的缓存\n            long bookNum = DbHelper.getDaoSession().getBookInfoBeanDao().queryBuilder()\n                    .where(BookInfoBeanDao.Properties.Name.eq(bookName)).count();\n            if (bookNum > 0) {\n                FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH + getCachePathName(bookShelfBean.getBookInfoBean().getName(), bookShelfBean.getTag()));\n                return;\n            }\n            // 没有同名书籍，删除本书所有的缓存\n            try {\n                File file = FileHelp.getFolder(AppConstant.BOOK_CACHE_PATH);\n                String[] bookCaches = file.list((dir, name) -> new File(dir, name).isDirectory() && name.startsWith(bookName + \"-\"));\n                for (String bookPath : bookCaches) {\n                    FileHelp.deleteFile(AppConstant.BOOK_CACHE_PATH + bookPath);\n                }\n            } catch (Exception ignored) {\n            }\n        }\n    }\n\n    /**\n     * 是否在书架\n     */\n    public static boolean isInBookShelf(String bookUrl) {\n        if (bookUrl == null) {\n            return false;\n        }\n\n        long count = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder()\n                .where(BookShelfBeanDao.Properties.NoteUrl.eq(bookUrl))\n                .count();\n        return count > 0;\n    }\n\n    /**\n     * 移除书籍\n     */\n    public static void removeFromBookShelf(BookShelfBean bookShelfBean) {\n        removeFromBookShelf(bookShelfBean, false);\n    }\n\n    /**\n     * 保存书籍\n     */\n    public static void saveBookToShelf(BookShelfBean bookShelfBean) {\n        if (bookShelfBean.getErrorMsg() == null) {\n            DbHelper.getDaoSession().getBookInfoBeanDao().insertOrReplace(bookShelfBean.getBookInfoBean());\n            DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean);\n        }\n    }\n\n    /**\n     * 搜索转书籍\n     */\n    public static BookShelfBean getBookFromSearchBook(SearchBookBean searchBookBean) {\n        BookShelfBean bookShelfBean = new BookShelfBean();\n        bookShelfBean.setTag(searchBookBean.getTag());\n        bookShelfBean.setNoteUrl(searchBookBean.getNoteUrl());\n        bookShelfBean.setFinalDate(System.currentTimeMillis());\n        bookShelfBean.setDurChapter(0);\n        bookShelfBean.setDurChapterPage(0);\n        bookShelfBean.setVariable(searchBookBean.getVariable());\n        BookInfoBean bookInfo = bookShelfBean.getBookInfoBean();\n        bookInfo.setNoteUrl(searchBookBean.getNoteUrl());\n        bookInfo.setAuthor(searchBookBean.getAuthor());\n        bookInfo.setCoverUrl(searchBookBean.getCoverUrl());\n        bookInfo.setName(searchBookBean.getName());\n        bookInfo.setTag(searchBookBean.getTag());\n        bookInfo.setOrigin(searchBookBean.getOrigin());\n        bookInfo.setIntroduce(searchBookBean.getIntroduce());\n        bookInfo.setChapterUrl(searchBookBean.getChapterUrl());\n        bookInfo.setBookInfoHtml(searchBookBean.getBookInfoHtml());\n        bookShelfBean.setVariable(searchBookBean.getVariable());\n        return bookShelfBean;\n    }\n\n    public static List<BookChapterBean> getChapterList(String noteUrl) {\n        return DbHelper.getDaoSession().getBookChapterBeanDao().queryBuilder()\n                .where(BookChapterBeanDao.Properties.NoteUrl.eq(noteUrl))\n                .orderAsc(BookChapterBeanDao.Properties.DurChapterIndex)\n                .build()\n                .list();\n    }\n\n    public static void delChapterList(String noteUrl) {\n        DbHelper.getDaoSession().getBookChapterBeanDao().queryBuilder()\n                .where(BookChapterBeanDao.Properties.NoteUrl.eq(noteUrl))\n                .buildDelete().executeDeleteWithoutDetachingEntities();\n    }\n\n    public static void saveBookmark(BookmarkBean bookmarkBean) {\n        DbHelper.getDaoSession().getBookmarkBeanDao().insertOrReplace(bookmarkBean);\n    }\n\n    public static void delBookmark(BookmarkBean bookmarkBean) {\n        DbHelper.getDaoSession().getBookmarkBeanDao().delete(bookmarkBean);\n    }\n\n    public static List<BookmarkBean> getBookmarkList(String bookName) {\n        return DbHelper.getDaoSession().getBookmarkBeanDao().queryBuilder()\n                .where(BookmarkBeanDao.Properties.BookName.eq(bookName))\n                .orderAsc(BookmarkBeanDao.Properties.ChapterIndex)\n                .build()\n                .list();\n    }\n\n    public static String getReadProgress(BookShelfBean bookShelfBean) {\n        return getReadProgress(bookShelfBean.getDurChapter(), bookShelfBean.getChapterListSize(), 0, 0);\n    }\n\n    public static String getReadProgress(int durChapterIndex, int chapterAll, int durPageIndex, int durPageAll) {\n        DecimalFormat df = new DecimalFormat(\"0.0%\");\n        if (chapterAll == 0 || (durPageAll == 0 && durChapterIndex == 0)) {\n            return \"0.0%\";\n        } else if (durPageAll == 0) {\n            return df.format((durChapterIndex + 1.0f) / chapterAll);\n        }\n        String percent = df.format(durChapterIndex * 1.0f / chapterAll + 1.0f / chapterAll * (durPageIndex + 1) / durPageAll);\n        if (percent.equals(\"100.0%\") && (durChapterIndex + 1 != chapterAll || durPageIndex + 1 != durPageAll)) {\n            percent = \"99.9%\";\n        }\n        return percent;\n    }\n\n    public static String formatAuthor(String author) {\n        if (author == null) {\n            return \"\";\n        }\n        return author.replaceAll(\"作\\\\s*者[\\\\s:：]*\", \"\").replaceAll(\"\\\\s+\", \" \").trim();\n    }\n\n    public static int guessChapterNum(String name) {\n        if (TextUtils.isEmpty(name) || name.matches(\"第.*?卷.*?第.*[章节回]\"))\n            return -1;\n        Matcher matcher = chapterNamePattern.matcher(name);\n        if (matcher.find()) {\n            return StringUtils.stringToInt(matcher.group(2));\n        }\n        return -1;\n    }\n\n    /**\n     * 排序\n     */\n    public static void order(List<BookShelfBean> books, String bookshelfOrder) {\n        if (books == null || books.size() == 0) {\n            return;\n        }\n        switch (bookshelfOrder) {\n            case \"0\":\n                Collections.sort(books, (o1, o2) -> Long.compare(o2.getFinalDate(), o1.getFinalDate()));\n                break;\n            case \"1\":\n                Collections.sort(books, (o1, o2) -> Long.compare(o2.getFinalRefreshData(), o1.getFinalRefreshData()));\n                break;\n            case \"2\":\n                Collections.sort(books, (o1, o2) -> Integer.compare(o1.getSerialNumber(), o2.getSerialNumber()));\n                break;\n        }\n    }\n\n    /**\n     * 清除书架\n     */\n    public static void clearBookshelf() {\n        DbHelper.getDaoSession().getBookShelfBeanDao().deleteAll();\n        DbHelper.getDaoSession().getBookInfoBeanDao().deleteAll();\n        DbHelper.getDaoSession().getBookChapterBeanDao().deleteAll();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/ChangeSourceHelp.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.bean.TwoDataBean;\nimport com.kunfei.bookshelf.model.SearchBookModel;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport io.reactivex.Observable;\n\npublic class ChangeSourceHelp {\n    private SearchBookModel searchBookModel;\n    private BookShelfBean bookShelfBean;\n    private ChangeSourceListener changeSourceListener;\n    private boolean finish;\n\n    public ChangeSourceHelp() {\n        SearchBookModel.OnSearchListener searchListener = new SearchBookModel.OnSearchListener() {\n            @Override\n            public void refreshSearchBook() {\n\n            }\n\n            @Override\n            public void refreshFinish(Boolean value) {\n\n            }\n\n            @Override\n            public void loadMoreFinish(Boolean value) {\n\n            }\n\n            @Override\n            public void loadMoreSearchBook(List<SearchBookBean> value) {\n                selectBook(value);\n            }\n\n            @Override\n            public void searchBookError(Throwable throwable) {\n                if (!finish && changeSourceListener != null) {\n                    changeSourceListener.error(throwable);\n                    searchBookModel.onDestroy();\n                }\n            }\n\n            @Override\n            public int getItemCount() {\n                return 0;\n            }\n        };\n        searchBookModel = new SearchBookModel(searchListener);\n    }\n\n    public void autoChange(BookShelfBean bookShelfBean, ChangeSourceListener changeSourceListener) {\n        this.bookShelfBean = bookShelfBean;\n        this.changeSourceListener = changeSourceListener;\n        long searchTime = System.currentTimeMillis();\n        finish = false;\n        searchBookModel.setSearchTime(searchTime);\n        searchBookModel.search(bookShelfBean.getBookInfoBean().getName(), searchTime, new ArrayList<>(), false);\n    }\n\n    private synchronized void selectBook(List<SearchBookBean> value) {\n        if (finish) return;\n        for (SearchBookBean searchBookBean : value) {\n            if (Objects.equals(searchBookBean.getName(), bookShelfBean.getBookInfoBean().getName())) {\n                if (Objects.equals(searchBookBean.getAuthor(), bookShelfBean.getBookInfoBean().getAuthor())) {\n                    if (changeSourceListener != null) {\n                        finish = true;\n                        changeBookSource(searchBookBean, bookShelfBean)\n                                .subscribe(new MyObserver<TwoDataBean<BookShelfBean, List<BookChapterBean>>>() {\n                                    @Override\n                                    public void onNext(TwoDataBean<BookShelfBean, List<BookChapterBean>> twoData) {\n                                        changeSourceListener.finish(twoData.getData1(), twoData.getData2());\n                                    }\n\n                                    @Override\n                                    public void onError(Throwable e) {\n                                        changeSourceListener.error(e);\n                                    }\n                                });\n                    }\n                    searchBookModel.onDestroy();\n                    break;\n                }\n            } else {\n                break;\n            }\n        }\n    }\n\n    public void stopSearch() {\n        if (searchBookModel != null) {\n            searchBookModel.onDestroy();\n        }\n    }\n\n    public static Observable<TwoDataBean<BookShelfBean, List<BookChapterBean>>> changeBookSource(SearchBookBean searchBook, BookShelfBean oldBook) {\n        BookShelfBean bookShelfBean = BookshelfHelp.getBookFromSearchBook(searchBook);\n        bookShelfBean.setSerialNumber(oldBook.getSerialNumber());\n        bookShelfBean.setLastChapterName(oldBook.getLastChapterName());\n        bookShelfBean.setDurChapterName(oldBook.getDurChapterName());\n        bookShelfBean.setDurChapter(oldBook.getDurChapter());\n        bookShelfBean.setDurChapterPage(oldBook.getDurChapterPage());\n        bookShelfBean.setReplaceEnable(oldBook.getReplaceEnable());\n        bookShelfBean.setAllowUpdate(oldBook.getAllowUpdate());\n        return WebBookModel.getInstance().getBookInfo(bookShelfBean)\n                .flatMap(book -> WebBookModel.getInstance().getChapterList(book))\n                .flatMap(chapterBeanList -> saveChangedBook(bookShelfBean, oldBook, chapterBeanList))\n                .compose(RxUtils::toSimpleSingle);\n    }\n\n    private static Observable<TwoDataBean<BookShelfBean, List<BookChapterBean>>> saveChangedBook(BookShelfBean newBook, BookShelfBean oldBook, List<BookChapterBean> chapterBeanList) {\n        return Observable.create(e -> {\n            if (newBook.getChapterListSize() <= oldBook.getChapterListSize()) {\n                newBook.setHasUpdate(false);\n            }\n            newBook.setCustomCoverPath(oldBook.getCustomCoverPath());\n            newBook.setDurChapter(BookshelfHelp.getDurChapter(oldBook.getDurChapter(), oldBook.getChapterListSize(), oldBook.getDurChapterName(), chapterBeanList));\n            newBook.setDurChapterName(chapterBeanList.get(newBook.getDurChapter()).getDurChapterName());\n            newBook.setGroup(oldBook.getGroup());\n            newBook.getBookInfoBean().setName(oldBook.getBookInfoBean().getName());\n            newBook.getBookInfoBean().setAuthor(oldBook.getBookInfoBean().getAuthor());\n            BookshelfHelp.removeFromBookShelf(oldBook);\n            BookshelfHelp.saveBookToShelf(newBook);\n            DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeanList);\n            e.onNext(new TwoDataBean<>(newBook, chapterBeanList));\n            e.onComplete();\n        });\n    }\n\n    public interface ChangeSourceListener {\n        void finish(BookShelfBean bookShelfBean, List<BookChapterBean> chapterBeanList);\n\n        void error(Throwable throwable);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/ChapterContentHelp.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\nimport com.kunfei.bookshelf.model.ReplaceRuleManager;\nimport com.luhuiguo.chinese.ChineseUtils;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class ChapterContentHelp {\n    private static ChapterContentHelp instance;\n\n    public static synchronized ChapterContentHelp getInstance() {\n        if (instance == null)\n            instance = new ChapterContentHelp();\n        return instance;\n    }\n\n    /**\n     * 转繁体\n     */\n    private String toTraditional(String content) {\n        int convertCTS = ReadBookControl.getInstance().getTextConvert();\n        switch (convertCTS) {\n            case 0:\n                break;\n            case 1:\n                content = ChineseUtils.toSimplified(content);\n                break;\n            case 2:\n                content = ChineseUtils.toTraditional(content);\n                break;\n        }\n        return content;\n    }\n\n    /**\n     * 替换净化\n     */\n    public String replaceContent(String bookName, String bookTag, String content, Boolean replaceEnable) {\n        if (!replaceEnable) return toTraditional(content);\n        if (ReplaceRuleManager.getEnabled().size() == 0) return toTraditional(content);\n        //替换\n        for (ReplaceRuleBean replaceRule : ReplaceRuleManager.getEnabled()) {\n            if (isUseTo(replaceRule.getUseTo(), bookTag, bookName)) {\n                {\n                    try {\n                        // 因为这里获取不到context，就不使用getString(R.string.replace_ad)了\n                        if (replaceRule.getReplaceSummary().matches(\"^广告话术(-.*|$)\")) {\n                            // 跳过太短的文本\n                            if (content.length() > 100)\n                                content = replaceAd2(content, replaceRule.getRegex());\n                        } else\n                            content = content.replaceAll(replaceRule.getFixedRegex(), replaceRule.getReplacement());\n                    } catch (Exception e) {\n                        e.printStackTrace();\n                    }\n                }\n            }\n        }\n        return toTraditional(content);\n    }\n\n    // 緩存生成的廣告規則正則表達式\n//    private Map<String, Pattern> adMap = new HashMap<>();\n    private Map<String, String> adMap = new HashMap<>();\n    // 缓存长表达式，使用普通方式替换\n    private Map<String, String> adMapL = new HashMap<>();\n\n    // 使用广告话术规则对正文进行替换，此方法为正则算法，效率较高，但是有漏失，故暂时放弃使用\n    private String replaceAd(String content, String replaceRule) {\n        // replaceRule只对选择的内容进行了切片，不包含正则\n\n        if (replaceRule == null)\n            return content;\n\n        replaceRule = replaceRule.substring(2, replaceRule.length() - 2).trim();\n\n//        Pattern rule = adMap.get(replaceRule);\n        String rule = adMap.get(replaceRule);\n        StringBuffer buffer = new StringBuffer(replaceRule.length() * 2);\n\n        if (rule == null) {\n            String rules[] = replaceRule.split(\"\\n\");\n\n            for (String s : rules) {\n                s = s.trim();\n                if (s.length() < 1)\n                    continue;\n\n                // 如果规则只包含特殊字符，且长度大于2，直接替换。如果长度不大于2，会在自动扩大范围的过程中包含字符\n                if (s.matches(\"\\\\p{P}*\")) {\n                    if (s.length() > 2) {\n                        if (buffer.length() > 0)\n                            buffer.append('|');\n                        buffer.append(Pattern.quote(s));\n                    }\n                } else {\n                    // 如果规则不止包含特殊字符，需要移除首尾的特殊字符，把中间的空字符转换为\\s+，把其他特殊字符转换为转义符\n                    if (buffer.length() > 0)\n                        buffer.append('|');\n                    buffer.append(s\n                            .replaceFirst(\"^\\\\p{P}+\", \"\")\n                            .replaceFirst(\"\\\\p{P}$\", \"\")\n                            .replaceAll(\"\\\\s+\", \"xxsp\")\n                            .replaceAll(\"(\\\\p{P})\", \"(\\\\\\\\p{P}?)\")\n                            .replaceAll(\"xxsp\", \"\\\\s+\")\n                    );\n                }\n            }\n            // 广告话术至少出现两次\n//            rule = Pattern.compile(\"((\" + buffer + \")(\\\\p{P}{0,2})){1,10}(\" + buffer + \")\");\n            rule = (\"((\" + buffer.toString() + \")(\\\\p{P}{0,2})){1,20}(\" + buffer.toString() + \")((\\\\p{P}{0,12})(?=\\\\p{P}{2}))?\");\n            adMap.put(replaceRule, rule);\n        }\n\n        content = content.replaceAll(rule, \"\");\n\n        rule = adMapL.get(replaceRule);\n        if (rule == null) {\n            String rules[] = replaceRule.split(\"\\n\");\n            buffer = new StringBuffer(replaceRule.length() * 2);\n\n            for (String s : rules) {\n                s = s.trim();\n                if (s.length() < 1)\n                    continue;\n                if (s.length() > 6) {\n                    if (buffer.length() > 0)\n                        buffer.append('|');\n                    buffer.append(Pattern.quote(s));\n                }\n            }\n            rule = \"(\" + buffer.toString() + \")\";\n            adMapL.put(replaceRule, rule);\n        }\n//        Pattern p=Pattern.compile(rule);\n        content = content.replaceAll(rule, \"\");\n        return content;\n    }\n\n\n    // 緩存生成的廣告 原文規則+正则扩展\n    // 原文与正则的最大区别，在于正则匹配规则对特殊符号的处理是保守的\n    private Map<String, Pattern> AdPatternMap = new HashMap<>();\n    // 不包含符号的文本形式的规则缓存。用于广告规则的第二次替换，以解决如下问题： 规则有 abc def，而实际出现了adefbc\n    private Map<String, String> AdStringDict = new HashMap<>();\n\n    // 使用广告话术规则对正文进行替换，此方法 使用Matcher匹配，合并相邻区域，再StringBlock.getResult()的算法取回没有被替换的部分\n    // 广告话术规则的相关代码可能存在以下问题： 零宽断言书写错误，  \\p{P}的使用（比如我最开始不知道\\p{P}是不包含\\\\s的）, getResult.remove()的算法（为了方便调试专门写了verify方法）\n    private String replaceAd2(String content, String replaceRule) {\n\n        if (replaceRule == null)\n            return content;\n\n        StringBlock block = new StringBlock(content);\n\n        Pattern rule = AdPatternMap.get(replaceRule);\n        String stringDict = AdStringDict.get(replaceRule);\n\n\n        if (rule == null) {\n            StringBuffer bufferRegex = new StringBuffer(replaceRule.length() * 3);\n            StringBuffer bufferDict = new StringBuffer();\n\n            String rules[] = replaceRule.split(\"\\n\");\n\n            for (String s : rules) {\n                s = s.trim();\n                if (s.length() < 1)\n                    continue;\n\n                s = Pattern.quote(s);\n\n                if (bufferRegex.length() > 0)\n                    bufferRegex.append('|');\n                else\n                    bufferRegex.append(\"(?=(\");\n                bufferRegex.append(s);\n\n            }\n\n            for (String s : rules) {\n                s = s.trim();\n                if (s.length() < 1)\n                    continue;\n\n                // 如果规则不止包含特殊字符，需要移除首尾的特殊字符，把中间的空字符转换为\\s+，把其他特殊字符转换为转义符\n                if (!s.matches(\"[\\\\p{P}\\\\s]*\")) {\n                    if (bufferRegex.length() > 0)\n                        bufferRegex.append('|');\n                    else\n                        bufferRegex.append(\"(?=(\");\n                    bufferRegex.append(s\n                            .replaceFirst(\"^\\\\p{P}+\", \"\")\n                            .replaceFirst(\"\\\\p{P}$\", \"\")\n                            .replaceAll(\"\\\\s+\", \"xxsp\")\n                            .replaceAll(\"(\\\\p{P})\", \"(\\\\\\\\p{P}?)\")\n                            .replaceAll(\"xxsp\", \"\\\\s+\")\n                    );\n                }\n                if (s.matches(\"[\\\\p{P}\\\\s]*[^\\\\p{P}]{4,}[\\\\p{P}\\\\s]*\")) {\n                    bufferDict.append('\\n');\n                    bufferDict.append(s);\n                }\n            }\n            bufferRegex.append(\"))((\\\\p{P}{0,12})(?=\\\\p{P}{2}))?\");\n            rule = Pattern.compile(bufferRegex.toString());\n            AdPatternMap.put(replaceRule, rule);\n            stringDict = bufferDict.toString();\n            AdStringDict.put(replaceRule, bufferDict.toString());\n        }\n\n        Matcher matcher0 = rule.matcher(content);\n        if (matcher0.groupCount() < 2) {\n//            构造的正则表达式分2个部分，第一部分匹配文字，第二部分匹配符号。完成匹配后实际已经不需要拆墙了\n            Log.w(\"replaceAd2\", \"2 > matcher0.group()==\" + matcher0.groupCount());\n            return content;\n        }\n\n        while (matcher0.find()) {\n\n            if (matcher0.group(2) != null)\n                block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length() + matcher0.group(2).length());\n            else\n                block.remove(matcher0.start(), matcher0.start() + matcher0.group(1).length());\n\n            Log.d(\"replaceAd2()\", \"Remove=\" + block.verify());\n        }\n        block.remove(\"(\\\\p{P}|\\\\s){1,6}([^\\\\p{P}]?(\\\\p{P}|\\\\s){1,6})?\");\n        block.removeDict(stringDict);\n        block.increase(5);\n        return block.getResult();\n    }\n\n\n    class StringBlock {\n        // 保存字符串本体\n        private String string = \"\";\n        // 保存可以复制的区域，奇数为start，偶数为end。\n        private ArrayList<Integer> list;\n        // 保存删除的区域，用于校验\n        private ArrayList<Integer> removed;\n\n        public StringBlock(String string) {\n            this.string = string;\n            list = new ArrayList<>();\n            list.add(0);\n            list.add(string.length());\n            removed = new ArrayList<>();\n        }\n\n        //          验证删除操作是否有bug 验证OK输出正数，异常输出负数\n        public int verify() {\n            // 验证list数列是否有异常\n            if (list.size() % 2 != 0)\n                return -1;\n\n            int p = list.get(0);\n            if (p < 0)\n                return -2;\n            for (int i = 1; i < list.size(); i++) {\n                int q = list.get(i);\n                if (q <= p)\n                    return -3;\n                p = q;\n            }\n            // 验证删除的区域是否还在list构成的区域内\n            for (int j = 0; j < removed.size() / 2; j++) {\n                int j2 = removed.get(j * 2);\n                int j2_1 = removed.get(j * 2 + 1);\n                for (int i = 0; i < list.size() / 2; i++) {\n                    int i2_1 = list.get(i * 2 + 1);\n                    int i2 = list.get(i * 2);\n                    if (i2 > j2) {\n                        break;\n                    }\n                    if (i2_1 < j2) {\n                        continue;\n                    }\n\n                    if (i2_1 == j2) {\n                        if (i * 2 + 2 < list.size()) {\n                            if (list.get(i * 2 + 2) < j2_1)\n                                return -4;\n                        }\n                    } else {\n                        return -5;\n                    }\n\n                }\n            }\n\n            return 0;\n        }\n\n        // 增加字符串的文本，避免被误删除\n        public void increase(int size) {\n            ArrayList<Integer> cache = new ArrayList<>();\n            if (list.get(0) > size)\n                cache.add(list.get(0));\n            else\n                cache.add(0);\n            for (int i = 1; i < list.size() - 1; i = i + 2) {\n                if (list.get(i + 1) - list.get(i) > size) {\n                    cache.add(list.get(i));\n                    cache.add(list.get(i + 1));\n                }\n            }\n            if (string.length() - list.get(list.size() - 1) > size)\n                cache.add(list.get(list.size() - 1));\n            else\n                cache.add(string.length());\n            list = cache;\n        }\n\n        // 去除长度小于等于墙厚的区域\n        public void remove(int wallThick) {\n            int j = list.size() / 2;\n            ArrayList<Integer> cache = new ArrayList<>();\n            for (int i = 0; i < j; i++) {\n                int i2_1 = list.get(i * 2 + 1);\n                int i2 = list.get(i * 2);\n                if ((i2_1 - i2) > wallThick) {\n                    cache.add(i2);\n                    cache.add(i2_1);\n                }\n            }\n            list = cache;\n        }\n\n        // 去除完全与正则匹配的区域\n        public void remove(String wall) {\n            int j = list.size() / 2;\n            ArrayList<Integer> cache = new ArrayList<>();\n            for (int i = 0; i < j; i++) {\n                int i2_1 = list.get(i * 2 + 1);\n                int i2 = list.get(i * 2);\n                if (!string.substring(i2, i2_1).matches(wall)) {\n                    cache.add(i2);\n                    cache.add(i2_1);\n                }\n            }\n            list = cache;\n        }\n\n        public void removeDict(String dict) {\n//            如果孔穴的两端刚好匹配到同一词条，说明这是嵌套的广告话术\n            int j = list.size() / 2;\n            // 缓存需要操作的参数\n            ArrayList<Integer> cache = new ArrayList<>();\n            for (int i = 1; i < j; i++) {\n\n                String str_s0 = getSubString(2 * i - 2).replaceFirst(\"[\\\\p{P}\\\\s]+$\", \"\");\n\n                String str_s1 = str_s0.replaceFirst(\"^.*[\\\\p{P}\\\\s][^$]\", \"\");\n                if (str_s1.length() < 1)\n                    continue;\n\n                String str_e0 = getSubString(2 * i).replaceFirst(\"^[\\\\p{P}\\\\s]+\", \"\");\n                String str_e1 = str_e0.replaceFirst(\"[\\\\p{P}\\\\s].*$\", \"\");\n                if (str_e1.length() < 1)\n                    continue;\n\n\n                // m 第一部分开始的位置\n                int m = list.get(i * 2 - 2) + str_s0.length() - str_s1.length();\n                // 第二部分结尾\n                int n = list.get(i * 2 + 1) - str_e0.length() + str_e1.length();\n\n                if (dict.matches(\"[\\\\s\\\\S]*(\" + str_s1 + \")([^\\\\p{P}]*)(\" + str_e1 + \")[\\\\s\\\\S]*\")) {\n                    cache.add(m);\n                    cache.add(n);\n                } else if (dict.matches(\"[\\\\s\\\\S]*(\\n|^).*\" + str_s1 + \".*(\\n|\\\\s*$)[\\\\s\\\\S]*\")) {\n                    cache.add(m);\n                    cache.add(list.get(i * 2));\n                } else if (dict.matches(\"[\\\\s\\\\S]*(\\n|^).*\" + str_e1 + \".*(\\n|\\\\s*$)[\\\\s\\\\S]*\")) {\n                    // 因为java.*不匹配\\n\n                    cache.add(list.get(i * 2));\n                    cache.add(n);\n                }\n            }\n\n            for (int i = 0; i < cache.size() / 2; i++) {\n                Log.d(\"removeDict\", string.substring(cache.get(i * 2), cache.get((i * 2 + 1))));\n                remove(cache.get(i * 2), cache.get((i * 2 + 1)));\n            }\n\n        }\n\n        public boolean remove(int start, int end) {\n            if (start < 0 || end < 0 || start > string.length() || end > string.length() || start >= end)\n                return false;\n\n            removed.add(start);\n            removed.add(end);\n\n            int j = list.size() / 2;\n            for (int i = 0; i < j; i++) {\n//                start在有效区间中间和在区间的两个边缘，是不同的算法。\n                int i2_1 = list.get(i * 2 + 1);\n                int i2 = list.get(i * 2);\n\n                if (start < i2)\n                    return true;\n\n                if (start == i2) {\n                    if (i2_1 > end) {\n                        list.set(i * 2, end);\n                        return true;\n                    } else {\n                        for (int k = 0; 2 * i + k < list.size(); k++) {\n                            if (list.get(k + 2 * i) > end) {\n                                if (k % 2 == 1) {\n                                    list.set(2 * i + k - 1, end);\n                                } else {\n                                    list.remove(i * 2);\n                                }\n                                for (int m = 0; m < k - 1; m++)\n                                    list.remove(i * 2);\n                                return true;\n                            }\n                        }\n                    }\n                } else if (i2 < start && i2_1 > start) {\n                    if (i2_1 > end) {\n                        list.add(i * 2 + 1, end);\n                        list.add(i * 2 + 1, start);\n                        return true;\n                    } else {\n                        list.set(i * 2 + 1, start);\n                        // i*2+2开始的元素可能需要被删除\n                        for (int k = 2; 2 * i + k < list.size(); k++) {\n                            if (list.get(k + 2 * i) < end)\n                                continue;\n\n                            if (k % 2 == 1) {\n                                if (list.get(k + 2 * i) > end) {\n                                    list.set(2 * i + k - 1, end);\n                                }\n                            } else {\n                                list.remove(i * 2 + 2);\n                            }\n\n                            for (int m = 0; m < k - 1; m++)\n                                list.remove(i * 2 + 2);\n                            return true;\n                        }\n                    }\n                }\n            }\n\n            return false;\n\n        }\n\n        public String getResult() {\n            StringBuffer buffer = new StringBuffer(string.length());\n            int j = list.size() / 2;\n            if (j * 2 > list.size())\n                Log.e(\"StringBlock\", \"list.size=\" + list.size());\n            for (int i = 0; i < j; i++) {\n                buffer.append(string, list.get(i * 2), list.get(i * 2 + 1));\n            }\n            return buffer.toString();\n        }\n\n        public String getSubString(int start) {\n            if (start >= 0 && start < list.size() - 1)\n                return string.substring(list.get(start), list.get(start + 1));\n            return null;\n        }\n    }\n\n    /**\n     * 段落重排算法入口。把整篇内容输入，连接错误的分段，再把每个段落调用其他方法重新切分\n     *\n     * @param content     正文\n     * @param chapterName 标题\n     * @return\n     */\n    public static String LightNovelParagraph2(String content, String chapterName) {\n        if (ReadBookControl.getInstance().getLightNovelParagraph()) {\n            String _content;\n            int chapterNameLength = chapterName.trim().length();\n            if (chapterNameLength > 1) {\n                String regexp = chapterName.trim().replaceAll(\"\\\\s+\", \"(\\\\\\\\s*)\");\n//            质量较低的页面，章节内可能重复出现章节标题\n                if (chapterNameLength > 5)\n                    _content = content.replaceAll(regexp, \"\").trim();\n                else\n                    _content = content.replaceFirst(\"^\\\\s*\" + regexp, \"\").trim();\n            } else {\n                _content = content;\n            }\n\n            List<String> dict = makeDict(_content);\n\n            String[] p = _content\n                    .replaceAll(\"&quot;\", \"“\")\n                    .replaceAll(\"[:：]['\\\"‘”“]+\", \"：“\")\n                    .replaceAll(\"[\\\"”“]+[\\\\s]*[\\\"”“][\\\\s\\\"”“]*\", \"”\\n“\")\n                    .split(\"\\n(\\\\s*)\");\n\n//      初始化StringBuffer的长度,在原content的长度基础上做冗余\n            StringBuffer buffer = new StringBuffer((int) (content.length() * 1.15));\n//          章节的文本格式为章节标题-空行-首段，所以处理段落时需要略过第一行文本。\n            buffer.append(\" \");\n\n            if (!chapterName.trim().equals(p[0].trim())) {\n                // 去除段落内空格。unicode 3000 象形字间隔（中日韩符号和标点），不包含在\\s内\n                buffer.append(p[0].replaceAll(\"[\\u3000\\\\s]+\", \"\"));\n            }\n\n//      如果原文存在分段错误，需要把段落重新黏合\n            for (int i = 1; i < p.length; i++) {\n                if (match(MARK_SENTENCES_END, buffer.charAt(buffer.length() - 1)))\n                    buffer.append(\"\\n\");\n//            段落开头以外的地方不应该有空格\n                // 去除段落内空格。unicode 3000 象形字间隔（中日韩符号和标点），不包含在\\s内\n                buffer.append(p[i].replaceAll(\"[\\u3000\\\\s]\", \"\"));\n\n            }\n            //     预分段预处理\n            //         ”“处理为”\\n“。\n            //         ”。“处理为”。\\n“。不考虑“？”  “！”的情况。\n//                  ”。xxx处理为 ”。\\n xxx\n            p = buffer.toString()\n                    .replaceAll(\"[\\\"”“]+[\\\\s]*[\\\"”“]+\", \"”\\n“\")\n                    .replaceAll(\"[\\\"”“]+(？。！?!~)[\\\"”“]+\", \"”$1\\n“\")\n                    .replaceAll(\"[\\\"”“]+(？。！?!~)([^\\\"”“])\", \"”$1\\n$2\")\n                    .replaceAll(\"([问说喊唱叫骂道着答])[\\\\.。]\", \"$1。\\n\")\n//                .replaceAll(\"([\\\\.。\\\\!！?？])([^\\\"”“]+)[:：][\\\"”“]\", \"$1\\n$2：“\")\n                    .split(\"\\n\");\n\n            buffer = new StringBuffer((int) (content.length() * 1.15));\n\n            for (String s : p) {\n                buffer.append(\"\\n\");\n                buffer.append(FindNewLines(s, dict)\n                );\n            }\n\n            buffer = reduceLength(buffer);\n\n            content = chapterName + \"\\n\\n\"\n                    + buffer.toString()\n                    //处理章节头部空格和换行\n                    .replaceFirst(\"^\\\\s+\", \"\")\n                    // 此规则会造成不规范引号被误换行，暂时无法解决，我认为利大于弊\n                    // 例句：“你”“我”“他”都是一样的\n                    // 误处理为 “你”\\n“我”\\n“他”都是一样的\n                    // 而规范使用的标点不会被误处理： “你”、“我”、“他”，都是一样的。\n                    .replaceAll(\"\\\\s*[\\\"”“]+[\\\\s]*[\\\"”“][\\\\s\\\"”“]*\", \"”\\n“\")\n                    // 规范 A：“B...\n                    .replaceAll(\"[:：][”“\\\"\\\\s]+\", \"：“\")\n                    // 处理奇怪的多余引号  \\n”A：“B... 为 \\nA:“B...\n                    .replaceAll(\"\\n[\\\"“”]([^\\n\\\"“”]+)([,:，：][\\\"”“])([^\\n\\\"“”]+)\", \"\\n$1：“$3\")\n                    .replaceAll(\"\\n(\\\\s*)\", \"\\n\")\n                    // 处理“……”\n//                    .replaceAll(\"\\n[\\\"”“][.,。，…]+\\\\s*[.,。，…]+[\\\"”“]\",\"\\n“……”\")\n                    // 处理被错误断行的省略号。存在较高的误判，但是我认为利大于弊\n                    .replaceAll(\"[.,。，…]+\\\\s*[.,。，…]+\", \"……\")\n                    .replaceAll(\"\\n([\\\\s:：，,]+)\", \"\\n\")\n            ;\n        }\n        return content;\n    }\n\n    /**\n     * 从字符串提取引号包围,且不止出现一次的内容为字典\n     *\n     * @param str\n     * @return 词条列表\n     */\n    private static List<String> makeDict(String str) {\n\n        // 引号中间不包含任何标点，但是没有排除空格\n        Pattern patten = Pattern.compile(\"(?<=[\\\"'”“])([^\\n\\\\p{P}]{1,\" + WORD_MAX_LENGTH + \"})(?=[\\\"'”“])\");\n        Matcher matcher = patten.matcher(str);\n\n        List<String> cache = new ArrayList<>();\n        List<String> dict = new ArrayList<>();\n        List<String> groups = new ArrayList<>();\n\n        while (matcher.find()) {\n            String word = matcher.group();\n            String w = word.replaceAll(\"\\\\s+\", \"\");\n            if (!groups.contains(word))\n                groups.add(word);\n            if (!groups.contains(w))\n                groups.add(w);\n        }\n\n        for (String word : groups) {\n            String w = word.replaceAll(\"\\\\s+\", \"\");\n            if (cache.contains(w)) {\n                if (!dict.contains(w)) {\n                    dict.add(w);\n                    if (!dict.contains(word))\n                        dict.add(word);\n                }\n            } else {\n                cache.add(w);\n                cache.add(word);\n            }\n        }\n/*\n        System.out.print(\"makeDict:\");\n        for (String s : dict)\n            System.out.print(\"\\t\" + s);\n        System.out.print(\"\\n\");\n */\n        return dict;\n    }\n\n    /**\n     * 强制切分，减少段落内的句子\n     * 如果连续2对引号的段落没有提示语，进入对话模式。最后一对引号后强制切分段落\n     * 如果引号内的内容长于5句，可能引号状态有误，随机分段\n     * 如果引号外的内容长于3句，随机分段\n     *\n     * @param str\n     * @return\n     */\n    private static StringBuffer reduceLength(StringBuffer str) {\n        String[] p = str.toString().split(\"\\n\");\n        int l = p.length;\n        boolean[] b = new boolean[l];\n\n        for (int i = 0; i < l; i++) {\n            if (p[i].matches(PARAGRAPH_DIAGLOG))\n                b[i] = true;\n            else\n                b[i] = false;\n        }\n\n        int dialogue = 0;\n\n        for (int i = 0; i < l; i++) {\n            if (b[i]) {\n                if (dialogue < 0)\n                    dialogue = 1;\n                else if (dialogue < 2)\n                    dialogue++;\n            } else {\n                if (dialogue > 1) {\n                    p[i] = splitQuote(p[i]);\n                    dialogue--;\n                } else if (dialogue > 0 && i < l - 2) {\n                    if (b[i + 1])\n                        p[i] = splitQuote(p[i]);\n                }\n            }\n        }\n\n        StringBuffer string = new StringBuffer();\n        for (int i = 0; i < l; i++) {\n            string.append('\\n');\n            string.append(p[i]);\n//            System.out.print(\" \"+b[i]);\n        }\n//        System.out.println(\" \" + str);\n        return string;\n    }\n\n    // 强制切分进入对话模式后，未构成 “xxx” 形式的段落\n    private static String splitQuote(String str) {\n//        System.out.println(\"splitQuote() \" + str);\n        int length = str.length();\n        if (length < 3)\n            return str;\n        if (match(MARK_QUOTATION, str.charAt(0))) {\n            int i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1;\n            if (i > 1)\n                if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1)))\n                    return str.substring(0, i) + \"\\n\" + str.substring(i);\n        } else if (match(MARK_QUOTATION, str.charAt(length - 1))) {\n            int i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false);\n            if (i > 1)\n                if (!match(MARK_QUOTATION_BEFORE, str.charAt(i - 1)))\n                    return str.substring(0, i) + \"\\n\" + str.substring(i);\n        }\n        return str;\n    }\n\n    /**\n     * 计算随机插入换行符的位置。\n     *\n     * @param str    字符串\n     * @param offset 传回的结果需要叠加的偏移量\n     * @param min    最低几个句子，随机插入换行\n     * @param gain   倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行\n     * @return\n     */\n    private static ArrayList<Integer> forceSplit(String str, int offset, int min, int gain, int tigger) {\n        ArrayList<Integer> result = new ArrayList<>();\n        ArrayList<Integer> array_end = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length() - 2, true);\n        ArrayList<Integer> array_mid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length() - 2, true);\n        if (array_end.size() < tigger && array_mid.size() < tigger * 3)\n            return result;\n        int j = 0;\n        for (int i = min; i < array_end.size(); i++) {\n            int k = 0;\n            for (; j < array_mid.size(); j++) {\n                if (array_mid.get(j) < array_end.get(i))\n                    k++;\n            }\n            if (Math.random() * gain < (0.8 + k / 2.5)) {\n                result.add(array_end.get(i) + offset);\n                i = Math.max(i + min, i);\n            }\n        }\n        return result;\n    }\n\n    // 对内容重新划分段落.输入参数str已经使用换行符预分割\n    private static String FindNewLines(String str, List<String> dict) {\n        StringBuffer string = new StringBuffer(str);\n        // 标记string中每个引号的位置.特别的，用引号进行列举时视为只有一对引号。 如：“锅”、“碗”视为“锅、碗”，从而避免误断句。\n        List<Integer> array_quote = new ArrayList<>();\n        // 标记忽略的引号\n        List<Integer> array_ignore_quote = new ArrayList<>();\n        //  标记插入换行符的位置，int为插入位置（str的char下标）\n        ArrayList<Integer> ins_n = new ArrayList<>();\n        //  标记不需要插入换行符的位置。功能暂未实现。\n        ArrayList<Integer> remove_n = new ArrayList<>();\n\n//      mod[i]标记str的每一段处于引号内还是引号外。范围： str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。\n//      长度：array_quote.size(),但是初始化时未预估占用的长度，用空间换时间\n//      0未知，正数引号内，负数引号外。\n//      如果相邻的两个标记都为+1，那么需要增加1个引号。\n//      引号内不进行断句\n        int[] mod = new int[str.length()];\n        boolean wait_close = false;\n\n        for (int i = 0; i < str.length(); i++) {\n            char c = str.charAt(i);\n            if (match(MARK_QUOTATION, c)) {\n                int size = array_quote.size();\n\n                //        把“xxx”、“yy”和“z”合并为“xxx_yy_z”进行处理\n                if (size > 0) {\n                    int quote_pre = array_quote.get(size - 1);\n                    if (i - quote_pre == 2) {\n                        boolean remove = false;\n                        if (wait_close) {\n                            if (match(\",，、/\", str.charAt(i - 1))) {\n                                // 考虑出现“和”这种特殊情况\n                                remove = true;\n                            }\n                        } else if (match(\",，、/和与或\", str.charAt(i - 1))) {\n                            remove = true;\n                        }\n                        if (remove) {\n                            string.setCharAt(i, '“');\n                            string.setCharAt(i - 2, '”');\n                            array_quote.remove(size - 1);\n                            mod[size - 1] = 1;\n                            mod[size] = -1;\n                            continue;\n                        }\n                    }\n                }\n                array_quote.add(i);\n\n                //  为xxx：“xxx”做标记\n                if (i > 1) {\n                    // 当前发言的正引号的前一个字符\n                    char char_b1 = str.charAt(i - 1);\n                    // 上次发言的正引号的前一个字符\n                    char char_b2 = 0;\n                    if (match(MARK_QUOTATION_BEFORE, char_b1)) {\n                        // 如果不是第一处引号，寻找上一处断句，进行分段\n                        if (array_quote.size() > 1) {\n                            int last_quote = array_quote.get(array_quote.size() - 2);\n                            int p = 0;\n                            if (char_b1 == ',' || char_b1 == '，') {\n                                if (array_quote.size() > 2) {\n                                    p = array_quote.get(array_quote.size() - 3);\n                                    if (p > 0) {\n                                        char_b2 = str.charAt(p - 1);\n                                    }\n                                }\n                            }\n//                            if(char_b2=='.' || char_b2=='。')\n                            if (match(MARK_SENTENCES_END_P, char_b2))\n                                ins_n.add(p - 1);\n                            else if (match(\"的\", char_b2)) {\n                                //剔除引号标记aaa的\"xxs\"，bbb的“yyy”\n\n                            } else {\n                                int last_end = seekLast(str, MARK_SENTENCES_END, i, last_quote);\n                                if (last_end > 0)\n                                    ins_n.add(last_end);\n                                else\n                                    ins_n.add(last_quote);\n                            }\n                        }\n\n                        wait_close = true;\n                        mod[size] = 1;\n                        if (size > 0) {\n                            mod[size - 1] = -1;\n                            if (size > 1) {\n                                mod[size - 2] = 1;\n                            }\n\n/*\n                            int quote_pre = array_quote.get(array_quote.size() - 2);\n                            boolean flag_ins_n = false;\n                            for (int j = i; j > quote_pre; j--) {\n                                if (match(MARK_SENTENCES_END, string.charAt(j))) {\n                                    ins_n.add(j);\n                                    flag_ins_n = true;\n                                }\n                            }\n                            if (!flag_ins_n)\n                                ins_n.add(quote_pre);\n                            */\n                        }\n                    } else if (wait_close) {\n                        {\n                            wait_close = false;\n                            ins_n.add(i);\n                        }\n                    }\n                }\n\n            }\n        }\n\n        int size = array_quote.size();\n\n\n//        标记循环状态，此位置前的引号是否已经配对\n        boolean opend = false;\n        if (size > 0) {\n\n//        第1次遍历array_quote，令其元素的值不为0\n            for (int i = 0; i < size; i++) {\n                if (mod[i] > 0) {\n                    opend = true;\n                } else if (mod[i] < 0) {\n//                连续2个反引号表明存在冲突，强制把前一个设为正引号\n                    if (!opend) {\n                        if (i > 0)\n                            mod[i] = 3;\n                    }\n                    opend = false;\n                } else {\n                    opend = !opend;\n                    if (opend)\n                        mod[i] = 2;\n                    else\n                        mod[i] = -2;\n                }\n            }\n//        修正，断尾必须封闭引号\n            if (opend) {\n                if (array_quote.get(size - 1) - string.length() > -3) {\n//            if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){\n                    if (size > 1)\n                        mod[size - 2] = 4;\n                    // 0<=i<size,故无需判断size>=1\n                    mod[size - 1] = -4;\n                } else if (!match(MARK_SENTENCES_SAY, string.charAt(string.length() - 2)))\n                    string.append(\"”\");\n            }\n\n\n//      第2次循环，mod[i]由负变正时，前1字符如果是句末，需要插入换行\n            int loop2_mod_1 = -1; //上一个引号跟随内容的状态\n            int loop2_mod_2; //当前引号跟随内容的状态\n            int i = 0;\n            int j = array_quote.get(0) - 1; //当前引号前一字符的序号\n            if (j < 0) {\n                i = 1;\n                loop2_mod_1 = 0;\n            }\n\n            for (; i < size; i++) {\n                j = array_quote.get(i) - 1;\n                loop2_mod_2 = mod[i];\n                if (loop2_mod_1 < 0 && loop2_mod_2 > 0) {\n                    if (match(MARK_SENTENCES_END, string.charAt(j)))\n                        ins_n.add(j);\n                }\n/*                else if (mod[i - 1] > 0 && mod[i] < 0) {\n                    if (j > 0) {\n                        if (match(MARK_SENTENCES_END, string.charAt(j)))\n                            ins_n.add(j);\n                    }\n                }\n*/\n                loop2_mod_1 = loop2_mod_2;\n            }\n        }\n\n//        第3次循环，匹配并插入换行。\n//        \"xxxx\" xxxx。\\n xxx“xxxx”\n//        未实现\n\n\n        // 使用字典验证ins_n , 避免插入不必要的换行。\n        // 由于目前没有插入、的列表，无法解决 “xx”、“xx”“xx” 被插入换行的问题\n        ArrayList<Integer> _ins_n = new ArrayList<>();\n        for (int i : ins_n) {\n            if (match(\"\\\"'”“\", string.charAt(i))) {\n                int start = seekLast(str, \"\\\"'”“\", i - 1, i - WORD_MAX_LENGTH);\n                if (start > 0) {\n                    String word = str.substring(start + 1, i);\n\n                    if (dict.contains(word)) {\n//                        System.out.println(\"使用字典验证 跳过\\tins_n=\" + i + \"  word=\" + word);\n//                        引号内如果是字典词条，后方不插入换行符（前方不需要优化）\n                        remove_n.add(i);\n                        continue;\n                    } else {\n                        System.out.println(\"使用字典验证 插入\\tins_n=\" + i + \"  word=\" + word);\n                        if (match(\"的地得和或\", str.charAt(start))) {\n//                        xx的“xx”，后方不插入换行符（前方不需要优化）\n                            continue;\n                        }\n\n                    }\n                }\n            } else {\n                //          System.out.println(\"使用字典验证 else\\tins_n=\" + i + \"  substring=\" + string.substring(i-5,i+5));\n\n            }\n            _ins_n.add(i);\n        }\n        ins_n = _ins_n;\n\n\n//        随机在句末插入换行符\n        ins_n = new ArrayList<Integer>(new HashSet<Integer>(ins_n));\n        Collections.sort(ins_n);\n\n\n        {\n            String subs = \"\";\n            int j = 0;\n            int progress = 0;\n\n            int next_line = -1;\n            if (ins_n.size() > 0)\n                next_line = ins_n.get(j);\n\n            int gain = 3;\n            int min = 0;\n            int trigger = 2;\n\n            for (int i = 0; i < array_quote.size(); i++) {\n                int qutoe = array_quote.get(i);\n                if (qutoe > 0) {\n                    gain = 4;\n                    min = 2;\n                    trigger = 4;\n                } else {\n                    gain = 3;\n                    min = 0;\n                    trigger = 2;\n                }\n\n//            把引号前的换行符与内容相间插入\n                for (; j < ins_n.size(); j++) {\n//                如果下一个换行符在当前引号前，那么需要此次处理.如果紧挨当前引号，需要考虑插入引号的情况\n                    if (next_line >= qutoe)\n                        break;\n                    next_line = ins_n.get(j);\n                    if (progress < next_line) {\n                        subs = string.substring(progress, next_line);\n                        ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));\n                        progress = next_line + 1;\n                    }\n                }\n                if (progress < qutoe) {\n                    subs = string.substring(progress, qutoe + 1);\n                    ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));\n                    progress = qutoe + 1;\n                }\n            }\n\n\n            for (; j < ins_n.size(); j++) {\n                next_line = ins_n.get(j);\n                if (progress < next_line) {\n                    subs = string.substring(progress, next_line);\n                    ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));\n                    progress = next_line + 1;\n                }\n            }\n\n            if (progress < string.length()) {\n                subs = string.substring(progress, string.length());\n                ins_n.addAll(forceSplit(subs, progress, min, gain, trigger));\n            }\n\n        }\n\n//     根据段落状态修正引号方向、计算需要插入引号的位置\n//     ins_quote跟随array_quote   ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'\n        boolean[] ins_quote = new boolean[size];\n        opend = false;\n        for (int i = 0; i < size; i++) {\n            int p = array_quote.get(i);\n            if (mod[i] > 0) {\n                string.setCharAt(p, '“');\n                if (opend)\n                    ins_quote[i] = true;\n                opend = true;\n            } else if (mod[i] < 0) {\n                string.setCharAt(p, '”');\n                opend = false;\n            } else {\n                opend = !opend;\n                if (opend)\n                    string.setCharAt(p, '“');\n                else\n                    string.setCharAt(p, '”');\n            }\n        }\n\n        ins_n = new ArrayList<Integer>(new HashSet<Integer>(ins_n));\n        Collections.sort(ins_n);\n\n//        输出log进行检验\n/*\n        System.out.println(\"quote[i]:position/mod\\t\" + string);\n        for (int i = 0; i < array_quote.size(); i++) {\n            System.out.print(\" [\" + i + \"]\" + array_quote.get(i) + \"/\" + mod[i]);\n        }\n        System.out.print(\"\\n\");\n\n        System.out.print(\"ins_q:\");\n        for (int i = 0; i < ins_quote.length; i++) {\n            System.out.print(\" \" + ins_quote[i]);\n        }\n        System.out.print(\"\\n\");\n\n        System.out.print(\"ins_n:\");\n\n        for (int i : ins_n) {\n            System.out.print(\" \" + i);\n        }\n        System.out.print(\"\\n\");\n*/\n\n//     完成字符串拼接（从string复制、插入引号和换行\n//     ins_quote 在引号前插入一个引号。   ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'\n//     ins_n 插入换行。数组的值表示插入换行符的位置\n        StringBuffer buffer = new StringBuffer((int) (str.length() * 1.15));\n\n        int j = 0;\n        int progress = 0;\n\n        int next_line = -1;\n        if (ins_n.size() > 0)\n            next_line = ins_n.get(j);\n\n        for (int i = 0; i < array_quote.size(); i++) {\n            int qutoe = array_quote.get(i);\n\n//            把引号前的换行符与内容相间插入\n            for (; j < ins_n.size(); j++) {\n//                如果下一个换行符在当前引号前，那么需要此次处理.如果紧挨当前引号，需要考虑插入引号的情况\n                if (next_line >= qutoe)\n                    break;\n                next_line = ins_n.get(j);\n                buffer.append(string, progress, next_line + 1);\n                buffer.append('\\n');\n                progress = next_line + 1;\n            }\n            if (progress < qutoe) {\n                buffer.append(string, progress, qutoe + 1);\n                progress = qutoe + 1;\n            }\n            if (ins_quote[i] && buffer.length() > 2) {\n                if (buffer.charAt(buffer.length() - 1) == '\\n')\n                    buffer.append('“');\n                else\n                    buffer.insert(buffer.length() - 1, \"”\\n\");\n            }\n        }\n\n        for (; j < ins_n.size(); j++) {\n            next_line = ins_n.get(j);\n            if (progress <= next_line) {\n                buffer.append(string, progress, next_line + 1);\n                buffer.append('\\n');\n                progress = next_line + 1;\n            }\n        }\n\n        if (progress < string.length()) {\n            buffer.append(string, progress, string.length());\n        }\n\n        return buffer.toString();\n    }\n\n    /**\n     * 计算匹配到字典的每个字符的位置\n     *\n     * @param str     待匹配的字符串\n     * @param key     字典\n     * @param from    从字符串的第几个字符开始匹配\n     * @param to      匹配到第几个字符结束\n     * @param inOrder 是否按照从前向后的顺序匹配\n     * @return 返回距离构成的ArrayList<Integer>\n     */\n    private static ArrayList<Integer> seekIndexs(String str, String key, int from, int to, boolean inOrder) {\n        ArrayList<Integer> list = new ArrayList<>();\n\n        if (str.length() - from < 1)\n            return list;\n        int i = 0;\n        if (from > i)\n            i = from;\n        int t = str.length();\n        if (to > 0)\n            t = Math.min(t, to);\n        char c;\n        for (; i < t; i++) {\n            if (inOrder)\n                c = str.charAt(i);\n            else\n                c = str.charAt(str.length() - i - 1);\n            if (key.indexOf(c) != -1) {\n                list.add(i);\n            }\n        }\n        return list;\n    }\n\n\n    /**\n     * 计算字符串最后出现与字典中字符匹配的位置\n     *\n     * @param str  数据字符串\n     * @param key  字典字符串\n     * @param from 从哪个字符开始匹配，默认最末位\n     * @param to   匹配到哪个字符（不包含此字符）默认0\n     * @return 位置（正向计算)\n     */\n    private static int seekLast(String str, String key, int from, int to) {\n        if (str.length() - from < 1)\n            return -1;\n        int i = str.length() - 1;\n        if (from < i && i > 0)\n            i = from;\n        int t = 0;\n        if (to > 0)\n            t = to;\n        char c;\n        for (; i > t; i--) {\n            c = str.charAt(i);\n            if (key.indexOf(c) != -1) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    /**\n     * 计算字符串与字典中字符的最短距离\n     *\n     * @param str     数据字符串\n     * @param key     字典字符串\n     * @param from    从哪个字符开始匹配，默认0\n     * @param to      匹配到哪个字符（不包含此字符）默认匹配到最末位\n     * @param inOrder 是否从正向开始匹配\n     * @return 返回最短距离, 注意不是str的char的下标\n     */\n    private static int seekIndex(String str, String key, int from, int to, boolean inOrder) {\n        if (str.length() - from < 1)\n            return -1;\n        int i = 0;\n        if (from > i)\n            i = from;\n        int t = str.length();\n        if (to > 0)\n            t = Math.min(t, to);\n        char c;\n        for (; i < t; i++) {\n            if (inOrder)\n                c = str.charAt(i);\n            else\n                c = str.charAt(str.length() - i - 1);\n            if (key.indexOf(c) != -1) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    /**\n     * 计算字符串与字典的距离。\n     *\n     * @param str     数据字符串\n     * @param form    从第几个字符开始匹配\n     * @param to      匹配到第几个字符串结束\n     * @param inOrder 是否从前向后匹配。\n     * @param words   可变长参数构成的字典。每个字符串代表一个字符\n     * @return 匹配结果。注意这个距离是使用第一个字符进行计算的\n     */\n    private static int seekWordsIndex(String str, int form, int to, boolean inOrder, String... words) {\n\n        if (words.length < 1)\n            return -2;\n\n        int i = seekIndex(str, words[0], form, to, inOrder);\n        if (i < 0)\n            return i;\n\n        for (int j = 1; j < words.length; j++) {\n            int k = seekIndex(str, words[j], form, to, inOrder);\n            if (inOrder) {\n                if (i + j != k)\n                    return -3;\n            } else {\n                if (i - j != k)\n                    return -3;\n            }\n        }\n        return i;\n    }\n\n    /* 搜寻引号并进行分段。处理了一、二、五三类常见情况\n    参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。\n    一、完整引用说话内容，在反引号内侧有断句标点。例如：\n            1) 丫姑折断几枝扔下来，边叫我的小名儿边说：“先喂饱你！”\n            2）“哎呀，真是美极了！”皇帝说，“我十分满意！”\n            3）“怕什么！海的美就在这里！”我说道。\n    二、部分引用，在反引号外侧有断句标点：\n            4）适当地改善自己的生活，岂但“你管得着吗”，而且是顺乎天理，合乎人情的。\n            5）现代画家徐悲鸿笔下的马，正如有的评论家所说的那样，“形神兼备，充满生机”。\n            6）唐朝的张嘉贞说它“制造奇特，人不知其所为”。\n    三、一段接着一段地直接引用时，中间段落只在段首用起引号，该段段尾却不用引回号。但是正统文学不在考虑范围内。\n    四、引号里面又要用引号时，外面一层用双引号，里面一层用单引号。暂时不需要考虑\n    五、反语和强调，周围没有断句符号。\n*/\n\n    //  段落换行符\n    private static String SPACE_BEFORE_PARAGRAPH = \"\\n    \";\n    //  段落末位的标点\n    private static String MARK_SENTENCES = \"？。！?!~”\\\"\";\n    //  句子结尾的标点。因为引号可能存在误判，不包含引号。\n    private static String MARK_SENTENCES_END = \"？。！?!~\";\n    private static String MARK_SENTENCES_END_P = \".？。！?!~\";\n    //  句中标点，由于某些网站常把“，”写为\".\"，故英文句点按照句中标点判断\n    private static String MARK_SENTENCES_MID = \".，、,—…\";\n    private static String MARK_SENTENCES_F = \"啊嘛吧吗噢哦了呢呐\";\n    private static String MARK_SENTENCES_SAY = \"问说喊唱叫骂道着答\";\n    //  XXX说：“”的冒号\n    private static String MARK_QUOTATION_BEFORE = \"，：,:\";\n    //  引号\n    private static String MARK_QUOTATION = \"\\\"“”\";\n\n    private static String PARAGRAPH_DIAGLOG = \"^[\\\"”“][^\\\"”“]+[\\\"”“]$\";\n    //  限制字典的长度\n    private static int WORD_MAX_LENGTH = 16;\n\n    private static boolean isFullSentences(String s) {\n        if (s.length() < 2)\n            return false;\n        char c = s.charAt(s.length() - 1);\n        return MARK_SENTENCES.indexOf(c) != -1;\n    }\n\n    private static boolean match(String rule, char chr) {\n        return rule.indexOf(chr) != -1;\n    }\n\n\n    private boolean isUseTo(String useTo, String bookTag, String bookName) {\n        return TextUtils.isEmpty(useTo)\n                || useTo.contains(bookTag)\n                || useTo.contains(bookName);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/CrashHandler.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.Log;\nimport android.widget.Toast;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.io.Writer;\nimport java.lang.reflect.Field;\nimport java.text.SimpleDateFormat;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * 异常管理类\n */\npublic class CrashHandler implements Thread.UncaughtExceptionHandler {\n\n    /**\n     * 系统默认UncaughtExceptionHandler\n     */\n    private Thread.UncaughtExceptionHandler mDefaultHandler;\n\n    /**\n     * context\n     */\n    private Context mContext;\n\n    /**\n     * 存储异常和参数信息\n     */\n    private Map<String, String> paramsMap = new HashMap<>();\n\n    /**\n     * 格式化时间\n     */\n    @SuppressLint(\"SimpleDateFormat\")\n    private SimpleDateFormat format = new SimpleDateFormat(\"yyyy-MM-dd-HH-mm-ss\");\n\n    private String TAG = this.getClass().getSimpleName();\n\n    @SuppressLint(\"StaticFieldLeak\")\n    private static CrashHandler mInstance;\n\n    private CrashHandler() {\n\n    }\n\n    /**\n     * 获取CrashHandler实例\n     */\n    public static synchronized CrashHandler getInstance() {\n        if (null == mInstance) {\n            mInstance = new CrashHandler();\n        }\n        return mInstance;\n    }\n\n    public void init(Context context) {\n        mContext = context;\n        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();\n        //设置该CrashHandler为系统默认的\n        Thread.setDefaultUncaughtExceptionHandler(this);\n    }\n\n    /**\n     * uncaughtException 回调函数\n     */\n    @Override\n    public void uncaughtException(Thread thread, Throwable ex) {\n        if (!handleException(ex) && mDefaultHandler != null) {\n            //如果自己没处理交给系统处理\n            mDefaultHandler.uncaughtException(thread, ex);\n        } else {\n            //自己处理\n            try {//延迟3秒杀进程\n                Thread.sleep(3000);\n            } catch (InterruptedException e) {\n                Log.e(TAG, \"error : \", e);\n            }\n        }\n\n    }\n\n    /**\n     * 收集错误信息.发送到服务器\n     *\n     * @return 处理了该异常返回true, 否则false\n     */\n    private boolean handleException(Throwable ex) {\n        if (ex == null) {\n            return false;\n        }\n        //收集设备参数信息\n        collectDeviceInfo(mContext);\n        //添加自定义信息\n        addCustomInfo();\n        try {\n            //使用Toast来显示异常信息\n            new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(mContext, ex.getMessage(), Toast.LENGTH_LONG).show());\n        } catch (Exception ignored) {\n        }\n        //保存日志文件\n        saveCrashInfo2File(ex);\n        return false;\n    }\n\n\n    /**\n     * 收集设备参数信息\n     */\n    private void collectDeviceInfo(Context ctx) {\n        //获取versionName,versionCode\n        try {\n            PackageManager pm = ctx.getPackageManager();\n            PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);\n            if (pi != null) {\n                String versionName = pi.versionName == null ? \"null\" : pi.versionName;\n                String versionCode = pi.versionCode + \"\";\n                paramsMap.put(\"versionName\", versionName);\n                paramsMap.put(\"versionCode\", versionCode);\n            }\n        } catch (PackageManager.NameNotFoundException e) {\n            Log.e(TAG, \"an error occured when collect package info\", e);\n        }\n        //获取所有系统信息\n        Field[] fields = Build.class.getDeclaredFields();\n        for (Field field : fields) {\n            try {\n                field.setAccessible(true);\n                paramsMap.put(field.getName(), field.get(null).toString());\n            } catch (Exception e) {\n                Log.e(TAG, \"an error occured when collect crash info\", e);\n            }\n        }\n    }\n\n    /**\n     * 添加自定义参数\n     */\n    private void addCustomInfo() {\n        Log.i(TAG, \"addCustomInfo: 程序出错了...\");\n    }\n\n    /**\n     * 保存错误信息到文件中\n     */\n    private void saveCrashInfo2File(Throwable ex) {\n\n        StringBuilder sb = new StringBuilder();\n        for (Map.Entry<String, String> entry : paramsMap.entrySet()) {\n            String key = entry.getKey();\n            String value = entry.getValue();\n            sb.append(key).append(\"=\").append(value).append(\"\\n\");\n        }\n\n        Writer writer = new StringWriter();\n        PrintWriter printWriter = new PrintWriter(writer);\n        ex.printStackTrace(printWriter);\n        Throwable cause = ex.getCause();\n        while (cause != null) {\n            cause.printStackTrace(printWriter);\n            cause = cause.getCause();\n        }\n        printWriter.close();\n        String result = writer.toString();\n        sb.append(result);\n        try {\n            long timestamp = System.currentTimeMillis();\n            String time = format.format(new Date());\n            String fileName = \"crash-\" + time + \"-\" + timestamp + \".log\";\n            String path = FileHelp.getCachePath() + \"/crash/\";\n            File dir = new File(path);\n            if (!dir.exists()) {\n                //noinspection ResultOfMethodCallIgnored\n                dir.mkdirs();\n            }\n            FileOutputStream fos = new FileOutputStream(path + fileName);\n            fos.write(sb.toString().getBytes());\n            Log.i(TAG, \"saveCrashInfo2File: \" + sb.toString());\n            fos.close();\n        } catch (Exception e) {\n            Log.e(TAG, \"an error occured while writing file...\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/DefaultValueHelper.kt",
    "content": "package com.kunfei.bookshelf.help\n\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.bean.BookSourceBean\nimport com.kunfei.bookshelf.utils.GSON\nimport com.kunfei.bookshelf.utils.fromJsonObject\nimport java.io.File\n\nobject DefaultValueHelper {\n\n    val xxlSource: BookSourceBean by lazy {\n        val json = String(\n            MApplication.getInstance().assets.open(\"data${File.separator}BookSourceXxl.json\")\n                .readBytes()\n        )\n        GSON.fromJsonObject(json)!!\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/DocumentHelper.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.graphics.Bitmap;\nimport android.net.Uri;\n\nimport androidx.documentfile.provider.DocumentFile;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.utils.DocumentUtil;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileNotFoundException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\n\n/**\n * Created by PureDark on 2016/9/24.\n */\n\npublic class DocumentHelper {\n\n    public static boolean isFileExist(String fileName, String rootPath, String... subDirs) {\n        return DocumentUtil.isFileExist(MApplication.getInstance(), fileName, rootPath, subDirs);\n    }\n\n    public static DocumentFile getDirDocument(String rootPath, String... subDirs) {\n        return DocumentUtil.getDirDocument(MApplication.getInstance(), rootPath, subDirs);\n    }\n\n    public static DocumentFile createFileIfNotExist(String fileName, String path, String... subDirs) {\n        if (!path.startsWith(\"content://\"))\n            path = \"file://\" + Uri.decode(path);\n        return DocumentUtil.createFileIfNotExist(MApplication.getInstance(), fileName, path, subDirs);\n    }\n\n    public static DocumentFile createDirIfNotExist(String path, String... subDirs) {\n        if (!path.startsWith(\"content://\"))\n            path = \"file://\" + Uri.decode(path);\n        return DocumentUtil.createDirIfNotExist(MApplication.getInstance(), path, subDirs);\n    }\n\n    public static boolean deleteFile(String fileName, String rootPath, String... subDirs) {\n        if (!rootPath.startsWith(\"content://\"))\n            rootPath = \"file://\" + Uri.decode(rootPath);\n        return DocumentUtil.deleteFile(MApplication.getInstance(), fileName, rootPath, subDirs);\n    }\n\n    public static boolean writeString(String string, DocumentFile file) {\n        return DocumentUtil.writeBytes(MApplication.getInstance(), string.getBytes(), file);\n    }\n\n    public static boolean writeString(String string, String fileName, String rootPath, String... subDirs) {\n        if (!rootPath.startsWith(\"content://\"))\n            rootPath = \"file://\" + Uri.decode(rootPath);\n        return DocumentUtil.writeBytes(MApplication.getInstance(), string.getBytes(), fileName, rootPath, subDirs);\n    }\n\n    public static String readString(String fileName, String rootPath, String... subDirs) {\n        byte[] data = DocumentUtil.readBytes(MApplication.getInstance(), fileName, rootPath, subDirs);\n        String string = null;\n        try {\n            string = new String(data, \"utf-8\");\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return string;\n    }\n\n    public static String readString(Uri uri) {\n        byte[] data = DocumentUtil.readBytes(MApplication.getInstance(), uri);\n        String string = null;\n        try {\n            string = new String(data, \"utf-8\");\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return string;\n    }\n\n    public static String readString(DocumentFile file) {\n        byte[] data = DocumentUtil.readBytes(MApplication.getInstance(), file);\n        String string = null;\n        try {\n            string = new String(data, \"utf-8\");\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return string;\n    }\n\n    public static boolean writeBytes(byte[] data, String fileName, String rootPath, String... subDirs) {\n        if (!rootPath.startsWith(\"content://\"))\n            rootPath = \"file://\" + Uri.decode(rootPath);\n        return DocumentUtil.writeBytes(MApplication.getInstance(), data, fileName, rootPath, subDirs);\n    }\n\n    public static boolean writeBytes(byte[] data, DocumentFile file) {\n        if (file == null)\n            return false;\n        return DocumentUtil.writeBytes(MApplication.getInstance(), data, file);\n    }\n\n    public static boolean writeFromFile(File fromFile, DocumentFile file) {\n        if (file == null)\n            return false;\n        try {\n            return DocumentUtil.writeFromInputStream(MApplication.getInstance(), new FileInputStream(fromFile), file);\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n            return false;\n        }\n    }\n\n    public static boolean writeFromInputStream(InputStream inStream, DocumentFile file) {\n        if (file == null)\n            return false;\n        return DocumentUtil.writeFromInputStream(MApplication.getInstance(), inStream, file);\n    }\n\n    public static void saveBitmapToFile(Bitmap bitmap, DocumentFile file) throws IOException {\n        saveBitmapToFile(bitmap, file.getUri());\n    }\n\n    public static void saveBitmapToFile(Bitmap bitmap, Uri fileUri) throws IOException {\n        OutputStream out = MApplication.getInstance().getContentResolver().openOutputStream(fileUri);\n        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);\n        assert out != null;\n        out.flush();\n        out.close();\n    }\n\n    public static OutputStream getFileOutputSteam(String fileName, String rootPath, String... subDirs) {\n        if (!rootPath.startsWith(\"content://\"))\n            rootPath = \"file://\" + Uri.decode(rootPath);\n        return DocumentUtil.getFileOutputSteam(MApplication.getInstance(), fileName, rootPath, subDirs);\n    }\n\n    public static InputStream getFileInputSteam(String fileName, String rootPath, String... subDirs) {\n        if (!rootPath.startsWith(\"content://\"))\n            rootPath = \"file://\" + Uri.decode(rootPath);\n        return DocumentUtil.getFileInputSteam(MApplication.getInstance(), fileName, rootPath, subDirs);\n    }\n\n    public static String filenameFilter(String str) {\n        return DocumentUtil.filenameFilter(str);\n    }\n\n    public static byte[] getBytes(File file) {\n        byte[] buffer = null;\n        try {\n            FileInputStream fis = new FileInputStream(file);\n            ByteArrayOutputStream bos = new ByteArrayOutputStream(1000);\n            byte[] b = new byte[1000];\n            int n;\n            while ((n = fis.read(b)) != -1) {\n                bos.write(b, 0, n);\n            }\n            fis.close();\n            bos.close();\n            buffer = bos.toByteArray();\n        } catch (FileNotFoundException e) {\n            e.printStackTrace();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return buffer;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/Donate.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\n\nimport java.net.URLEncoder;\n\n/**\n * Created by GKF on 2017/12/18.\n * 捐赠\n */\n\npublic class Donate {\n\n    public static void aliDonate(Context context) {\n        try {\n            String qrCode = URLEncoder.encode(\"https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=web-other\", \"utf-8\");\n            String aliPayQr = \"alipayqr://platformapi/startapp?saId=10000007&qrcode=\" + qrCode + \"&_t=\" + System.currentTimeMillis();\n            openUri(context, aliPayQr);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * 发送一个intent\n     */\n    private static void openUri(Context context, String s) {\n        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s));\n        context.startActivity(intent);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/EncodeConverter.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.utils.EncodingDetect;\n\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Type;\nimport java.nio.charset.Charset;\n\nimport okhttp3.MediaType;\nimport okhttp3.ResponseBody;\nimport retrofit2.Converter;\nimport retrofit2.Retrofit;\n\npublic class EncodeConverter extends Converter.Factory {\n    private String encode;\n\n    private EncodeConverter() {\n\n    }\n\n    private EncodeConverter(String encode) {\n        this.encode = encode;\n    }\n\n    public static EncodeConverter create() {\n        return new EncodeConverter();\n    }\n\n    public static EncodeConverter create(String en) {\n        return new EncodeConverter(en);\n    }\n\n    @Override\n    public Converter<ResponseBody, String> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {\n        return value -> {\n            byte[] responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes());\n            if (!TextUtils.isEmpty(encode)) {\n                try {\n                    return new String((responseBytes), Charset.forName(encode));\n                } catch (Exception ignored) {\n                }\n            }\n            String charsetStr;\n            MediaType mediaType = value.contentType();\n            //根据http头判断\n            if (mediaType != null) {\n                Charset charset = mediaType.charset();\n                if (charset != null) {\n                    return new String((responseBytes), charset);\n                }\n            }\n            //根据内容判断\n            charsetStr = EncodingDetect.getEncodeInHtml(responseBytes);\n            return new String(responseBytes, Charset.forName(charsetStr));\n        };\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/ExoPlayerHelper.kt",
    "content": "package com.kunfei.bookshelf.help\n\nimport android.net.Uri\nimport com.google.android.exoplayer2.C\nimport com.google.android.exoplayer2.MediaItem\nimport com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource\nimport com.google.android.exoplayer2.source.MediaSource\nimport com.google.android.exoplayer2.source.ProgressiveMediaSource\nimport com.google.android.exoplayer2.source.dash.DashMediaSource\nimport com.google.android.exoplayer2.source.hls.HlsMediaSource\nimport com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource\nimport com.google.android.exoplayer2.util.Util.inferContentType\nimport com.kunfei.bookshelf.base.BaseModelImpl\n\n\nobject ExoPlayerHelper {\n\n    fun createMediaSource(uri: Uri, overrideExtension: String?): MediaSource {\n        val mediaItem = MediaItem.fromUri(uri)\n        val dataSourceFactory = OkHttpDataSource.Factory(BaseModelImpl.getClient())\n        val mediaSourceFactory = when (inferContentType(uri, overrideExtension)) {\n            C.TYPE_SS -> SsMediaSource.Factory(dataSourceFactory)\n            C.TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory)\n            C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory)\n            else -> ProgressiveMediaSource.Factory(dataSourceFactory)\n        }\n        return mediaSourceFactory.createMediaSource(mediaItem)\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/FileHelp.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.os.Environment;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.utils.IOUtils;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.text.DecimalFormat;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.reactivex.Single;\n\n/**\n * Created by newbiechen on 17-5-11.\n */\n\n@SuppressWarnings(\"ALL\")\npublic class FileHelp {\n    public static final byte BLANK = 0x0a;\n    //采用自己的格式去设置文件，防止文件被系统文件查询到\n    public static final String SUFFIX_NB = \".nb\";\n    public static final String SUFFIX_TXT = \".txt\";\n    public static final String SUFFIX_EPUB = \".epub\";\n    public static final String SUFFIX_PDF = \".pdf\";\n\n    //获取文件夹\n    public static File getFolder(String filePath) {\n        File file = new File(filePath);\n        //如果文件夹不存在，就创建它\n        if (!file.exists()) {\n            file.mkdirs();\n        }\n        return file;\n    }\n\n    //获取文件\n    public static synchronized File createFileIfNotExist(String filePath) {\n        File file = new File(filePath);\n        try {\n            if (!file.exists()) {\n                //创建父类文件夹\n                getFolder(file.getParent());\n                //创建文件\n                file.createNewFile();\n            }\n        } catch (IOException e) {\n        }\n        return file;\n    }\n\n    //获取Cache文件夹\n    public static String getFilesPath() {\n        if (isSdCardExist()) {\n            try {\n                return MApplication.getInstance()\n                        .getExternalFilesDir(null)\n                        .getAbsolutePath();\n            } catch (Exception ignored) {\n            }\n        }\n        return MApplication.getInstance()\n                .getFilesDir()\n                .getAbsolutePath();\n    }\n\n    //获取Cache文件夹\n    public static String getCachePath() {\n        if (isSdCardExist()) {\n            try {\n                return MApplication.getInstance()\n                        .getExternalCacheDir()\n                        .getAbsolutePath();\n            } catch (Exception ignored) {\n            }\n        }\n        return MApplication.getInstance()\n                .getCacheDir()\n                .getAbsolutePath();\n    }\n\n    public static long getDirSize(File file) {\n        //判断文件是否存在\n        if (file.exists()) {\n            //如果是目录则递归计算其内容的总大小\n            if (file.isDirectory()) {\n                File[] children = file.listFiles();\n                long size = 0;\n                for (File f : children)\n                    size += getDirSize(f);\n                return size;\n            } else {\n                return file.length();\n            }\n        } else {\n            return 0;\n        }\n    }\n\n    public static String getFileSize(long size) {\n        if (size <= 0) return \"0\";\n        final String[] units = new String[]{\"b\", \"kb\", \"M\", \"G\", \"T\"};\n        //计算单位的，原理是利用lg,公式是 lg(1024^n) = nlg(1024)，最后 nlg(1024)/lg(1024) = n。\n        int digitGroups = (int) (Math.log10(size) / Math.log10(1024));\n        //计算原理是，size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2\n        return new DecimalFormat(\"#,##0.##\").format(size / Math.pow(1024, digitGroups)) + \" \" + units[digitGroups];\n    }\n\n    /**\n     * 本来是获取File的内容的。但是为了解决文本缩进、换行的问题\n     * 这个方法就是专门用来获取书籍的...\n     * <p>\n     * 应该放在BookRepository中。。。\n     *\n     * @param file\n     * @return\n     */\n    public static String getFileContent(File file) {\n        Reader reader = null;\n        String str = null;\n        StringBuilder sb = new StringBuilder();\n        try {\n            reader = new FileReader(file);\n            BufferedReader br = new BufferedReader(reader);\n            while ((str = br.readLine()) != null) {\n                //过滤空语句\n                if (!str.equals(\"\")) {\n                    //由于sb会自动过滤\\n,所以需要加上去\n                    sb.append(\"    \" + str + \"\\n\");\n                }\n            }\n        } catch (FileNotFoundException e) {\n        } catch (IOException e) {\n        } finally {\n            IOUtils.close(reader);\n        }\n        return sb.toString();\n    }\n\n    //判断是否挂载了SD卡\n    public static boolean isSdCardExist() {\n        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {\n            return true;\n        }\n        return false;\n    }\n\n    //递归删除文件夹下的数据\n    public static synchronized void deleteFile(String filePath) {\n        File file = new File(filePath);\n        if (!file.exists()) return;\n\n        if (file.isDirectory()) {\n            File[] files = file.listFiles();\n            for (File subFile : files) {\n                String path = subFile.getPath();\n                deleteFile(path);\n            }\n        }\n        //删除文件\n        file.delete();\n    }\n\n    //由于递归的耗时问题，取巧只遍历内部三层\n\n    //获取txt文件\n    public static List<File> getTxtFiles(String filePath, int layer) {\n        List<File> txtFiles = new ArrayList<File>();\n        File file = new File(filePath);\n\n        //如果层级为 3，则直接返回\n        if (layer == 3) {\n            return txtFiles;\n        }\n\n        //获取文件夹\n        File[] dirs = file.listFiles(\n                pathname -> {\n                    if (pathname.isDirectory() && !pathname.getName().startsWith(\".\")) {\n                        return true;\n                    }\n                    //获取txt文件\n                    else if (pathname.getName().endsWith(\".txt\")) {\n                        txtFiles.add(pathname);\n                        return false;\n                    } else {\n                        return false;\n                    }\n                }\n        );\n        //遍历文件夹\n        for (File dir : dirs) {\n            //递归遍历txt文件\n            txtFiles.addAll(getTxtFiles(dir.getPath(), layer + 1));\n        }\n        return txtFiles;\n    }\n\n    //由于遍历比较耗时\n    public static Single<List<File>> getSDTxtFile() {\n        //外部存储卡路径\n        String rootPath = Environment.getExternalStorageDirectory().getPath();\n        return Single.create(e -> {\n            List<File> files = getTxtFiles(rootPath, 0);\n            e.onSuccess(files);\n        });\n    }\n\n\n    public static String getFileSuffix(String filePath) {\n        File file = new File(filePath);\n        return getFileSuffix(file);\n    }\n\n    public static String getFileSuffix(File file) {\n        if (file == null || !file.exists() || file.isDirectory()) {\n            return \"\";\n        }\n        String fileName = file.getName();\n        int dotIndex = fileName.lastIndexOf(\".\");\n        return dotIndex > 0 ? fileName.substring(dotIndex) : \"\";\n    }\n\n    public static void createFolderIfNotExists(String path) {\n        File folder = new File(path);\n        if (!folder.exists()) {\n            folder.mkdirs();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/IntentData.kt",
    "content": "package com.kunfei.bookshelf.help\n\nobject IntentData {\n\n    private val bigData: MutableMap<String, Any> = mutableMapOf()\n\n    @Synchronized\n    fun put(key: String, data: Any?) {\n        data?.let {\n            bigData[key] = data\n        }\n    }\n\n    @Synchronized\n    fun put(data: Any?): String {\n        val key = System.currentTimeMillis().toString()\n        data?.let {\n            bigData[key] = data\n        }\n        return key\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    @Synchronized\n    fun <T> get(key: String?): T? {\n        if (key == null) return null\n        val data = bigData[key]\n        bigData.remove(key)\n        return data as? T\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/ItemTouchCallback.java",
    "content": "package com.kunfei.bookshelf.help;\n\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout;\nimport androidx.viewpager.widget.ViewPager;\n\n/**\n * Created by GKF on 2018/3/16.\n */\n\npublic class ItemTouchCallback extends ItemTouchHelper.Callback {\n\n    private SwipeRefreshLayout swipeRefreshLayout;\n    private ViewPager viewPager;\n\n    public void setSwipeRefreshLayout(SwipeRefreshLayout swipeRefreshLayout) {\n        this.swipeRefreshLayout = swipeRefreshLayout;\n    }\n\n    public void setViewPager(ViewPager viewPager) {\n        this.viewPager = viewPager;\n    }\n\n    /**\n     * Item操作的回调\n     */\n    private OnItemTouchCallbackListener onItemTouchCallbackListener;\n\n    /**\n     * 是否可以拖拽\n     */\n    private boolean isCanDrag = false;\n    /**\n     * 是否可以被滑动\n     */\n    private boolean isCanSwipe = false;\n\n    /**\n     * 设置Item操作的回调，去更新UI和数据源\n     */\n    public void setOnItemTouchCallbackListener(OnItemTouchCallbackListener onItemTouchCallbackListener) {\n        this.onItemTouchCallbackListener = onItemTouchCallbackListener;\n    }\n\n    /**\n     * 设置是否可以被拖拽\n     *\n     * @param canDrag 是true，否false\n     */\n    public void setDragEnable(boolean canDrag) {\n        isCanDrag = canDrag;\n    }\n\n    /**\n     * 设置是否可以被滑动\n     *\n     * @param canSwipe 是true，否false\n     */\n    public void setSwipeEnable(boolean canSwipe) {\n        isCanSwipe = canSwipe;\n    }\n\n    /**\n     * 当Item被长按的时候是否可以被拖拽\n     */\n    @Override\n    public boolean isLongPressDragEnabled() {\n        return isCanDrag;\n    }\n\n    /**\n     * Item是否可以被滑动(H：左右滑动，V：上下滑动)\n     */\n    @Override\n    public boolean isItemViewSwipeEnabled() {\n        return isCanSwipe;\n    }\n\n    /**\n     * 当用户拖拽或者滑动Item的时候需要我们告诉系统滑动或者拖拽的方向\n     */\n    @Override\n    public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {\n        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();\n        if (layoutManager instanceof GridLayoutManager) {// GridLayoutManager\n            // flag如果值是0，相当于这个功能被关闭\n            int dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT | ItemTouchHelper.UP | ItemTouchHelper.DOWN;\n            int swipeFlag = 0;\n            // create make\n            return makeMovementFlags(dragFlag, swipeFlag);\n        } else if (layoutManager instanceof LinearLayoutManager) {// linearLayoutManager\n            LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;\n            int orientation = linearLayoutManager.getOrientation();\n\n            int dragFlag = 0;\n            int swipeFlag = 0;\n\n            // 为了方便理解，相当于分为横着的ListView和竖着的ListView\n            if (orientation == LinearLayoutManager.HORIZONTAL) {// 如果是横向的布局\n                swipeFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;\n                dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;\n            } else if (orientation == LinearLayoutManager.VERTICAL) {// 如果是竖向的布局，相当于ListView\n                dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;\n                swipeFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;\n            }\n            return makeMovementFlags(dragFlag, swipeFlag);\n        }\n        return 0;\n    }\n\n    /**\n     * 当Item被拖拽的时候被回调\n     *\n     * @param recyclerView     recyclerView\n     * @param srcViewHolder    拖拽的ViewHolder\n     * @param targetViewHolder 目的地的viewHolder\n     */\n    @Override\n    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder srcViewHolder, @NonNull RecyclerView.ViewHolder targetViewHolder) {\n        if (onItemTouchCallbackListener != null) {\n            return onItemTouchCallbackListener.onMove(srcViewHolder.getAdapterPosition(), targetViewHolder.getAdapterPosition());\n        }\n        return false;\n    }\n\n    @Override\n    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {\n        if (onItemTouchCallbackListener != null) {\n            onItemTouchCallbackListener.onSwiped(viewHolder.getAdapterPosition());\n        }\n    }\n\n    @Override\n    public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {\n        super.onSelectedChanged(viewHolder, actionState);\n        final boolean swiping = actionState == ItemTouchHelper.ACTION_STATE_DRAG;\n        if (swipeRefreshLayout != null) {\n            swipeRefreshLayout.setEnabled(!swiping);\n        }\n        if (viewPager != null) {\n            viewPager.requestDisallowInterceptTouchEvent(swiping);\n        }\n    }\n\n    public interface OnItemTouchCallbackListener {\n        /**\n         * 当某个Item被滑动删除的时候\n         *\n         * @param adapterPosition item的position\n         */\n        void onSwiped(int adapterPosition);\n\n        /**\n         * 当两个Item位置互换的时候被回调\n         *\n         * @param srcPosition    拖拽的item的position\n         * @param targetPosition 目的地的Item的position\n         * @return 开发者处理了操作应该返回true，开发者没有处理就返回false\n         */\n        boolean onMove(int srcPosition, int targetPosition);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/JsExtensions.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.bean.CookieBean;\nimport com.kunfei.bookshelf.constant.AppConst;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl;\nimport com.kunfei.bookshelf.utils.MD5Utils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport org.jsoup.Connection;\nimport org.jsoup.Jsoup;\n\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport retrofit2.Response;\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic interface JsExtensions {\n\n    /**\n     * js实现跨域访问,不能删\n     */\n    default String ajax(String urlStr) {\n        try {\n            AnalyzeUrl analyzeUrl = new AnalyzeUrl(urlStr, AnalyzeHeaders.getDefaultHeader());\n            Response<String> response = BaseModelImpl.getInstance().getResponseO(analyzeUrl)\n                    .blockingFirst();\n            return response.body();\n        } catch (Exception e) {\n            return e.getLocalizedMessage();\n        }\n    }\n\n    /**\n     * js实现跨域访问,不能删\n     */\n    default Response<String> getResponse(String urlStr) {\n        try {\n            AnalyzeUrl analyzeUrl = new AnalyzeUrl(urlStr, AnalyzeHeaders.getDefaultHeader());\n            return BaseModelImpl.getInstance().getResponseO(analyzeUrl)\n                    .blockingFirst();\n        } catch (Exception e) {\n            return Response.success(e.getLocalizedMessage());\n        }\n    }\n\n    /**\n     * js实现解码,不能删\n     */\n    default String base64Decoder(String base64) {\n        return StringUtils.base64Decode(base64);\n    }\n\n    /**\n     * 章节数转数字\n     */\n    default String toNumChapter(String s) {\n        if (s == null) {\n            return null;\n        }\n        Pattern pattern = Pattern.compile(\"(第)(.+?)(章)\");\n        Matcher matcher = pattern.matcher(s);\n        if (matcher.find()) {\n            return matcher.group(1) + StringUtils.stringToInt(matcher.group(2)) + matcher.group(3);\n        }\n        return s;\n    }\n\n    /**\n     * js实现重定向拦截,不能删\n     */\n    default Connection.Response get(String urlStr, Map<String, String> headers) throws IOException {\n        return Jsoup.connect(urlStr)\n                .sslSocketFactory(SSLSocketClient.getSSLSocketFactory())\n                .ignoreContentType(true)\n                .followRedirects(false)\n                .headers(headers)\n                .method(Connection.Method.GET)\n                .execute();\n    }\n\n    /**\n     * js实现重定向拦截,不能删\n     */\n    default Connection.Response post(String urlStr, String body, Map<String, String> headers) throws IOException {\n        return Jsoup.connect(urlStr)\n                .sslSocketFactory(SSLSocketClient.getSSLSocketFactory())\n                .ignoreContentType(true)\n                .followRedirects(false)\n                .requestBody(body)\n                .headers(headers)\n                .method(Connection.Method.POST)\n                .execute();\n    }\n\n    default void putCache(String key, String value) {\n        CookieBean cookie = new CookieBean(key, value);\n        DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(cookie);\n    }\n\n    default String getCache(String key) {\n        CookieBean cookie = DbHelper.getDaoSession().getCookieBeanDao().load(key);\n        if (cookie == null) {\n            return null;\n        }\n        return cookie.getCookie();\n    }\n\n    default String md5Encode(String text) {\n        return MD5Utils.strToMd5By32(text);\n    }\n\n    default String androidId() {\n        return AppConst.INSTANCE.getAndroidId();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/LauncherIcon.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.content.ComponentName;\nimport android.content.pm.PackageManager;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\n\n/**\n * Created by GKF on 2018/2/27.\n * 更换图标\n */\n\npublic class LauncherIcon {\n    private static PackageManager packageManager = MApplication.getInstance().getPackageManager();\n    private static ComponentName componentNameMain = new ComponentName(MApplication.getInstance(), \"com.kunfei.bookshelf.view.activity.WelcomeActivity\");\n    private static ComponentName componentNameBookMain = new ComponentName(MApplication.getInstance(), \"com.kunfei.bookshelf.view.activity.WelcomeBookActivity\");\n\n    public static void ChangeIcon(String icon) {\n\n        if (icon.equals(MApplication.getInstance().getString(R.string.icon_book))) {\n            if (packageManager.getComponentEnabledSetting(componentNameBookMain) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {\n                //启用\n                packageManager.setComponentEnabledSetting(componentNameBookMain,\n                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);\n                //禁用\n                packageManager.setComponentEnabledSetting(componentNameMain,\n                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);\n            }\n        } else {\n            if (packageManager.getComponentEnabledSetting(componentNameMain) != PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {\n                //启用\n                packageManager.setComponentEnabledSetting(componentNameMain,\n                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);\n                //禁用\n                packageManager.setComponentEnabledSetting(componentNameBookMain,\n                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);\n            }\n        }\n    }\n\n    public static String getInUseIcon() {\n        if (packageManager.getComponentEnabledSetting(componentNameBookMain) == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {\n            return MApplication.getInstance().getString(R.string.icon_book);\n        }\n        return MApplication.getInstance().getString(R.string.icon_main);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/MediaManager.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.content.Context;\nimport android.media.AudioManager;\nimport android.media.MediaPlayer;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\n\n/**\n * Created by GKF on 2018/1/9.\n * 播放音频\n */\n\npublic class MediaManager {\n    private static int VOLUME;\n    private AudioManager audioManager;\n    private int stream;\n    private final int FADE_DURATION = 1000;\n    private final int FADE_INTERVAL = 100;\n    private boolean isFading = false;\n    private boolean cancelFading = false;\n\n    public static MediaManager instance;\n\n    private MediaManager() {\n        audioManager = (AudioManager) MApplication.getInstance().getSystemService(Context.AUDIO_SERVICE);\n    }\n\n    public void setStream(int stream) {\n        this.stream = stream;\n        getSysVolume();\n    }\n\n    public static synchronized MediaManager getInstance() {\n        if (instance == null)\n            instance = new MediaManager();\n        return instance;\n    }\n\n    public void fadeInVolume() {\n        if (!isFading) {\n            getSysVolume();\n        } else {\n            cancelFading = true;\n        }\n        while (isFading) try {\n            Thread.sleep(10);\n        } catch (Exception ignored) {\n        }\n        cancelFading = false;\n        startAudioFade(1, VOLUME);\n        setSysVolume(VOLUME);\n    }\n\n    public void fadeOutVolume() {\n        if (!isFading) {\n            getSysVolume();\n        } else {\n            cancelFading = true;\n        }\n        while (isFading) try {\n            Thread.sleep(FADE_INTERVAL);\n        } catch (Exception ignored) {\n        }\n        cancelFading = false;\n        startAudioFade(VOLUME, 1);\n        setSysVolume(VOLUME);\n    }\n\n    private void getSysVolume() {\n        VOLUME = audioManager.getStreamVolume(stream);\n    }\n\n    private void setSysVolume(float vol) {\n        audioManager.setStreamVolume(stream, (int) vol, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);\n    }\n\n    private void startAudioFade(float from, float to) {\n        isFading = true;\n        cancelFading = false;\n        int numberOfSteps = FADE_DURATION / FADE_INTERVAL;\n        float deltaVolume = (to - from) / numberOfSteps;\n        for (float vol = from; (vol - to) * (vol - from) <= 0 && !cancelFading; vol += deltaVolume) {\n            setSysVolume(vol);\n            try {\n                Thread.sleep(FADE_INTERVAL);\n            } catch (Exception ignored) {\n            }\n        }\n        isFading = false;\n        cancelFading = false;\n    }\n\n    public static void playSilentSound(Context mContext) {\n        try {\n            // Stupid Android 8 \"Oreo\" hack to make media buttons work\n            MediaPlayer mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent_sound);\n            mMediaPlayer.setOnCompletionListener(MediaPlayer::release);\n            mMediaPlayer.start();\n        } catch (Exception ignored) {\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/ProcessTextHelp.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.content.ComponentName;\nimport android.content.pm.PackageManager;\n\nimport com.kunfei.bookshelf.MApplication;\n\npublic class ProcessTextHelp {\n\n    private static PackageManager packageManager = MApplication.getInstance().getPackageManager();\n    private static ComponentName componentName = new ComponentName(MApplication.getInstance(), \"com.kunfei.bookshelf.view.activity.ReceivingSharedActivity\");\n\n    public static boolean isProcessTextEnabled() {\n        return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED;\n    }\n\n    public static void setProcessTextEnable(boolean enable) {\n        if (enable) {\n            packageManager.setComponentEnabledSetting(componentName,\n                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);\n        } else {\n            packageManager.setComponentEnabledSetting(componentName,\n                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/ReadBookControl.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.help;\n\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.content.res.Resources;\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.provider.Settings;\nimport android.util.DisplayMetrics;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.utils.BitmapUtil;\nimport com.kunfei.bookshelf.utils.MeUtils;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport static com.kunfei.bookshelf.widget.page.PageLoader.DEFAULT_MARGIN_WIDTH;\n\npublic class ReadBookControl {\n    private static final int DEFAULT_BG = 1;\n    private int textDrawableIndex = DEFAULT_BG;\n    private List<Map<String, Integer>> textDrawable;\n    private Bitmap bgBitmap;\n    private int screenDirection;\n    private int speechRate;\n    private boolean speechRateFollowSys;\n    private int textSize;\n    private int textColor;\n    private boolean bgIsColor;\n    private int bgColor;\n    private float lineMultiplier;\n    private float paragraphSize;\n    private int pageMode;\n    private Boolean lightNovelParagraph;\n    private Boolean hideStatusBar;\n    private Boolean hideNavigationBar;\n    private String fontPath;\n    private int textConvert;\n    private int navBarColor;\n    private Boolean textBold;\n    private Boolean canClickTurn;\n    private Boolean canKeyTurn;\n    private Boolean readAloudCanKeyTurn;\n    private int CPM;\n    private Boolean clickAllNext;\n    private Boolean showTitle;\n    private Boolean showTimeBattery;\n    private Boolean showLine;\n    private Boolean darkStatusIcon;\n    private int indent;\n    private int screenTimeOut;\n    private int paddingLeft;\n    private int paddingTop;\n    private int paddingRight;\n    private int paddingBottom;\n    private int tipPaddingLeft;\n    private int tipPaddingTop;\n    private int tipPaddingRight;\n    private int tipPaddingBottom;\n    private float textLetterSpacing;\n    private boolean canSelectText;\n    public int minCPM = 200;\n    public int maxCPM = 2000;\n    private int defaultCPM = 500;\n\n    private SharedPreferences preferences;\n\n    private static ReadBookControl readBookControl;\n\n    public static ReadBookControl getInstance() {\n        if (readBookControl == null) {\n            synchronized (ReadBookControl.class) {\n                if (readBookControl == null) {\n                    readBookControl = new ReadBookControl();\n                }\n            }\n        }\n        return readBookControl;\n    }\n\n    private ReadBookControl() {\n        preferences = MApplication.getConfigPreferences();\n        initTextDrawable();\n        updateReaderSettings();\n    }\n\n    public void updateReaderSettings() {\n        this.lightNovelParagraph = preferences.getBoolean(\"light_novel_paragraph\", false);\n        this.hideStatusBar = preferences.getBoolean(\"hide_status_bar\", false);\n        this.hideNavigationBar = preferences.getBoolean(\"hide_navigation_bar\", false);\n        this.indent = preferences.getInt(\"indent\", 2);\n        this.textSize = preferences.getInt(\"textSize\", 20);\n        this.canClickTurn = preferences.getBoolean(\"canClickTurn\", true);\n        this.canKeyTurn = preferences.getBoolean(\"canKeyTurn\", true);\n        this.readAloudCanKeyTurn = preferences.getBoolean(\"readAloudCanKeyTurn\", false);\n        this.lineMultiplier = preferences.getFloat(\"lineMultiplier\", 1);\n        this.paragraphSize = preferences.getFloat(\"paragraphSize\", 1);\n        this.CPM = preferences.getInt(\"CPM\", defaultCPM) > maxCPM\n                ? minCPM : preferences.getInt(\"CPM\", defaultCPM);\n        this.clickAllNext = preferences.getBoolean(\"clickAllNext\", false);\n        this.fontPath = preferences.getString(\"fontPath\", null);\n        this.textConvert = preferences.getInt(\"textConvertInt\", 0);\n        this.textBold = preferences.getBoolean(\"textBold\", false);\n        this.speechRate = preferences.getInt(\"speechRate\", 10);\n        this.speechRateFollowSys = preferences.getBoolean(\"speechRateFollowSys\", true);\n        this.showTitle = preferences.getBoolean(\"showTitle\", true);\n        this.showTimeBattery = preferences.getBoolean(\"showTimeBattery\", true);\n        this.showLine = preferences.getBoolean(\"showLine\", true);\n        this.screenTimeOut = preferences.getInt(\"screenTimeOut\", 0);\n        this.paddingLeft = preferences.getInt(\"paddingLeft\", DEFAULT_MARGIN_WIDTH);\n        this.paddingTop = preferences.getInt(\"paddingTop\", 0);\n        this.paddingRight = preferences.getInt(\"paddingRight\", DEFAULT_MARGIN_WIDTH);\n        this.paddingBottom = preferences.getInt(\"paddingBottom\", 0);\n        this.tipPaddingLeft = preferences.getInt(\"tipPaddingLeft\", DEFAULT_MARGIN_WIDTH);\n        this.tipPaddingTop = preferences.getInt(\"tipPaddingTop\", 0);\n        this.tipPaddingRight = preferences.getInt(\"tipPaddingRight\", DEFAULT_MARGIN_WIDTH);\n        this.tipPaddingBottom = preferences.getInt(\"tipPaddingBottom\", 0);\n        this.pageMode = preferences.getInt(\"pageMode\", 0);\n        this.screenDirection = preferences.getInt(\"screenDirection\", 0);\n        this.navBarColor = preferences.getInt(\"navBarColorInt\", 0);\n        this.textLetterSpacing = preferences.getFloat(\"textLetterSpacing\", 0);\n        this.canSelectText = preferences.getBoolean(\"canSelectText\", false);\n        initTextDrawableIndex();\n    }\n\n    //阅读背景\n    private void initTextDrawable() {\n        if (null == textDrawable) {\n            textDrawable = new ArrayList<>();\n            Map<String, Integer> temp1 = new HashMap<>();\n            temp1.put(\"textColor\", Color.parseColor(\"#3E3D3B\"));\n            temp1.put(\"bgIsColor\", 1);\n            temp1.put(\"textBackground\", Color.parseColor(\"#F3F3F3\"));\n            temp1.put(\"darkStatusIcon\", 1);\n            textDrawable.add(temp1);\n\n            Map<String, Integer> temp2 = new HashMap<>();\n            temp2.put(\"textColor\", Color.parseColor(\"#5E432E\"));\n            temp2.put(\"bgIsColor\", 1);\n            temp2.put(\"textBackground\", Color.parseColor(\"#C6BAA1\"));\n            temp2.put(\"darkStatusIcon\", 1);\n            textDrawable.add(temp2);\n\n            Map<String, Integer> temp3 = new HashMap<>();\n            temp3.put(\"textColor\", Color.parseColor(\"#22482C\"));\n            temp3.put(\"bgIsColor\", 1);\n            temp3.put(\"textBackground\", Color.parseColor(\"#E1F1DA\"));\n            temp3.put(\"darkStatusIcon\", 1);\n            textDrawable.add(temp3);\n\n            Map<String, Integer> temp4 = new HashMap<>();\n            temp4.put(\"textColor\", Color.parseColor(\"#FFFFFF\"));\n            temp4.put(\"bgIsColor\", 1);\n            temp4.put(\"textBackground\", Color.parseColor(\"#015A86\"));\n            temp4.put(\"darkStatusIcon\", 0);\n            textDrawable.add(temp4);\n\n            Map<String, Integer> temp5 = new HashMap<>();\n            temp5.put(\"textColor\", Color.parseColor(\"#808080\"));\n            temp5.put(\"bgIsColor\", 1);\n            temp5.put(\"textBackground\", Color.parseColor(\"#000000\"));\n            temp5.put(\"darkStatusIcon\", 0);\n            textDrawable.add(temp5);\n        }\n    }\n\n    public void initTextDrawableIndex() {\n        if (getIsNightTheme()) {\n            textDrawableIndex = preferences.getInt(\"textDrawableIndexNight\", 4);\n        } else {\n            textDrawableIndex = preferences.getInt(\"textDrawableIndex\", DEFAULT_BG);\n        }\n        if (textDrawableIndex == -1) {\n            textDrawableIndex = DEFAULT_BG;\n        }\n        initPageStyle();\n        setTextDrawable();\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    private void initPageStyle() {\n        int bgCustom = getBgCustom(textDrawableIndex);\n        if ((bgCustom == 2 || bgCustom == 3) && getBgPath(textDrawableIndex) != null) {\n            bgIsColor = false;\n            String bgPath = getBgPath(textDrawableIndex);\n            Resources resources = MApplication.getInstance().getResources();\n            DisplayMetrics dm = resources.getDisplayMetrics();\n            int width = dm.widthPixels;\n            int height = dm.heightPixels;\n            if (bgCustom == 2) {\n                bgBitmap = BitmapUtil.getFitSampleBitmap(bgPath, width, height);\n            } else {\n                bgBitmap = MeUtils.getFitAssetsSampleBitmap(MApplication.getInstance().getAssets(), bgPath, width, height);\n            }\n            if (bgBitmap != null) {\n                return;\n            }\n        } else if (getBgCustom(textDrawableIndex) == 1) {\n            bgIsColor = true;\n            bgColor = getBgColor(textDrawableIndex);\n            return;\n        }\n        bgIsColor = true;\n        bgColor = textDrawable.get(textDrawableIndex).get(\"textBackground\");\n    }\n\n    private void setTextDrawable() {\n        darkStatusIcon = getDarkStatusIcon(textDrawableIndex);\n        textColor = getTextColor(textDrawableIndex);\n    }\n\n    public int getTextColor(int textDrawableIndex) {\n        if (preferences.getInt(\"textColor\" + textDrawableIndex, 0) != 0) {\n            return preferences.getInt(\"textColor\" + textDrawableIndex, 0);\n        } else {\n            return getDefaultTextColor(textDrawableIndex);\n        }\n    }\n\n    public void setTextColor(int textDrawableIndex, int textColor) {\n        preferences.edit()\n                .putInt(\"textColor\" + textDrawableIndex, textColor)\n                .apply();\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    public Drawable getBgDrawable(int textDrawableIndex, Context context, int width, int height) {\n        int color;\n        try {\n            Bitmap bitmap = null;\n            switch (getBgCustom(textDrawableIndex)) {\n                case 3:\n                    bitmap = MeUtils.getFitAssetsSampleBitmap(context.getAssets(), getBgPath(textDrawableIndex), width, height);\n                    if (bitmap != null) {\n                        return new BitmapDrawable(context.getResources(), bitmap);\n                    }\n                case 2:\n                    bitmap = BitmapUtil.getFitSampleBitmap(getBgPath(textDrawableIndex), width, height);\n                    if (bitmap != null) {\n                        return new BitmapDrawable(context.getResources(), bitmap);\n                    }\n                    break;\n                case 1:\n                    color = getBgColor(textDrawableIndex);\n                    return new ColorDrawable(color);\n            }\n            if (textDrawable.get(textDrawableIndex).get(\"bgIsColor\") != 0) {\n                color = textDrawable.get(textDrawableIndex).get(\"textBackground\");\n                return new ColorDrawable(color);\n            } else {\n                return getDefaultBgDrawable(textDrawableIndex, context);\n            }\n        } catch (Exception e) {\n            if (textDrawable.get(textDrawableIndex).get(\"bgIsColor\") != 0) {\n                color = textDrawable.get(textDrawableIndex).get(\"textBackground\");\n                return new ColorDrawable(color);\n            } else {\n                return getDefaultBgDrawable(textDrawableIndex, context);\n            }\n        }\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    public Drawable getDefaultBgDrawable(int textDrawableIndex, Context context) {\n        if (textDrawable.get(textDrawableIndex).get(\"bgIsColor\") != 0) {\n            return new ColorDrawable(textDrawable.get(textDrawableIndex).get(\"textBackground\"));\n        } else {\n            return context.getResources().getDrawable(getDefaultBg(textDrawableIndex));\n        }\n    }\n\n    public int getBgCustom(int textDrawableIndex) {\n        return preferences.getInt(\"bgCustom\" + textDrawableIndex, 0);\n    }\n\n    public void setBgCustom(int textDrawableIndex, int bgCustom) {\n        preferences.edit()\n                .putInt(\"bgCustom\" + textDrawableIndex, bgCustom)\n                .apply();\n    }\n\n    public String getBgPath(int textDrawableIndex) {\n        return preferences.getString(\"bgPath\" + textDrawableIndex, null);\n    }\n\n    public void setBgPath(int textDrawableIndex, String bgUri) {\n        preferences.edit()\n                .putString(\"bgPath\" + textDrawableIndex, bgUri)\n                .apply();\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    public int getDefaultTextColor(int textDrawableIndex) {\n        return textDrawable.get(textDrawableIndex).get(\"textColor\");\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    private int getDefaultBg(int textDrawableIndex) {\n        return textDrawable.get(textDrawableIndex).get(\"textBackground\");\n    }\n\n    public int getBgColor(int index) {\n        return preferences.getInt(\"bgColor\" + index, Color.parseColor(\"#1e1e1e\"));\n    }\n\n    public void setBgColor(int index, int bgColor) {\n        preferences.edit()\n                .putInt(\"bgColor\" + index, bgColor)\n                .apply();\n    }\n\n    private boolean getIsNightTheme() {\n        return MApplication.getInstance().isNightTheme();\n    }\n\n    public boolean getImmersionStatusBar() {\n        return preferences.getBoolean(\"immersionStatusBar\", false);\n    }\n\n    public void setImmersionStatusBar(boolean immersionStatusBar) {\n        preferences.edit()\n                .putBoolean(\"immersionStatusBar\", immersionStatusBar)\n                .apply();\n    }\n\n    public int getTextSize() {\n        return textSize;\n    }\n\n    public void setTextSize(int textSize) {\n        this.textSize = textSize;\n        preferences.edit()\n                .putInt(\"textSize\", textSize)\n                .apply();\n    }\n\n    public int getTextColor() {\n        return textColor;\n    }\n\n    public boolean bgIsColor() {\n        return bgIsColor;\n    }\n\n    public Drawable getTextBackground(Context context) {\n        if (bgIsColor) {\n            return new ColorDrawable(bgColor);\n        }\n        return new BitmapDrawable(context.getResources(), bgBitmap);\n    }\n\n    public int getBgColor() {\n        return bgColor;\n    }\n\n    public boolean bgBitmapIsNull() {\n        return bgBitmap == null || bgBitmap.isRecycled();\n    }\n\n    public Bitmap getBgBitmap() {\n        return bgBitmap.copy(Bitmap.Config.ARGB_8888, true);\n    }\n\n    public int getTextDrawableIndex() {\n        return textDrawableIndex;\n    }\n\n    public void setTextDrawableIndex(int textDrawableIndex) {\n        this.textDrawableIndex = textDrawableIndex;\n        if (getIsNightTheme()) {\n            preferences.edit()\n                    .putInt(\"textDrawableIndexNight\", textDrawableIndex)\n                    .apply();\n        } else {\n            preferences.edit()\n                    .putInt(\"textDrawableIndex\", textDrawableIndex)\n                    .apply();\n        }\n        setTextDrawable();\n    }\n\n    public void setTextConvert(int textConvert) {\n        this.textConvert = textConvert;\n        preferences.edit()\n                .putInt(\"textConvertInt\", textConvert)\n                .apply();\n    }\n\n    public void setNavBarColor(int navBarColor) {\n        this.navBarColor = navBarColor;\n        preferences.edit()\n                .putInt(\"navBarColorInt\", navBarColor)\n                .apply();\n    }\n\n    public int getNavBarColor() {\n        return navBarColor;\n    }\n\n\n    public void setTextBold(boolean textBold) {\n        this.textBold = textBold;\n        preferences.edit()\n                .putBoolean(\"textBold\", textBold)\n                .apply();\n    }\n\n    public void setReadBookFont(String fontPath) {\n        this.fontPath = fontPath;\n        preferences.edit()\n                .putString(\"fontPath\", fontPath)\n                .apply();\n    }\n\n    public String getFontPath() {\n        return fontPath;\n    }\n\n    public int getTextConvert() {\n        return textConvert == -1 ? 2 : textConvert;\n    }\n\n    public Boolean getTextBold() {\n        return textBold;\n    }\n\n    public Boolean getCanKeyTurn(Boolean isPlay) {\n        if (!canKeyTurn) {\n            return false;\n        } else if (readAloudCanKeyTurn) {\n            return true;\n        } else {\n            return !isPlay;\n        }\n    }\n\n    public Boolean getCanKeyTurn() {\n        return canKeyTurn;\n    }\n\n    public void setCanKeyTurn(Boolean canKeyTurn) {\n        this.canKeyTurn = canKeyTurn;\n        preferences.edit()\n                .putBoolean(\"canKeyTurn\", canKeyTurn)\n                .apply();\n    }\n\n    public Boolean getAloudCanKeyTurn() {\n        return readAloudCanKeyTurn;\n    }\n\n    public void setAloudCanKeyTurn(Boolean canAloudKeyTurn) {\n        this.readAloudCanKeyTurn = canAloudKeyTurn;\n        preferences.edit()\n                .putBoolean(\"readAloudCanKeyTurn\", canAloudKeyTurn)\n                .apply();\n    }\n\n    public Boolean getCanClickTurn() {\n        return canClickTurn;\n    }\n\n    public void setCanClickTurn(Boolean canClickTurn) {\n        this.canClickTurn = canClickTurn;\n        preferences.edit()\n                .putBoolean(\"canClickTurn\", canClickTurn)\n                .apply();\n    }\n\n    public float getTextLetterSpacing() {\n        return textLetterSpacing;\n    }\n\n    public void setTextLetterSpacing(float textLetterSpacing) {\n        this.textLetterSpacing = textLetterSpacing;\n        preferences.edit()\n                .putFloat(\"textLetterSpacing\", textLetterSpacing)\n                .apply();\n    }\n\n    public float getLineMultiplier() {\n        return lineMultiplier;\n    }\n\n    public void setLineMultiplier(float lineMultiplier) {\n        this.lineMultiplier = lineMultiplier;\n        preferences.edit()\n                .putFloat(\"lineMultiplier\", lineMultiplier)\n                .apply();\n    }\n\n    public float getParagraphSize() {\n        return paragraphSize;\n    }\n\n    public void setParagraphSize(float paragraphSize) {\n        this.paragraphSize = paragraphSize;\n        preferences.edit()\n                .putFloat(\"paragraphSize\", paragraphSize)\n                .apply();\n    }\n\n    public int getCPM() {\n        return CPM;\n    }\n\n    public void setCPM(int cpm) {\n        if (cpm < minCPM || cpm > maxCPM) cpm = defaultCPM;\n        this.CPM = cpm;\n        preferences.edit()\n                .putInt(\"CPM\", cpm)\n                .apply();\n    }\n\n    public Boolean getClickAllNext() {\n        return clickAllNext;\n    }\n\n    public void setClickAllNext(Boolean clickAllNext) {\n        this.clickAllNext = clickAllNext;\n        preferences.edit()\n                .putBoolean(\"clickAllNext\", clickAllNext)\n                .apply();\n    }\n\n    public int getSpeechRate() {\n        return speechRate;\n    }\n\n    public void setSpeechRate(int speechRate) {\n        this.speechRate = speechRate;\n        preferences.edit()\n                .putInt(\"speechRate\", speechRate)\n                .apply();\n    }\n\n    public boolean isSpeechRateFollowSys() {\n        return speechRateFollowSys;\n    }\n\n    public void setSpeechRateFollowSys(boolean speechRateFollowSys) {\n        this.speechRateFollowSys = speechRateFollowSys;\n        preferences.edit()\n                .putBoolean(\"speechRateFollowSys\", speechRateFollowSys)\n                .apply();\n    }\n\n    public Boolean getShowTitle() {\n        return showTitle;\n    }\n\n    public void setShowTitle(Boolean showTitle) {\n        this.showTitle = showTitle;\n        preferences.edit()\n                .putBoolean(\"showTitle\", showTitle)\n                .apply();\n    }\n\n    public Boolean getShowTimeBattery() {\n        return showTimeBattery;\n    }\n\n    public void setShowTimeBattery(Boolean showTimeBattery) {\n        this.showTimeBattery = showTimeBattery;\n        preferences.edit()\n                .putBoolean(\"showTimeBattery\", showTimeBattery)\n                .apply();\n    }\n\n    public Boolean getLightNovelParagraph(){return lightNovelParagraph;}\n\n    public void setLightNovelParagraph(Boolean lightNovelParagraph) {\n        this.lightNovelParagraph = lightNovelParagraph;\n        preferences.edit()\n                .putBoolean(\"light_novel_paragraph\", lightNovelParagraph)\n                .apply();\n    }\n\n    public Boolean getHideStatusBar() {\n        return hideStatusBar;\n    }\n\n    public void setHideStatusBar(Boolean hideStatusBar) {\n        this.hideStatusBar = hideStatusBar;\n        preferences.edit()\n                .putBoolean(\"hide_status_bar\", hideStatusBar)\n                .apply();\n    }\n\n    public Boolean getToLh() {\n        return preferences.getBoolean(\"toLh\", false);\n    }\n\n    public void setToLh(Boolean toLh) {\n        preferences.edit()\n                .putBoolean(\"toLh\", toLh)\n                .apply();\n    }\n\n    public Boolean getHideNavigationBar() {\n        return hideNavigationBar;\n    }\n\n    public void setHideNavigationBar(Boolean hideNavigationBar) {\n        this.hideNavigationBar = hideNavigationBar;\n        preferences.edit()\n                .putBoolean(\"hide_navigation_bar\", hideNavigationBar)\n                .apply();\n    }\n\n    public Boolean getShowLine() {\n        return showLine;\n    }\n\n    public void setShowLine(Boolean showLine) {\n        this.showLine = showLine;\n        preferences.edit()\n                .putBoolean(\"showLine\", showLine)\n                .apply();\n    }\n\n    public boolean getDarkStatusIcon() {\n        return darkStatusIcon;\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    public boolean getDarkStatusIcon(int textDrawableIndex) {\n        return preferences.getBoolean(\"darkStatusIcon\" + textDrawableIndex, textDrawable.get(textDrawableIndex).get(\"darkStatusIcon\") != 0);\n    }\n\n    public void setDarkStatusIcon(int textDrawableIndex, Boolean darkStatusIcon) {\n        preferences.edit()\n                .putBoolean(\"darkStatusIcon\" + textDrawableIndex, darkStatusIcon)\n                .apply();\n    }\n\n    public int getScreenTimeOut() {\n        return screenTimeOut;\n    }\n\n    public void setScreenTimeOut(int screenTimeOut) {\n        this.screenTimeOut = screenTimeOut;\n        preferences.edit()\n                .putInt(\"screenTimeOut\", screenTimeOut)\n                .apply();\n    }\n\n    public int getPaddingLeft() {\n        return paddingLeft;\n    }\n\n    public void setPaddingLeft(int paddingLeft) {\n        this.paddingLeft = paddingLeft;\n        preferences.edit()\n                .putInt(\"paddingLeft\", paddingLeft)\n                .apply();\n    }\n\n    public int getPaddingTop() {\n        return paddingTop;\n    }\n\n    public void setPaddingTop(int paddingTop) {\n        this.paddingTop = paddingTop;\n        preferences.edit()\n                .putInt(\"paddingTop\", paddingTop)\n                .apply();\n    }\n\n    public int getPaddingRight() {\n        return paddingRight;\n    }\n\n    public void setPaddingRight(int paddingRight) {\n        this.paddingRight = paddingRight;\n        preferences.edit()\n                .putInt(\"paddingRight\", paddingRight)\n                .apply();\n    }\n\n    public int getPaddingBottom() {\n        return paddingBottom;\n    }\n\n    public void setPaddingBottom(int paddingBottom) {\n        this.paddingBottom = paddingBottom;\n        preferences.edit()\n                .putInt(\"paddingBottom\", paddingBottom)\n                .apply();\n    }\n\n    public int getTipPaddingLeft() {\n        return tipPaddingLeft;\n    }\n\n    public void setTipPaddingLeft(int tipPaddingLeft) {\n        this.tipPaddingLeft = tipPaddingLeft;\n        preferences.edit()\n                .putInt(\"tipPaddingLeft\", tipPaddingLeft)\n                .apply();\n    }\n\n    public boolean isCanSelectText() {\n        return canSelectText;\n    }\n\n    public void setCanSelectText(boolean canSelectText) {\n        this.canSelectText = canSelectText;\n        preferences.edit()\n                .putBoolean(\"canSelectText\", canSelectText)\n                .apply();\n    }\n\n    public int getTipPaddingTop() {\n        return tipPaddingTop;\n    }\n\n    public void setTipPaddingTop(int tipPaddingTop) {\n        this.tipPaddingTop = tipPaddingTop;\n        preferences.edit()\n                .putInt(\"tipPaddingTop\", tipPaddingTop)\n                .apply();\n    }\n\n    public int getTipPaddingRight() {\n        return tipPaddingRight;\n    }\n\n    public void setTipPaddingRight(int tipPaddingRight) {\n        this.tipPaddingRight = tipPaddingRight;\n        preferences.edit()\n                .putInt(\"tipPaddingRight\", tipPaddingRight)\n                .apply();\n    }\n\n    public int getTipPaddingBottom() {\n        return tipPaddingBottom;\n    }\n\n    public void setTipPaddingBottom(int tipPaddingBottom) {\n        this.tipPaddingBottom = tipPaddingBottom;\n        preferences.edit()\n                .putInt(\"tipPaddingBottom\", tipPaddingBottom)\n                .apply();\n    }\n\n    public int getPageMode() {\n        return pageMode;\n    }\n\n    public void setPageMode(int pageMode) {\n        this.pageMode = pageMode;\n        preferences.edit()\n                .putInt(\"pageMode\", pageMode)\n                .apply();\n    }\n\n    public int getScreenDirection() {\n        return screenDirection;\n    }\n\n    public void setScreenDirection(int screenDirection) {\n        this.screenDirection = screenDirection;\n        preferences.edit()\n                .putInt(\"screenDirection\", screenDirection)\n                .apply();\n    }\n\n    public void setIndent(int indent) {\n        this.indent = indent;\n        preferences.edit()\n                .putInt(\"indent\", indent)\n                .apply();\n    }\n\n    public int getIndent() {\n        return indent;\n    }\n\n    public int getLight() {\n        return preferences.getInt(\"light\", getScreenBrightness());\n    }\n\n    public void setLight(int light) {\n        preferences.edit()\n                .putInt(\"light\", light)\n                .apply();\n    }\n\n    public Boolean getLightFollowSys() {\n        return preferences.getBoolean(\"lightFollowSys\", true);\n    }\n\n    public void setLightFollowSys(boolean isFollowSys) {\n        preferences.edit()\n                .putBoolean(\"lightFollowSys\", isFollowSys)\n                .apply();\n    }\n\n    private int getScreenBrightness() {\n        int value = 0;\n        ContentResolver cr = MApplication.getInstance().getContentResolver();\n        try {\n            value = Settings.System.getInt(cr, Settings.System.SCREEN_BRIGHTNESS);\n        } catch (Settings.SettingNotFoundException ignored) {\n        }\n        return value;\n    }\n\n    public boolean disableScrollClickTurn() {\n        return preferences.getBoolean(\"disableScrollClickTurn\", false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/SSLSocketClient.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport android.annotation.SuppressLint;\n\nimport java.security.SecureRandom;\nimport java.security.cert.X509Certificate;\n\nimport javax.net.ssl.HostnameVerifier;\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLSocketFactory;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\n\n/**\n * Created by GKF on 2018/3/1.\n * 忽略证书\n */\n\npublic class SSLSocketClient {\n    //获取这个SSLSocketFactory\n    public static SSLSocketFactory getSSLSocketFactory() {\n        try {\n            SSLContext sslContext = SSLContext.getInstance(\"SSL\");\n            sslContext.init(null, new TrustManager[]{createTrustAllManager()}, new SecureRandom());\n            return sslContext.getSocketFactory();\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static X509TrustManager createTrustAllManager() {\n        X509TrustManager tm = null;\n        try {\n            tm = new X509TrustManager() {\n                @SuppressLint(\"TrustAllX509TrustManager\")\n                public void checkClientTrusted(X509Certificate[] chain, String authType) {\n                    //do nothing，接受任意客户端证书\n                }\n\n                @SuppressLint(\"TrustAllX509TrustManager\")\n                public void checkServerTrusted(X509Certificate[] chain, String authType) {\n                    //do nothing，接受任意服务端证书\n                }\n\n                public X509Certificate[] getAcceptedIssuers() {\n                    return new X509Certificate[0];\n                }\n            };\n        } catch (Exception ignored) {\n        }\n        return tm;\n    }\n\n    //获取HostnameVerifier\n    public static HostnameVerifier getHostnameVerifier() {\n        return (s, sslSession) -> true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/SourceHelp.kt",
    "content": "package com.kunfei.bookshelf.help\n\nimport android.os.Handler\nimport android.os.Looper\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.bean.BookSourceBean\nimport com.kunfei.bookshelf.model.BookSourceManager\nimport com.kunfei.bookshelf.utils.EncoderUtils\nimport com.kunfei.bookshelf.utils.splitNotBlank\nimport org.jetbrains.anko.toast\n\nobject SourceHelp {\n\n    private val handler = Handler(Looper.getMainLooper())\n    private val list18Plus by lazy {\n        try {\n            return@lazy String(MApplication.getInstance().assets.open(\"18PlusList.txt\").readBytes())\n                    .splitNotBlank(\"\\n\")\n        } catch (e: Exception) {\n            return@lazy arrayOf<String>()\n        }\n    }\n\n    fun insertBookSource(vararg bookSources: BookSourceBean) {\n        bookSources.forEach { bookSource ->\n            if (is18Plus(bookSource.bookSourceUrl)) {\n                handler.post {\n                    MApplication.getInstance().toast(\"${bookSource.bookSourceName}是18+网址,禁止导入.\")\n                }\n            } else {\n                BookSourceManager.addBookSource(bookSource)\n            }\n        }\n    }\n\n    private fun is18Plus(url: String?): Boolean {\n        url ?: return false\n        val baseUrl = getBaseUrl(url)\n        baseUrl ?: return false\n        try {\n            val host = baseUrl.split(\"//\", \".\")\n            val base64Url = EncoderUtils.base64Encode(\"${host[host.lastIndex - 1]}.${host.last()}\")\n            list18Plus.forEach {\n                if (base64Url == it) {\n                    return true\n                }\n            }\n        } catch (e: Exception) {\n        }\n        return false\n    }\n\n    fun getBaseUrl(url: String?): String? {\n        if (url == null || !url.startsWith(\"http\")) return null\n        val index = url.indexOf(\"/\", 9)\n        return if (index == -1) {\n            url\n        } else url.substring(0, index)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/UTF8BOMFighter.java",
    "content": "package com.kunfei.bookshelf.help;\n\npublic class UTF8BOMFighter {\n    private static final byte[] UTF8_BOM_BYTES = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};\n\n    private UTF8BOMFighter() {\n    }\n\n    static public String removeUTF8BOM(String xmlText) {\n        byte[] bytes = xmlText.getBytes();\n        boolean containsBOM = bytes.length > 3\n                && bytes[0] == UTF8_BOM_BYTES[0]\n                && bytes[1] == UTF8_BOM_BYTES[1]\n                && bytes[2] == UTF8_BOM_BYTES[2];\n        if (containsBOM) {\n            xmlText = new String(bytes, 3, bytes.length - 3);\n        }\n        return xmlText;\n    }\n\n    static public byte[] removeUTF8BOM(byte[] bytes) {\n        boolean containsBOM = bytes.length > 3\n                && bytes[0] == UTF8_BOM_BYTES[0]\n                && bytes[1] == UTF8_BOM_BYTES[1]\n                && bytes[2] == UTF8_BOM_BYTES[2];\n        if (containsBOM) {\n            byte[] copy = new byte[bytes.length - 3];\n            System.arraycopy(bytes, 3, copy, 0, bytes.length - 3);\n            return copy;\n        }\n        return bytes;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/UpdateManager.java",
    "content": "package com.kunfei.bookshelf.help;\n\nimport static android.content.Context.DOWNLOAD_SERVICE;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.util.Log;\nimport android.widget.Toast;\n\nimport androidx.core.content.FileProvider;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.kunfei.bookshelf.BuildConfig;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.UpdateInfoBean;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders;\nimport com.kunfei.bookshelf.model.impl.IHttpGetApi;\n\nimport java.io.File;\n\nimport io.reactivex.Observable;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class UpdateManager {\n    private Activity activity;\n\n    public static UpdateManager getInstance(Activity activity) {\n        return new UpdateManager(activity);\n    }\n\n    private UpdateManager(Activity activity) {\n        this.activity = activity;\n    }\n\n    public void checkUpdate(boolean showMsg) {\n        BaseModelImpl.getInstance().getRetrofitString(\"https://api.github.com\")\n                .create(IHttpGetApi.class)\n                .get(MApplication.getInstance().getString(R.string.latest_release_api), AnalyzeHeaders.getDefaultHeader())\n                .flatMap(response -> analyzeLastReleaseApi(response.body()))\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<UpdateInfoBean>() {\n                    @Override\n                    public void onNext(UpdateInfoBean updateInfo) {\n                        if (updateInfo.getUpDate()) {\n\n                        } else if (showMsg) {\n                            Toast.makeText(activity, \"已是最新版本\", Toast.LENGTH_SHORT).show();\n\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        if (showMsg) {\n                            Toast.makeText(activity, \"检测新版本出错\", Toast.LENGTH_SHORT).show();\n                        }\n                    }\n                });\n    }\n\n    private Observable<UpdateInfoBean> analyzeLastReleaseApi(String jsonStr) {\n        return Observable.create(emitter -> {\n            try {\n                UpdateInfoBean updateInfo = new UpdateInfoBean();\n                JsonObject version = new JsonParser().parse(jsonStr).getAsJsonObject();\n                if (version.get(\"prerelease\").getAsBoolean())\n                    return;\n                JsonArray assets = version.get(\"assets\").getAsJsonArray();\n                if (assets.size() > 0) {\n                    String lastVersion = version.get(\"tag_name\").getAsString();\n                    String url = assets.get(0).getAsJsonObject().get(\"browser_download_url\").getAsString();\n                    String detail = version.get(\"body\").getAsString();\n                    String thisVersion = MApplication.getVersionName().split(\"\\\\s\")[0];\n                    updateInfo.setUrl(url);\n                    updateInfo.setLastVersion(lastVersion);\n                    updateInfo.setDetail(\"# \" + lastVersion + \"\\n\" + detail);\n                    if (Integer.valueOf(lastVersion.split(\"\\\\.\")[2]) > Integer.valueOf(thisVersion.split(\"\\\\.\")[2])) {\n                        updateInfo.setUpDate(true);\n                    } else {\n                        updateInfo.setUpDate(false);\n                    }\n                }\n                emitter.onNext(updateInfo);\n                emitter.onComplete();\n            } catch (Exception e) {\n                emitter.onError(e);\n                emitter.onComplete();\n            }\n        });\n    }\n\n    /**\n     * 安装apk\n     */\n    public void installApk(File apkFile) {\n        if (!apkFile.exists()) {\n            return;\n        }\n        Intent intent = new Intent();\n        //执行动作\n        intent.setAction(Intent.ACTION_VIEW);\n        //判读版本是否在7.0以上\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            Uri apkUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + \".fileProvider\", apkFile);\n            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n            intent.setDataAndType(apkUri, \"application/vnd.android.package-archive\");\n        } else {\n            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n            intent.setDataAndType(Uri.fromFile(apkFile), \"application/vnd.android.package-archive\");\n        }\n        try {\n            activity.startActivity(intent);\n        } catch (Exception e) {\n            Log.d(\"wwd\", \"Failed to launcher installing activity\");\n        }\n    }\n\n    public static String getSavePath(String fileName) {\n        return Environment.getExternalStoragePublicDirectory(DOWNLOAD_SERVICE).getPath() + fileName;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/coroutine/CompositeCoroutine.kt",
    "content": "package io.legado.app.help.coroutine\n\n@Suppress(\"unused\")\nclass CompositeCoroutine : CoroutineContainer {\n\n    private var resources: HashSet<Coroutine<*>>? = null\n\n    val size: Int\n        get() = resources?.size ?: 0\n\n    val isEmpty: Boolean\n        get() = size == 0\n\n    constructor()\n\n    constructor(vararg coroutines: Coroutine<*>) {\n        this.resources = hashSetOf(*coroutines)\n    }\n\n    constructor(coroutines: Iterable<Coroutine<*>>) {\n        this.resources = hashSetOf()\n        for (d in coroutines) {\n            this.resources?.add(d)\n        }\n    }\n\n    override fun add(coroutine: Coroutine<*>): Boolean {\n        synchronized(this) {\n            var set: HashSet<Coroutine<*>>? = resources\n            if (resources == null) {\n                set = hashSetOf()\n                resources = set\n            }\n            return set!!.add(coroutine)\n        }\n    }\n\n    override fun addAll(vararg coroutines: Coroutine<*>): Boolean {\n        synchronized(this) {\n            var set: HashSet<Coroutine<*>>? = resources\n            if (resources == null) {\n                set = hashSetOf()\n                resources = set\n            }\n            for (coroutine in coroutines) {\n                val add = set!!.add(coroutine)\n                if (!add) {\n                    return false\n                }\n            }\n        }\n        return true\n    }\n\n    override fun remove(coroutine: Coroutine<*>): Boolean {\n        if (delete(coroutine)) {\n            coroutine.cancel()\n            return true\n        }\n        return false\n    }\n\n    override fun delete(coroutine: Coroutine<*>): Boolean {\n        synchronized(this) {\n            val set = resources\n            if (set == null || !set.remove(coroutine)) {\n                return false\n            }\n        }\n        return true\n    }\n\n    override fun clear() {\n        val set: HashSet<Coroutine<*>>?\n        synchronized(this) {\n            set = resources\n            resources = null\n        }\n\n        set?.forEachIndexed { _, coroutine ->\n            coroutine.cancel()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/coroutine/Coroutine.kt",
    "content": "package io.legado.app.help.coroutine\n\nimport kotlinx.coroutines.*\nimport timber.log.Timber\nimport kotlin.coroutines.CoroutineContext\n\n\n@Suppress(\"unused\")\nclass Coroutine<T>(\n    val scope: CoroutineScope,\n    context: CoroutineContext = Dispatchers.IO,\n    block: suspend CoroutineScope.() -> T\n) {\n\n    companion object {\n\n        private val DEFAULT = MainScope()\n\n        fun <T> async(\n            scope: CoroutineScope = DEFAULT,\n            context: CoroutineContext = Dispatchers.IO,\n            block: suspend CoroutineScope.() -> T\n        ): Coroutine<T> {\n            return Coroutine(scope, context, block)\n        }\n\n    }\n\n    private val job: Job\n\n    private var start: VoidCallback? = null\n    private var success: Callback<T>? = null\n    private var error: Callback<Throwable>? = null\n    private var finally: VoidCallback? = null\n    private var cancel: VoidCallback? = null\n\n    private var timeMillis: Long? = null\n    private var errorReturn: Result<T>? = null\n\n    val isCancelled: Boolean\n        get() = job.isCancelled\n\n    val isActive: Boolean\n        get() = job.isActive\n\n    val isCompleted: Boolean\n        get() = job.isCompleted\n\n    init {\n        this.job = executeInternal(context, block)\n    }\n\n    fun timeout(timeMillis: () -> Long): Coroutine<T> {\n        this.timeMillis = timeMillis()\n        return this@Coroutine\n    }\n\n    fun timeout(timeMillis: Long): Coroutine<T> {\n        this.timeMillis = timeMillis\n        return this@Coroutine\n    }\n\n    fun onErrorReturn(value: () -> T?): Coroutine<T> {\n        this.errorReturn = Result(value())\n        return this@Coroutine\n    }\n\n    fun onErrorReturn(value: T?): Coroutine<T> {\n        this.errorReturn = Result(value)\n        return this@Coroutine\n    }\n\n    fun onStart(\n        context: CoroutineContext? = null,\n        block: (suspend CoroutineScope.() -> Unit)\n    ): Coroutine<T> {\n        this.start = VoidCallback(context, block)\n        return this@Coroutine\n    }\n\n    fun onSuccess(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.(T) -> Unit\n    ): Coroutine<T> {\n        this.success = Callback(context, block)\n        return this@Coroutine\n    }\n\n    fun onError(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.(Throwable) -> Unit\n    ): Coroutine<T> {\n        this.error = Callback(context, block)\n        return this@Coroutine\n    }\n\n    fun onFinally(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.() -> Unit\n    ): Coroutine<T> {\n        this.finally = VoidCallback(context, block)\n        return this@Coroutine\n    }\n\n    fun onCancel(\n        context: CoroutineContext? = null,\n        block: suspend CoroutineScope.() -> Unit\n    ): Coroutine<T> {\n        this.cancel = VoidCallback(context, block)\n        return this@Coroutine\n    }\n\n    //取消当前任务\n    fun cancel(cause: CancellationException? = null) {\n        job.cancel(cause)\n        cancel?.let {\n            MainScope().launch {\n                if (null == it.context) {\n                    it.block.invoke(scope)\n                } else {\n                    withContext(scope.coroutineContext.plus(it.context)) {\n                        it.block.invoke(this)\n                    }\n                }\n            }\n        }\n    }\n\n    fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle {\n        return job.invokeOnCompletion(handler)\n    }\n\n    private fun executeInternal(\n        context: CoroutineContext,\n        block: suspend CoroutineScope.() -> T\n    ): Job {\n        return scope.plus(Dispatchers.Main).launch {\n            try {\n                start?.let { dispatchVoidCallback(this, it) }\n                val value = executeBlock(scope, context, timeMillis ?: 0L, block)\n                if (isActive) {\n                    success?.let { dispatchCallback(this, value, it) }\n                }\n            } catch (e: Throwable) {\n                Timber.e(e)\n                val consume: Boolean = errorReturn?.value?.let { value ->\n                    if (isActive) {\n                        success?.let { dispatchCallback(this, value, it) }\n                    }\n                    true\n                } ?: false\n\n                if (!consume && isActive) {\n                    error?.let { dispatchCallback(this, e, it) }\n                }\n            } finally {\n                if (isActive) {\n                    finally?.let { dispatchVoidCallback(this, it) }\n                }\n            }\n        }\n    }\n\n    private suspend inline fun dispatchVoidCallback(scope: CoroutineScope, callback: VoidCallback) {\n        if (null == callback.context) {\n            callback.block.invoke(scope)\n        } else {\n            withContext(scope.coroutineContext.plus(callback.context)) {\n                callback.block.invoke(this)\n            }\n        }\n    }\n\n    private suspend inline fun <R> dispatchCallback(\n        scope: CoroutineScope,\n        value: R,\n        callback: Callback<R>\n    ) {\n        if (!scope.isActive) return\n        if (null == callback.context) {\n            callback.block.invoke(scope, value)\n        } else {\n            withContext(scope.coroutineContext.plus(callback.context)) {\n                callback.block.invoke(this, value)\n            }\n        }\n    }\n\n    private suspend inline fun executeBlock(\n        scope: CoroutineScope,\n        context: CoroutineContext,\n        timeMillis: Long,\n        noinline block: suspend CoroutineScope.() -> T\n    ): T {\n        return withContext(scope.coroutineContext.plus(context)) {\n            if (timeMillis > 0L) withTimeout(timeMillis) {\n                block()\n            } else {\n                block()\n            }\n        }\n    }\n\n    private data class Result<out T>(val value: T?)\n\n    private inner class VoidCallback(\n        val context: CoroutineContext?,\n        val block: suspend CoroutineScope.() -> Unit\n    )\n\n    private inner class Callback<VALUE>(\n        val context: CoroutineContext?,\n        val block: suspend CoroutineScope.(VALUE) -> Unit\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/coroutine/CoroutineContainer.kt",
    "content": "package io.legado.app.help.coroutine\n\ninternal interface CoroutineContainer {\n\n    fun add(coroutine: Coroutine<*>): Boolean\n\n    fun addAll(vararg coroutines: Coroutine<*>): Boolean\n\n    fun remove(coroutine: Coroutine<*>): Boolean\n\n    fun delete(coroutine: Coroutine<*>): Boolean\n\n    fun clear()\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/glide/ImageLoader.kt",
    "content": "package com.kunfei.bookshelf.help.glide\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport androidx.annotation.DrawableRes\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.RequestBuilder\nimport java.io.File\n\nobject ImageLoader {\n\n    fun load(context: Context, path: String?): RequestBuilder<Drawable> {\n        return when {\n            path.isNullOrEmpty() -> Glide.with(context).load(path)\n            path.startsWith(\"http\", true) -> GlideApp.with(context).load(path)\n            else -> try {\n                Glide.with(context).load(File(path))\n            } catch (e: Exception) {\n                Glide.with(context).load(path)\n            }\n        }\n    }\n\n    fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder<Drawable> {\n        return Glide.with(context).load(resId)\n    }\n\n    fun load(context: Context, file: File?): RequestBuilder<Drawable> {\n        return Glide.with(context).load(file)\n    }\n\n    fun load(context: Context, uri: Uri?): RequestBuilder<Drawable> {\n        return Glide.with(context).load(uri)\n    }\n\n    fun load(context: Context, drawable: Drawable?): RequestBuilder<Drawable> {\n        return Glide.with(context).load(drawable)\n    }\n\n    fun load(context: Context, bitmap: Bitmap?): RequestBuilder<Drawable> {\n        return Glide.with(context).load(bitmap)\n    }\n\n    fun load(context: Context, bytes: ByteArray?): RequestBuilder<Drawable> {\n        return Glide.with(context).load(bytes)\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpGlideModule.kt",
    "content": "package com.kunfei.bookshelf.help.glide\n\nimport android.content.Context\nimport com.bumptech.glide.Glide\nimport com.bumptech.glide.Registry\nimport com.bumptech.glide.annotation.GlideModule\nimport com.bumptech.glide.load.model.GlideUrl\nimport com.bumptech.glide.module.AppGlideModule\nimport java.io.InputStream\n\n@GlideModule\nclass OkHttpGlideModule : AppGlideModule() {\n    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {\n        registry.replace(\n            GlideUrl::class.java,\n            InputStream::class.java,\n            OkHttpModeLoaderFactory\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpModeLoaderFactory.kt",
    "content": "package com.kunfei.bookshelf.help.glide\n\nimport com.bumptech.glide.load.model.GlideUrl\nimport com.bumptech.glide.load.model.ModelLoader\nimport com.bumptech.glide.load.model.ModelLoaderFactory\nimport com.bumptech.glide.load.model.MultiModelLoaderFactory\nimport java.io.InputStream\n\n\nobject OkHttpModeLoaderFactory : ModelLoaderFactory<GlideUrl?, InputStream?> {\n\n    override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl?, InputStream?> {\n        return OkHttpModelLoader\n    }\n\n    override fun teardown() {\n        // Do nothing, this instance doesn't own the client.\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpModelLoader.kt",
    "content": "package com.kunfei.bookshelf.help.glide\n\nimport com.bumptech.glide.load.Options\nimport com.bumptech.glide.load.model.GlideUrl\nimport com.bumptech.glide.load.model.ModelLoader\nimport java.io.InputStream\n\nobject OkHttpModelLoader : ModelLoader<GlideUrl?, InputStream?> {\n\n    override fun buildLoadData(\n        model: GlideUrl,\n        width: Int,\n        height: Int,\n        options: Options\n    ): ModelLoader.LoadData<InputStream?> {\n        return ModelLoader.LoadData(model, OkHttpStreamFetcher(model))\n    }\n\n    override fun handles(model: GlideUrl): Boolean {\n        return true\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/glide/OkHttpStreamFetcher.kt",
    "content": "package com.kunfei.bookshelf.help.glide\n\nimport com.bumptech.glide.Priority\nimport com.bumptech.glide.load.DataSource\nimport com.bumptech.glide.load.HttpException\nimport com.bumptech.glide.load.data.DataFetcher\nimport com.bumptech.glide.load.model.GlideUrl\nimport com.bumptech.glide.util.ContentLengthInputStream\nimport com.bumptech.glide.util.Preconditions\nimport com.kunfei.bookshelf.base.BaseModelImpl\nimport okhttp3.Call\nimport okhttp3.Request\nimport okhttp3.Response\nimport okhttp3.ResponseBody\nimport java.io.IOException\nimport java.io.InputStream\n\n\nclass OkHttpStreamFetcher(private val url: GlideUrl) :\n    DataFetcher<InputStream>, okhttp3.Callback {\n    private var stream: InputStream? = null\n    private var responseBody: ResponseBody? = null\n    private var callback: DataFetcher.DataCallback<in InputStream>? = null\n    private var call: Call? = null\n\n    override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {\n        val requestBuilder: Request.Builder = Request.Builder().url(url.toStringUrl())\n        for ((key, value) in url.headers.entries) {\n            requestBuilder.addHeader(key, value)\n        }\n        val request: Request = requestBuilder.build()\n        this.callback = callback\n        call = BaseModelImpl.getClient().newCall(request)\n        call?.enqueue(this)\n    }\n\n    override fun cleanup() {\n        try {\n            stream?.close()\n        } catch (e: IOException) {\n            // Ignored\n        }\n        responseBody?.close()\n        callback = null\n    }\n\n    override fun cancel() {\n        call?.cancel()\n    }\n\n    override fun getDataClass(): Class<InputStream> {\n        return InputStream::class.java\n    }\n\n    override fun getDataSource(): DataSource {\n        return DataSource.REMOTE\n    }\n\n    override fun onFailure(call: Call, e: IOException) {\n        callback?.onLoadFailed(e)\n    }\n\n    override fun onResponse(call: Call, response: Response) {\n        responseBody = response.body\n        if (response.isSuccessful) {\n            val contentLength: Long = Preconditions.checkNotNull(responseBody).contentLength()\n            stream = ContentLengthInputStream.obtain(responseBody!!.byteStream(), contentLength)\n            callback!!.onDataReady(stream)\n        } else {\n            callback!!.onLoadFailed(HttpException(response.message, response.code))\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/media/LoaderCreator.java",
    "content": "package com.kunfei.bookshelf.help.media;\n\nimport android.content.Context;\nimport android.os.Bundle;\n\nimport androidx.loader.content.CursorLoader;\n\n/**\n * Created by newbiechen on 2018/1/14.\n */\n\npublic class LoaderCreator {\n    public static final int ALL_BOOK_FILE = 1;\n\n    public static CursorLoader create(Context context, int id, Bundle bundle) {\n        LocalFileLoader loader = null;\n        if (id == ALL_BOOK_FILE) {\n            loader = new LocalFileLoader(context);\n        } else {\n            loader = null;\n        }\n        if (loader != null) {\n            return loader;\n        }\n\n        throw new IllegalArgumentException(\"The id of Loader is invalid!\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/media/LocalFileLoader.java",
    "content": "package com.kunfei.bookshelf.help.media;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.provider.MediaStore;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.loader.content.CursorLoader;\n\nimport java.io.File;\nimport java.sql.Blob;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Created by newbiechen on 2018/1/14.\n */\n\npublic class LocalFileLoader extends CursorLoader {\n    private static final String TAG = \"LocalFileLoader\";\n\n    private static final Uri FILE_URI = Uri.parse(\"content://media/external/file\");\n    private static final String SELECTION = MediaStore.Files.FileColumns.DATA + \" like ? or \" + MediaStore.Files.FileColumns.DATA + \" like ?\";\n    private static final String[] SEARCH_TYPE = new String[]{\"%.txt\", \"%.epub\"};\n    private static final String SORT_ORDER = MediaStore.Files.FileColumns.DISPLAY_NAME + \" DESC\";\n    private static final String[] FILE_PROJECTION = {\n            MediaStore.Files.FileColumns.DATA,\n            MediaStore.Files.FileColumns.DISPLAY_NAME\n    };\n\n    public LocalFileLoader(Context context) {\n        super(context);\n        initLoader();\n    }\n\n    /**\n     * 为 Cursor 设置默认参数\n     */\n    private void initLoader() {\n        setUri(FILE_URI);\n        setProjection(FILE_PROJECTION);\n        setSelection(SELECTION);\n        setSelectionArgs(SEARCH_TYPE);\n        setSortOrder(SORT_ORDER);\n    }\n\n    public void parseData(Cursor cursor, final MediaStoreHelper.MediaResultCallback resultCallback) {\n        List<File> files = new ArrayList<>();\n        // 判断是否存在数据\n        if (cursor == null) {\n            // 暂时直接返回空数据\n            resultCallback.onResultCallback(files);\n            return;\n        }\n        // 重复使用Loader时，需要重置cursor的position；\n        cursor.moveToPosition(-1);\n        while (cursor.moveToNext()) {\n            String path;\n\n            path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));\n            // 路径无效\n            if (!TextUtils.isEmpty(path)) {\n                File file = new File(path);\n                if (!file.isDirectory() && file.exists() && file.length() > 1024) {\n                    files.add(file);\n                }\n            }\n        }\n        if (resultCallback != null) {\n            resultCallback.onResultCallback(files);\n        }\n    }\n\n    /**\n     * 从Cursor中读取对应columnName的值\n     *\n     * @param cursor\n     * @param columnName\n     * @param defaultValue\n     * @return 当columnName无效时返回默认值；\n     */\n    protected Object getValueFromCursor(@NonNull Cursor cursor, String columnName, Object defaultValue) {\n        try {\n            int index = cursor.getColumnIndexOrThrow(columnName);\n            int type = cursor.getType(index);\n            switch (type) {\n                case Cursor.FIELD_TYPE_STRING:\n                    // TO SOLVE:某些手机的数据库将数值类型存为String类型\n                    String value = cursor.getString(index);\n                    try {\n                        if (defaultValue instanceof String) {\n                            return value;\n                        } else if (defaultValue instanceof Long) {\n                            return Long.valueOf(value);\n                        } else if (defaultValue instanceof Integer) {\n                            return Integer.valueOf(value);\n                        } else if (defaultValue instanceof Double) {\n                            return Double.valueOf(value);\n                        } else if (defaultValue instanceof Float) {\n                            return Float.valueOf(value);\n                        }\n                    } catch (NumberFormatException e) {\n                        return defaultValue;\n                    }\n                case Cursor.FIELD_TYPE_INTEGER:\n                    if (defaultValue instanceof Long) {\n                        return cursor.getLong(index);\n                    } else if (defaultValue instanceof Integer) {\n                        return cursor.getInt(index);\n                    }\n                case Cursor.FIELD_TYPE_FLOAT:\n                    if (defaultValue instanceof Float) {\n                        return cursor.getFloat(index);\n                    } else if (defaultValue instanceof Double) {\n                        return cursor.getDouble(index);\n                    }\n                case Cursor.FIELD_TYPE_BLOB:\n                    if (defaultValue instanceof Blob) {\n                        return cursor.getBlob(index);\n                    }\n                case Cursor.FIELD_TYPE_NULL:\n                default:\n                    return defaultValue;\n            }\n        } catch (IllegalArgumentException e) {\n            return defaultValue;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/media/MediaStoreHelper.java",
    "content": "package com.kunfei.bookshelf.help.media;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.os.Bundle;\n\nimport androidx.annotation.NonNull;\nimport androidx.fragment.app.FragmentActivity;\nimport androidx.loader.app.LoaderManager;\nimport androidx.loader.content.Loader;\n\nimport java.io.File;\nimport java.lang.ref.WeakReference;\nimport java.util.List;\n\n/**\n * Created by newbiechen on 2018/1/14.\n * 获取媒体库的数据。\n */\n\npublic class MediaStoreHelper {\n\n    /**\n     * 获取媒体库中所有的书籍文件\n     * <p>\n     * 暂时只支持 TXT\n     *\n     * @param activity\n     * @param resultCallback\n     */\n    public static void getAllBookFile(FragmentActivity activity, MediaResultCallback resultCallback) {\n        // 将文件的获取处理交给 LoaderManager。\n        activity.getSupportLoaderManager()\n                .initLoader(LoaderCreator.ALL_BOOK_FILE, null, new MediaLoaderCallbacks(activity, resultCallback));\n    }\n\n    public interface MediaResultCallback {\n        void onResultCallback(List<File> files);\n    }\n\n    /**\n     * Loader 回调处理\n     */\n    static class MediaLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {\n        protected WeakReference<Context> mContext;\n        protected MediaResultCallback mResultCallback;\n\n        public MediaLoaderCallbacks(Context context, MediaResultCallback resultCallback) {\n            mContext = new WeakReference<>(context);\n            mResultCallback = resultCallback;\n        }\n\n        @NonNull\n        @Override\n        public Loader<Cursor> onCreateLoader(int id, Bundle args) {\n            return LoaderCreator.create(mContext.get(), id, args);\n        }\n\n        @Override\n        public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {\n            LocalFileLoader localFileLoader = (LocalFileLoader) loader;\n            localFileLoader.parseData(data, mResultCallback);\n        }\n\n        @Override\n        public void onLoaderReset(@NonNull Loader<Cursor> loader) {\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/ActivitySource.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport java.lang.ref.WeakReference\n\ninternal class ActivitySource(activity: Activity) : RequestSource {\n\n    private val actRef: WeakReference<Activity> = WeakReference(activity)\n\n    override val context: Context?\n        get() = actRef.get()\n\n    override fun startActivity(intent: Intent) {\n        actRef.get()?.startActivity(intent)\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/FragmentSource.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.fragment.app.Fragment\n\nimport java.lang.ref.WeakReference\n\ninternal class FragmentSource(fragment: Fragment) : RequestSource {\n\n    private val fragRef: WeakReference<Fragment> = WeakReference(fragment)\n\n    override val context: Context?\n        get() = fragRef.get()?.requireContext()\n\n    override fun startActivity(intent: Intent) {\n        fragRef.get()?.startActivity(intent)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/OnPermissionsDeniedCallback.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\ninterface OnPermissionsDeniedCallback {\n    fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>)\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/OnPermissionsGrantedCallback.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\ninterface OnPermissionsGrantedCallback {\n\n    fun onPermissionsGranted(requestCode: Int)\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/OnPermissionsResultCallback.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\ninterface OnPermissionsResultCallback {\n\n    fun onPermissionsGranted(requestCode: Int)\n\n    fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>)\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/OnRequestPermissionsResultCallback.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.content.Intent\n\ninterface OnRequestPermissionsResultCallback {\n\n    fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray)\n\n    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/PermissionActivity.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.provider.Settings\nimport android.view.KeyEvent\nimport android.widget.Toast\nimport androidx.core.app.ActivityCompat\nimport com.kunfei.bookshelf.R\n\nclass PermissionActivity : Activity() {\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        when (intent.getIntExtra(KEY_INPUT_REQUEST_TYPE, Request.TYPE_REQUEST_PERMISSION)) {\n            Request.TYPE_REQUEST_PERMISSION//权限请求\n            -> {\n                val requestCode = intent.getIntExtra(KEY_INPUT_PERMISSIONS_CODE, 1000)\n                val permissions = intent.getStringArrayExtra(KEY_INPUT_PERMISSIONS)\n                if (permissions != null) {\n                    ActivityCompat.requestPermissions(this, permissions, requestCode)\n                } else {\n                    finish()\n                }\n            }\n            Request.TYPE_REQUEST_SETTING//跳转到设置界面\n            -> try {\n                val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)\n                settingIntent.data = Uri.fromParts(\"package\", packageName, null)\n                startActivityForResult(settingIntent, Request.TYPE_REQUEST_SETTING)\n            } catch (e: Exception) {\n                Toast.makeText(this, R.string.tip_cannot_jump_setting_page, Toast.LENGTH_SHORT).show()\n                finish()\n            }\n\n        }\n    }\n\n    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        RequestPlugins.sRequestCallback?.onRequestPermissionsResult(requestCode, permissions, grantResults)\n        finish()\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        RequestPlugins.sRequestCallback?.onActivityResult(requestCode, resultCode, data)\n        finish()\n    }\n\n    override fun startActivity(intent: Intent) {\n        super.startActivity(intent)\n        overridePendingTransition(0, 0)\n    }\n\n    override fun finish() {\n        super.finish()\n        overridePendingTransition(0, 0)\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        return if (keyCode == KeyEvent.KEYCODE_BACK) {\n            true\n        } else super.onKeyDown(keyCode, event)\n    }\n\n    companion object {\n\n        const val KEY_INPUT_REQUEST_TYPE = \"KEY_INPUT_REQUEST_TYPE\"\n        const val KEY_INPUT_PERMISSIONS_CODE = \"KEY_INPUT_PERMISSIONS_CODE\"\n        const val KEY_INPUT_PERMISSIONS = \"KEY_INPUT_PERMISSIONS\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/Permissions.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nobject Permissions {\n\n    const val READ_CALENDAR = \"android.permission.READ_CALENDAR\"\n    const val WRITE_CALENDAR = \"android.permission.WRITE_CALENDAR\"\n\n    const val CAMERA = \"android.permission.CAMERA\"\n\n    const val READ_CONTACTS = \"android.permission.READ_CONTACTS\"\n    const val WRITE_CONTACTS = \"android.permission.WRITE_CONTACTS\"\n    const val GET_ACCOUNTS = \"android.permission.GET_ACCOUNTS\"\n\n    const val ACCESS_FINE_LOCATION = \"android.permission.ACCESS_FINE_LOCATION\"\n    const val ACCESS_COARSE_LOCATION = \"android.permission.ACCESS_COARSE_LOCATION\"\n\n    const val RECORD_AUDIO = \"android.permission.RECORD_AUDIO\"\n\n    const val READ_PHONE_STATE = \"android.permission.READ_PHONE_STATE\"\n    const val CALL_PHONE = \"android.permission.CALL_PHONE\"\n    const val READ_CALL_LOG = \"android.permission.READ_CALL_LOG\"\n    const val WRITE_CALL_LOG = \"android.permission.WRITE_CALL_LOG\"\n    const val ADD_VOICEMAIL = \"com.android.voicemail.permission.ADD_VOICEMAIL\"\n    const val USE_SIP = \"android.permission.USE_SIP\"\n    const val PROCESS_OUTGOING_CALLS = \"android.permission.PROCESS_OUTGOING_CALLS\"\n\n    const val BODY_SENSORS = \"android.permission.BODY_SENSORS\"\n\n    const val SEND_SMS = \"android.permission.SEND_SMS\"\n    const val RECEIVE_SMS = \"android.permission.RECEIVE_SMS\"\n    const val READ_SMS = \"android.permission.READ_SMS\"\n    const val RECEIVE_WAP_PUSH = \"android.permission.RECEIVE_WAP_PUSH\"\n    const val RECEIVE_MMS = \"android.permission.RECEIVE_MMS\"\n\n    const val READ_EXTERNAL_STORAGE = \"android.permission.READ_EXTERNAL_STORAGE\"\n    const val WRITE_EXTERNAL_STORAGE = \"android.permission.WRITE_EXTERNAL_STORAGE\"\n\n    object Group {\n        val CALENDAR = arrayOf(READ_CALENDAR, WRITE_CALENDAR)\n\n        val CAMERA = arrayOf(Permissions.CAMERA)\n\n        val CONTACTS = arrayOf(READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS)\n\n        val LOCATION = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)\n\n        val MICROPHONE = arrayOf(RECORD_AUDIO)\n\n        val PHONE = arrayOf(\n                READ_PHONE_STATE,\n                CALL_PHONE,\n                READ_CALL_LOG,\n                WRITE_CALL_LOG,\n                ADD_VOICEMAIL,\n                USE_SIP,\n                PROCESS_OUTGOING_CALLS\n        )\n\n        val SENSORS = arrayOf(BODY_SENSORS)\n\n        val SMS = arrayOf(\n                SEND_SMS,\n                RECEIVE_SMS,\n                READ_SMS,\n                RECEIVE_WAP_PUSH,\n                RECEIVE_MMS\n        )\n\n        val STORAGE = arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/PermissionsCompat.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.app.Activity\nimport androidx.annotation.StringRes\nimport androidx.fragment.app.Fragment\n\nclass PermissionsCompat private constructor() {\n\n    private var request: Request? = null\n\n    fun request() {\n        RequestManager.pushRequest(request)\n    }\n\n    class Builder {\n        private val request: Request\n\n        constructor(activity: Activity) {\n            request = Request(activity)\n        }\n\n        constructor(fragment: Fragment) {\n            request = Request(fragment)\n        }\n\n        fun addPermissions(vararg permissions: String): Builder {\n            request.addPermissions(*permissions)\n            return this\n        }\n\n        fun requestCode(requestCode: Int): Builder {\n            request.setRequestCode(requestCode)\n            return this\n        }\n\n        fun onGranted(callback: (requestCode: Int) -> Unit): Builder {\n            request.setOnGrantedCallback(object : OnPermissionsGrantedCallback {\n                override fun onPermissionsGranted(requestCode: Int) {\n                    callback(requestCode)\n                }\n            })\n            return this\n        }\n\n        fun onDenied(callback: (requestCode: Int, deniedPermissions: Array<String>) -> Unit): Builder {\n            request.setOnDeniedCallback(object : OnPermissionsDeniedCallback {\n                override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) {\n                    callback(requestCode, deniedPermissions)\n                }\n            })\n            return this\n        }\n\n        fun rationale(rationale: CharSequence): Builder {\n            request.setRationale(rationale)\n            return this\n        }\n\n        fun rationale(@StringRes resId: Int): Builder {\n            request.setRationale(resId)\n            return this\n        }\n\n        fun build(): PermissionsCompat {\n            val compat = PermissionsCompat()\n            compat.request = request\n            return compat\n        }\n\n        fun request(): PermissionsCompat {\n            val compat = build()\n            compat.request = request\n            compat.request()\n            return compat\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/Request.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.content.ContextCompat\nimport androidx.fragment.app.Fragment\nimport com.kunfei.bookshelf.R\nimport org.jetbrains.anko.startActivity\nimport java.util.*\n\ninternal class Request : OnRequestPermissionsResultCallback {\n\n    internal val requestTime: Long\n    private var requestCode: Int = TYPE_REQUEST_PERMISSION\n    private var source: RequestSource? = null\n    private var permissions: ArrayList<String>? = null\n    private var grantedCallback: OnPermissionsGrantedCallback? = null\n    private var deniedCallback: OnPermissionsDeniedCallback? = null\n    private var rationaleResId: Int = 0\n    private var rationale: CharSequence? = null\n\n    private var rationaleDialog: AlertDialog? = null\n\n    private val deniedPermissions: Array<String>?\n        get() {\n            return getDeniedPermissions(this.permissions?.toTypedArray())\n        }\n\n    constructor(activity: Activity) {\n        source = ActivitySource(activity)\n        permissions = ArrayList()\n        requestTime = System.currentTimeMillis()\n    }\n\n    constructor(fragment: Fragment) {\n        source = FragmentSource(fragment)\n        permissions = ArrayList()\n        requestTime = System.currentTimeMillis()\n    }\n\n    fun addPermissions(vararg permissions: String) {\n        this.permissions?.addAll(Arrays.asList(*permissions))\n    }\n\n    fun setRequestCode(requestCode: Int) {\n        this.requestCode = requestCode\n    }\n\n    fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) {\n        grantedCallback = callback\n    }\n\n    fun setOnDeniedCallback(callback: OnPermissionsDeniedCallback) {\n        deniedCallback = callback\n    }\n\n    fun setRationale(@StringRes resId: Int) {\n        rationaleResId = resId\n        rationale = null\n    }\n\n    fun setRationale(rationale: CharSequence) {\n        this.rationale = rationale\n        rationaleResId = 0\n    }\n\n    fun start() {\n        RequestPlugins.setOnRequestPermissionsCallback(this)\n\n        val deniedPermissions = deniedPermissions\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n            if (deniedPermissions == null) {\n                onPermissionsGranted(requestCode)\n            } else {\n                val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale\n                if (rationale != null) {\n                    showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) }\n                } else {\n                    onPermissionsDenied(requestCode, deniedPermissions)\n                }\n            }\n        } else {\n            if (deniedPermissions != null) {\n                source?.context?.startActivity<PermissionActivity>(\n                        Pair(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_PERMISSION),\n                        Pair(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode),\n                        Pair(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)\n                )\n            } else {\n                onPermissionsGranted(requestCode)\n            }\n        }\n    }\n\n    fun clear() {\n        grantedCallback = null\n        deniedCallback = null\n    }\n\n    private fun getDeniedPermissions(permissions: Array<String>?): Array<String>? {\n        if (permissions != null) {\n            val deniedPermissionList = ArrayList<String>()\n            for (permission in permissions) {\n                if (source?.context?.let {\n                            ContextCompat.checkSelfPermission(\n                                    it,\n                                    permission\n                            )\n                        } != PackageManager.PERMISSION_GRANTED\n                ) {\n                    deniedPermissionList.add(permission)\n                }\n            }\n            val size = deniedPermissionList.size\n            if (size > 0) {\n                return deniedPermissionList.toTypedArray()\n            }\n        }\n        return null\n    }\n\n    private fun showSettingDialog(rationale: CharSequence, cancel: () -> Unit) {\n        rationaleDialog?.dismiss()\n        source?.context?.let {\n            runCatching {\n                rationaleDialog = AlertDialog.Builder(it)\n                        .setTitle(R.string.dialog_title)\n                        .setMessage(rationale)\n                        .setPositiveButton(R.string.dialog_setting) { _, _ ->\n                            it.startActivity<PermissionActivity>(\n                                    Pair(\n                                            PermissionActivity.KEY_INPUT_REQUEST_TYPE,\n                                            TYPE_REQUEST_SETTING\n                                    )\n                            )\n                        }\n                        .setNegativeButton(R.string.dialog_cancel) { _, _ -> cancel() }\n                        .show()\n            }\n        }\n    }\n\n    private fun onPermissionsGranted(requestCode: Int) {\n        try {\n            grantedCallback?.onPermissionsGranted(requestCode)\n        } catch (ignore: Exception) {\n        }\n\n        RequestPlugins.sResultCallback?.onPermissionsGranted(requestCode)\n    }\n\n    private fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) {\n        try {\n            deniedCallback?.onPermissionsDenied(requestCode, deniedPermissions)\n        } catch (ignore: Exception) {\n        }\n\n        RequestPlugins.sResultCallback?.onPermissionsDenied(requestCode, deniedPermissions)\n    }\n\n    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {\n        val deniedPermissions = getDeniedPermissions(permissions)\n        if (deniedPermissions != null) {\n            val rationale = if (rationaleResId != 0) source?.context?.getText(rationaleResId) else rationale\n            if (rationale != null) {\n                showSettingDialog(rationale) { onPermissionsDenied(requestCode, deniedPermissions) }\n            } else {\n                onPermissionsDenied(requestCode, deniedPermissions)\n            }\n        } else {\n            onPermissionsGranted(requestCode)\n        }\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        val deniedPermissions = deniedPermissions\n        if (deniedPermissions == null) {\n            onPermissionsGranted(this.requestCode)\n        } else {\n            onPermissionsDenied(this.requestCode, deniedPermissions)\n        }\n    }\n\n    companion object {\n        const val TYPE_REQUEST_PERMISSION = 1\n        const val TYPE_REQUEST_SETTING = 2\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/RequestManager.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.os.Handler\nimport android.os.Looper\nimport java.util.*\n\ninternal object RequestManager : OnPermissionsResultCallback {\n\n    private var requests: Stack<Request>? = null\n    private var request: Request? = null\n\n    private val handler = Handler(Looper.getMainLooper())\n\n    private val requestRunnable = Runnable {\n        request?.start()\n    }\n\n    private val isCurrentRequestInvalid: Boolean\n        get() = request?.let { System.currentTimeMillis() - it.requestTime > 5 * 1000L } ?: true\n\n    init {\n        RequestPlugins.setOnPermissionsResultCallback(this)\n    }\n\n    fun pushRequest(request: Request?) {\n        if (request == null) return\n\n        if (requests == null) {\n            requests = Stack()\n        }\n\n        requests?.let {\n            val index = it.indexOf(request)\n            if (index >= 0) {\n                val to = it.size - 1\n                if (index != to) {\n                    Collections.swap(requests, index, to)\n                }\n            } else {\n                it.push(request)\n            }\n\n            if (!it.empty() && isCurrentRequestInvalid) {\n                this.request = it.pop()\n                handler.post(requestRunnable)\n            }\n        }\n    }\n\n    private fun startNextRequest() {\n        request?.clear()\n        request = null\n\n        requests?.let {\n            request = if (it.empty()) null else it.pop()\n            request?.let { handler.post(requestRunnable) }\n        }\n    }\n\n    override fun onPermissionsGranted(requestCode: Int) {\n        startNextRequest()\n    }\n\n    override fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) {\n        startNextRequest()\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/RequestPlugins.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\ninternal object RequestPlugins {\n\n    @Volatile\n    var sRequestCallback: OnRequestPermissionsResultCallback? = null\n\n    @Volatile\n    var sResultCallback: OnPermissionsResultCallback? = null\n\n    fun setOnRequestPermissionsCallback(callback: OnRequestPermissionsResultCallback) {\n        sRequestCallback = callback\n    }\n\n    fun setOnPermissionsResultCallback(callback: OnPermissionsResultCallback) {\n        sResultCallback = callback\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/permission/RequestSource.kt",
    "content": "package com.kunfei.bookshelf.help.permission\n\nimport android.content.Context\nimport android.content.Intent\n\ninterface RequestSource {\n\n    val context: Context?\n\n    fun startActivity(intent: Intent)\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/storage/Backup.kt",
    "content": "package com.kunfei.bookshelf.help.storage\n\nimport android.content.Context\nimport android.net.Uri\nimport androidx.documentfile.provider.DocumentFile\nimport com.kunfei.bookshelf.DbHelper\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.base.observer.MySingleObserver\nimport com.kunfei.bookshelf.help.BookshelfHelp\nimport com.kunfei.bookshelf.help.FileHelp\nimport com.kunfei.bookshelf.model.BookSourceManager\nimport com.kunfei.bookshelf.model.ReplaceRuleManager\nimport com.kunfei.bookshelf.model.TxtChapterRuleManager\nimport com.kunfei.bookshelf.utils.DocumentUtil\nimport com.kunfei.bookshelf.utils.FileUtils\nimport com.kunfei.bookshelf.utils.GSON\nimport io.reactivex.Single\nimport io.reactivex.SingleOnSubscribe\nimport io.reactivex.android.schedulers.AndroidSchedulers\nimport io.reactivex.schedulers.Schedulers\nimport java.io.File\nimport java.util.concurrent.TimeUnit\n\n\nobject Backup {\n\n    val backupPath = MApplication.getInstance().filesDir.absolutePath + File.separator + \"backup\"\n\n    val defaultPath by lazy {\n        FileUtils.getSdCardPath() + File.separator + \"YueDu\"\n    }\n\n    val backupFileNames by lazy {\n        arrayOf(\n                \"myBookShelf.json\",\n                \"myBookSource.json\",\n                \"myBookSearchHistory.json\",\n                \"myBookReplaceRule.json\",\n                \"myTxtChapterRule.json\",\n                \"config.xml\"\n        )\n    }\n\n    fun autoBack() {\n        val lastBackup = MApplication.getConfigPreferences().getLong(\"lastBackup\", 0)\n        if (System.currentTimeMillis() - lastBackup < TimeUnit.DAYS.toMillis(1)) {\n            return\n        }\n        val path = MApplication.getConfigPreferences().getString(\"backupPath\", defaultPath)\n        if (path == null) {\n            backup(MApplication.getInstance(), defaultPath, null, true)\n        } else {\n            backup(MApplication.getInstance(), path, null, true)\n        }\n    }\n\n    fun backup(context: Context, path: String, callBack: CallBack?, isAuto: Boolean = false) {\n        MApplication.getConfigPreferences().edit().putLong(\"lastBackup\", System.currentTimeMillis()).apply()\n        Single.create(SingleOnSubscribe<Boolean> { e ->\n            BookshelfHelp.getAllBook().let {\n                if (it.isNotEmpty()) {\n                    val json = GSON.toJson(it)\n                    FileHelp.createFileIfNotExist(backupPath + File.separator + \"myBookShelf.json\").writeText(json)\n                }\n            }\n            BookSourceManager.getAllBookSource().let {\n                if (it.isNotEmpty()) {\n                    val json = GSON.toJson(it)\n                    FileHelp.createFileIfNotExist(backupPath + File.separator + \"myBookSource.json\").writeText(json)\n                }\n            }\n            DbHelper.getDaoSession().searchHistoryBeanDao.queryBuilder().list().let {\n                if (it.isNotEmpty()) {\n                    val json = GSON.toJson(it)\n                    FileHelp.createFileIfNotExist(backupPath + File.separator + \"myBookSearchHistory.json\")\n                            .writeText(json)\n                }\n            }\n            ReplaceRuleManager.getAll().blockingGet().let {\n                if (it.isNotEmpty()) {\n                    val json = GSON.toJson(it)\n                    FileHelp.createFileIfNotExist(backupPath + File.separator + \"myBookReplaceRule.json\").writeText(json)\n                }\n            }\n            TxtChapterRuleManager.getAll().let {\n                if (it.isNotEmpty()) {\n                    val json = GSON.toJson(it)\n                    FileHelp.createFileIfNotExist(backupPath + File.separator + \"myTxtChapterRule.json\")\n                            .writeText(json)\n                }\n            }\n            Preferences.getSharedPreferences(context, backupPath, \"config\")?.let { sp ->\n                val edit = sp.edit()\n                MApplication.getConfigPreferences().all.map {\n                    when (val value = it.value) {\n                        is Int -> edit.putInt(it.key, value)\n                        is Boolean -> edit.putBoolean(it.key, value)\n                        is Long -> edit.putLong(it.key, value)\n                        is Float -> edit.putFloat(it.key, value)\n                        is String -> edit.putString(it.key, value)\n                        else -> Unit\n                    }\n                }\n                edit.commit()\n            }\n            WebDavHelp.backUpWebDav(backupPath)\n            if (path.isContentPath()) {\n                copyBackup(context, Uri.parse(path), isAuto)\n            } else {\n                copyBackup(path, isAuto)\n            }\n            e.onSuccess(true)\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(object : MySingleObserver<Boolean>() {\n                    override fun onSuccess(t: Boolean) {\n                        callBack?.backupSuccess()\n                    }\n\n                    override fun onError(e: Throwable) {\n                        e.printStackTrace()\n                        callBack?.backupError(e.localizedMessage ?: \"ERROR\")\n                    }\n                })\n    }\n\n    @Throws(Exception::class)\n    private fun copyBackup(context: Context, uri: Uri, isAuto: Boolean) {\n        synchronized(this) {\n            DocumentFile.fromTreeUri(context, uri)?.let { treeDoc ->\n                for (fileName in backupFileNames) {\n                    val file = File(backupPath + File.separator + fileName)\n                    if (file.exists()) {\n                        if (isAuto) {\n                            treeDoc.findFile(\"auto\")?.findFile(fileName)?.delete()\n                            var autoDoc = treeDoc.findFile(\"auto\")\n                            if (autoDoc == null) {\n                                autoDoc = treeDoc.createDirectory(\"auto\")\n                            }\n                            autoDoc?.createFile(\"\", fileName)?.let {\n                                DocumentUtil.writeBytes(context, file.readBytes(), it)\n                            }\n                        } else {\n                            treeDoc.findFile(fileName)?.delete()\n                            treeDoc.createFile(\"\", fileName)?.let {\n                                DocumentUtil.writeBytes(context, file.readBytes(), it)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    @Throws(java.lang.Exception::class)\n    private fun copyBackup(path: String, isAuto: Boolean) {\n        synchronized(this) {\n            for (fileName in backupFileNames) {\n                if (isAuto) {\n                    val file = File(backupPath + File.separator + fileName)\n                    if (file.exists()) {\n                        file.copyTo(FileHelp.createFileIfNotExist(path + File.separator + \"auto\" + File.separator + fileName), true)\n                    }\n                } else {\n                    val file = File(backupPath + File.separator + fileName)\n                    if (file.exists()) {\n                        file.copyTo(FileHelp.createFileIfNotExist(path + File.separator + fileName), true)\n                    }\n                }\n            }\n        }\n    }\n\n    interface CallBack {\n        fun backupSuccess()\n        fun backupError(msg: String)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/storage/BackupRestoreUi.kt",
    "content": "package com.kunfei.bookshelf.help.storage\n\nimport android.app.Activity\nimport android.app.Activity.RESULT_OK\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Build\nimport android.text.TextUtils\nimport androidx.core.content.ContextCompat\nimport androidx.documentfile.provider.DocumentFile\nimport com.hwangjr.rxbus.RxBus\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.base.observer.MySingleObserver\nimport com.kunfei.bookshelf.constant.RxBusTag\nimport com.kunfei.bookshelf.help.permission.Permissions\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat\nimport com.kunfei.bookshelf.help.storage.WebDavHelp.getWebDavFileNames\nimport com.kunfei.bookshelf.help.storage.WebDavHelp.showRestoreDialog\nimport com.kunfei.bookshelf.widget.filepicker.picker.FilePicker\nimport io.reactivex.Single\nimport io.reactivex.SingleEmitter\nimport io.reactivex.android.schedulers.AndroidSchedulers\nimport io.reactivex.schedulers.Schedulers\nimport org.jetbrains.anko.alert\nimport org.jetbrains.anko.toast\nimport java.util.*\n\nobject BackupRestoreUi : Backup.CallBack, Restore.CallBack {\n\n    private const val backupSelectRequestCode = 22\n    private const val restoreSelectRequestCode = 33\n\n    private fun getBackupPath(): String? {\n        return MApplication.getConfigPreferences().getString(\"backupPath\", null)\n    }\n\n    private fun setBackupPath(path: String?) {\n        if (path.isNullOrEmpty()) {\n            MApplication.getConfigPreferences().edit().remove(\"backupPath\").apply()\n        } else {\n            MApplication.getConfigPreferences().edit().putString(\"backupPath\", path).apply()\n        }\n    }\n\n    override fun backupSuccess() {\n        MApplication.getInstance().toast(R.string.backup_success)\n    }\n\n    override fun backupError(msg: String) {\n        MApplication.getInstance().toast(msg)\n    }\n\n    override fun restoreSuccess() {\n        MApplication.getInstance().toast(R.string.restore_success)\n        RxBus.get().post(RxBusTag.RECREATE, true)\n    }\n\n    override fun restoreError(msg: String) {\n        MApplication.getInstance().toast(msg)\n    }\n\n    fun backup(activity: Activity) {\n        val backupPath = getBackupPath()\n        if (backupPath.isNullOrEmpty()) {\n            selectBackupFolder(activity)\n        } else if (backupPath.isContentPath()) {\n            val uri = Uri.parse(backupPath)\n            val doc = DocumentFile.fromTreeUri(activity, uri)\n            if (doc?.canWrite() == true) {\n                Backup.backup(activity, backupPath, this)\n            } else {\n                selectBackupFolder(activity)\n            }\n        } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {\n            selectBackupFolder(activity)\n        } else {\n            backupUsePermission(activity)\n        }\n    }\n\n    private fun backupUsePermission(activity: Activity, path: String = Backup.defaultPath) {\n        PermissionsCompat.Builder(activity)\n            .addPermissions(*Permissions.Group.STORAGE)\n            .rationale(R.string.get_storage_per)\n            .onGranted {\n                setBackupPath(path)\n                Backup.backup(activity, path, this)\n            }\n            .request()\n    }\n\n    fun selectBackupFolder(activity: Activity) {\n        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {\n            try {\n                val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)\n                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                activity.startActivityForResult(intent, backupSelectRequestCode)\n            } catch (e: java.lang.Exception) {\n                e.printStackTrace()\n                activity.toast(e.localizedMessage ?: \"ERROR\")\n            }\n            return\n        }\n        activity.alert {\n            titleResource = R.string.select_folder\n            items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index ->\n                when (index) {\n                    0 -> {\n                        try {\n                            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)\n                            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                            activity.startActivityForResult(intent, backupSelectRequestCode)\n                        } catch (e: java.lang.Exception) {\n                            e.printStackTrace()\n                            activity.toast(e.localizedMessage ?: \"ERROR\")\n                        }\n                    }\n                    1 -> {\n                        PermissionsCompat.Builder(activity)\n                            .addPermissions(*Permissions.Group.STORAGE)\n                            .rationale(R.string.get_storage_per)\n                            .onGranted {\n                                selectBackupFolderApp(activity, false)\n                            }\n                            .request()\n                    }\n                    2 -> {\n                        setBackupPath(Backup.defaultPath)\n                        backupUsePermission(activity)\n                    }\n                }\n            }\n        }.show()\n    }\n\n    private fun selectBackupFolderApp(activity: Activity, isRestore: Boolean) {\n        val picker = FilePicker(activity, FilePicker.DIRECTORY)\n        picker.setBackgroundColor(ContextCompat.getColor(activity, R.color.background))\n        picker.setTopBackgroundColor(ContextCompat.getColor(activity, R.color.background))\n        picker.setItemHeight(30)\n        picker.setOnFilePickListener { currentPath: String ->\n            setBackupPath(currentPath)\n            if (isRestore) {\n                Restore.restore(currentPath, this)\n            } else {\n                Backup.backup(activity, currentPath, this)\n            }\n        }\n        picker.show()\n    }\n\n    fun restore(activity: Activity) {\n        Single.create { emitter: SingleEmitter<ArrayList<String>?> ->\n            emitter.onSuccess(getWebDavFileNames())\n        }.subscribeOn(Schedulers.io())\n            .observeOn(AndroidSchedulers.mainThread())\n            .subscribe(object : MySingleObserver<ArrayList<String>?>() {\n                override fun onSuccess(strings: ArrayList<String>) {\n                    if (!showRestoreDialog(activity, strings, this@BackupRestoreUi)) {\n                        val path = getBackupPath()\n                        if (TextUtils.isEmpty(path)) {\n                            selectRestoreFolder(activity)\n                        } else if (path.isContentPath()) {\n                            val uri = Uri.parse(path)\n                            val doc = DocumentFile.fromTreeUri(activity, uri)\n                            if (doc?.canWrite() == true) {\n                                Restore.restore(activity, Uri.parse(path), this@BackupRestoreUi)\n                            } else {\n                                selectRestoreFolder(activity)\n                            }\n                        } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {\n                            selectRestoreFolder(activity)\n                        } else {\n                            restoreUsePermission(activity)\n                        }\n                    }\n                }\n            })\n    }\n\n    private fun restoreUsePermission(activity: Activity, path: String = Backup.defaultPath) {\n        PermissionsCompat.Builder(activity)\n            .addPermissions(*Permissions.Group.STORAGE)\n            .rationale(R.string.get_storage_per)\n            .onGranted {\n                setBackupPath(path)\n                Restore.restore(path, this)\n            }\n            .request()\n    }\n\n    private fun selectRestoreFolder(activity: Activity) {\n        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {\n            try {\n                val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)\n                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                activity.startActivityForResult(intent, restoreSelectRequestCode)\n            } catch (e: java.lang.Exception) {\n                e.printStackTrace()\n                activity.toast(e.localizedMessage ?: \"ERROR\")\n            }\n            return\n        }\n        activity.alert {\n            titleResource = R.string.select_folder\n            items(activity.resources.getStringArray(R.array.select_folder).toList()) { _, index ->\n                when (index) {\n                    0 -> {\n                        try {\n                            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)\n                            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                            activity.startActivityForResult(intent, restoreSelectRequestCode)\n                        } catch (e: java.lang.Exception) {\n                            e.printStackTrace()\n                            activity.toast(e.localizedMessage ?: \"ERROR\")\n                        }\n                    }\n                    1 -> {\n                        PermissionsCompat.Builder(activity)\n                            .addPermissions(*Permissions.Group.STORAGE)\n                            .rationale(R.string.get_storage_per)\n                            .onGranted {\n                                selectBackupFolderApp(activity, true)\n                            }\n                            .request()\n                    }\n                    2 -> restoreUsePermission(activity)\n                }\n            }\n        }.show()\n    }\n\n    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        when (requestCode) {\n            backupSelectRequestCode -> if (resultCode == RESULT_OK) {\n                data?.data?.let { uri ->\n                    MApplication.getInstance().contentResolver.takePersistableUriPermission(\n                        uri,\n                        Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION\n                    )\n                    setBackupPath(uri.toString())\n                    Backup.backup(MApplication.getInstance(), uri.toString(), this)\n                }\n            }\n            restoreSelectRequestCode -> if (resultCode == RESULT_OK) {\n                data?.data?.let { uri ->\n                    MApplication.getInstance().contentResolver.takePersistableUriPermission(\n                        uri,\n                        Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION\n                    )\n                    setBackupPath(uri.toString())\n                    Restore.restore(MApplication.getInstance(), uri, this)\n                }\n            }\n        }\n    }\n\n}\n\nfun String?.isContentPath(): Boolean = this?.startsWith(\"content://\") == true"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/storage/Preferences.kt",
    "content": "package com.kunfei.bookshelf.help.storage\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.ContextWrapper\nimport android.content.SharedPreferences\nimport java.io.File\n\nobject Preferences {\n\n    /**\n     * 用反射生成 SharedPreferences\n     * @param context\n     * @param dir\n     * @param fileName 文件名,不需要 '.xml' 后缀\n     * @return\n     */\n    fun getSharedPreferences(\n            context: Context,\n            dir: String,\n            fileName: String\n    ): SharedPreferences? {\n        try {\n            // 获取 ContextWrapper对象中的mBase变量。该变量保存了 ContextImpl 对象\n            val fieldMBase = ContextWrapper::class.java.getDeclaredField(\"mBase\")\n            fieldMBase.isAccessible = true\n            // 获取 mBase变量\n            val objMBase = fieldMBase.get(context)\n            // 获取 ContextImpl。mPreferencesDir变量，该变量保存了数据文件的保存路径\n            val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField(\"mPreferencesDir\")\n            fieldMPreferencesDir.isAccessible = true\n            // 创建自定义路径\n            // String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+\"/Android\";\n            val file = File(dir)\n            // 修改mPreferencesDir变量的值\n            fieldMPreferencesDir.set(objMBase, file)\n            // 返回修改路径以后的 SharedPreferences :%FILE_PATH%/%fileName%.xml\n            return context.getSharedPreferences(fileName, Activity.MODE_PRIVATE)\n        } catch (e: NoSuchFieldException) {\n            e.printStackTrace()\n        } catch (e: IllegalArgumentException) {\n            e.printStackTrace()\n        } catch (e: IllegalAccessException) {\n            e.printStackTrace()\n        }\n        return null\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/storage/Restore.kt",
    "content": "package com.kunfei.bookshelf.help.storage\n\nimport android.content.Context\nimport android.net.Uri\nimport androidx.documentfile.provider.DocumentFile\nimport com.kunfei.bookshelf.DbHelper\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.base.observer.MySingleObserver\nimport com.kunfei.bookshelf.bean.*\nimport com.kunfei.bookshelf.help.FileHelp\nimport com.kunfei.bookshelf.help.LauncherIcon\nimport com.kunfei.bookshelf.help.ReadBookControl\nimport com.kunfei.bookshelf.model.BookSourceManager\nimport com.kunfei.bookshelf.model.ReplaceRuleManager\nimport com.kunfei.bookshelf.model.TxtChapterRuleManager\nimport com.kunfei.bookshelf.utils.DocumentUtil\nimport com.kunfei.bookshelf.utils.GSON\nimport com.kunfei.bookshelf.utils.fromJsonArray\nimport io.reactivex.Single\nimport io.reactivex.SingleOnSubscribe\nimport io.reactivex.android.schedulers.AndroidSchedulers\nimport io.reactivex.schedulers.Schedulers\nimport java.io.File\n\nobject Restore {\n\n    fun restore(context: Context, uri: Uri, callBack: CallBack?) {\n        Single.create(SingleOnSubscribe<Boolean> { e ->\n            DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc ->\n                for (fileName in Backup.backupFileNames) {\n                    if (doc.name == fileName) {\n                        DocumentUtil.readBytes(context, doc.uri)?.let {\n                            FileHelp.createFileIfNotExist(Backup.backupPath + File.separator + fileName)\n                                    .writeBytes(it)\n                        }\n                    }\n                }\n            }\n            e.onSuccess(true)\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(object : MySingleObserver<Boolean>() {\n                    override fun onSuccess(t: Boolean) {\n                        restore(Backup.backupPath, callBack)\n                    }\n\n                    override fun onError(e: Throwable) {\n                        e.printStackTrace()\n                        callBack?.restoreError(e.localizedMessage ?: \"ERROR\")\n                    }\n                })\n    }\n\n    fun restore(path: String, callBack: CallBack?) {\n        Single.create(SingleOnSubscribe<Boolean> { e ->\n            try {\n                val file = FileHelp.createFileIfNotExist(path + File.separator + \"myBookShelf.json\")\n                val json = file.readText()\n                GSON.fromJsonArray<BookShelfBean>(json)?.forEach { bookshelf ->\n                    if (bookshelf.noteUrl != null) {\n                        DbHelper.getDaoSession().bookShelfBeanDao.insertOrReplace(bookshelf)\n                    }\n                    if (bookshelf.bookInfoBean.noteUrl != null) {\n                        DbHelper.getDaoSession().bookInfoBeanDao.insertOrReplace(bookshelf.bookInfoBean)\n                    }\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n            try {\n                val file = FileHelp.createFileIfNotExist(path + File.separator + \"myBookSource.json\")\n                val json = file.readText()\n                GSON.fromJsonArray<BookSourceBean>(json)?.let {\n                    BookSourceManager.addBookSource(it)\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n            try {\n                val file = FileHelp.createFileIfNotExist(path + File.separator + \"myBookSearchHistory.json\")\n                val json = file.readText()\n                GSON.fromJsonArray<SearchHistoryBean>(json)?.let {\n                    DbHelper.getDaoSession().searchHistoryBeanDao.insertOrReplaceInTx(it)\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n            try {\n                val file = FileHelp.createFileIfNotExist(path + File.separator + \"myBookReplaceRule.json\")\n                val json = file.readText()\n                GSON.fromJsonArray<ReplaceRuleBean>(json)?.let {\n                    ReplaceRuleManager.addDataS(it)\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n            try {\n                val file = FileHelp.createFileIfNotExist(path + File.separator + \"myTxtChapterRule.json\")\n                val json = file.readText()\n                GSON.fromJsonArray<TxtChapterRuleBean>(json)?.let {\n                    TxtChapterRuleManager.save(it)\n                }\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n            var donateHb = MApplication.getConfigPreferences().getLong(\"DonateHb\", 0)\n            donateHb = if (donateHb > System.currentTimeMillis()) 0 else donateHb\n            Preferences.getSharedPreferences(MApplication.getInstance(), path, \"config\")?.all?.map {\n                val edit = MApplication.getConfigPreferences().edit()\n                when (val value = it.value) {\n                    is Int -> edit.putInt(it.key, value)\n                    is Boolean -> edit.putBoolean(it.key, value)\n                    is Long -> edit.putLong(it.key, value)\n                    is Float -> edit.putFloat(it.key, value)\n                    is String -> edit.putString(it.key, value)\n                    else -> Unit\n                }\n                edit.putLong(\"DonateHb\", donateHb)\n                edit.putInt(\"versionCode\", MApplication.getVersionCode())\n                edit.apply()\n            }\n            e.onSuccess(true)\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(object : MySingleObserver<Boolean>() {\n                    override fun onSuccess(t: Boolean) {\n                        LauncherIcon.ChangeIcon(MApplication.getConfigPreferences().getString(\"launcher_icon\", MApplication.getInstance().getString(R.string.icon_main)))\n                        ReadBookControl.getInstance().updateReaderSettings()\n                        MApplication.getInstance().upThemeStore()\n                        MApplication.getInstance().initNightTheme()\n                        callBack?.restoreSuccess()\n                    }\n\n                    override fun onError(e: Throwable) {\n                        e.printStackTrace()\n                        callBack?.restoreError(e.localizedMessage ?: \"ERROR\")\n                    }\n                })\n    }\n\n\n    interface CallBack {\n        fun restoreSuccess()\n        fun restoreError(msg: String)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/help/storage/WebDavHelp.kt",
    "content": "package com.kunfei.bookshelf.help.storage\n\nimport android.content.Context\nimport android.os.Handler\nimport android.os.Looper\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.base.observer.MySingleObserver\nimport com.kunfei.bookshelf.constant.AppConstant\nimport com.kunfei.bookshelf.help.FileHelp\nimport com.kunfei.bookshelf.utils.ZipUtils\nimport com.kunfei.bookshelf.utils.webdav.WebDav\nimport com.kunfei.bookshelf.utils.webdav.http.HttpAuth\nimport io.reactivex.Single\nimport io.reactivex.SingleOnSubscribe\nimport io.reactivex.android.schedulers.AndroidSchedulers\nimport io.reactivex.schedulers.Schedulers\nimport org.jetbrains.anko.selector\nimport org.jetbrains.anko.toast\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport kotlin.math.min\n\nobject WebDavHelp {\n    private val zipFilePath = FileHelp.getCachePath() + \"/backup\" + \".zip\"\n    private val unzipFilesPath by lazy {\n        FileHelp.getCachePath()\n    }\n\n    private fun getWebDavUrl(): String {\n        var url = MApplication.getConfigPreferences().getString(\"web_dav_url\", AppConstant.DEFAULT_WEB_DAV_URL)\n        if (url.isNullOrEmpty()) {\n            url = AppConstant.DEFAULT_WEB_DAV_URL\n        }\n        if (!url.endsWith(\"/\")) url += \"/\"\n        return url\n    }\n\n    private fun initWebDav(): Boolean {\n        val account = MApplication.getConfigPreferences().getString(\"web_dav_account\", \"\")\n        val password = MApplication.getConfigPreferences().getString(\"web_dav_password\", \"\")\n        if (!account.isNullOrBlank() && !password.isNullOrBlank()) {\n            HttpAuth.auth = HttpAuth.Auth(account, password)\n            return true\n        }\n        return false\n    }\n\n    fun getWebDavFileNames(): ArrayList<String> {\n        val url = getWebDavUrl()\n        val names = arrayListOf<String>()\n        try {\n            if (initWebDav()) {\n                var files = WebDav(url + \"YueDu/\").listFiles()\n                files = files.reversed()\n                for (index: Int in 0 until min(10, files.size)) {\n                    files[index].displayName?.let {\n                        names.add(it)\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        return names\n    }\n\n    fun showRestoreDialog(context: Context, names: ArrayList<String>, callBack: Restore.CallBack?): Boolean {\n        return if (names.isNotEmpty()) {\n            context.selector(title = \"选择恢复文件\", items = names) { _, index ->\n                if (index in 0 until names.size) {\n                    restoreWebDav(names[index], callBack)\n                }\n            }\n            true\n        } else {\n            false\n        }\n    }\n\n    private fun restoreWebDav(name: String, callBack: Restore.CallBack?) {\n        Single.create(SingleOnSubscribe<Boolean> { e ->\n            getWebDavUrl().let {\n                val file = WebDav(it + \"YueDu/\" + name)\n                file.downloadTo(zipFilePath, true)\n                @Suppress(\"BlockingMethodInNonBlockingContext\")\n                ZipUtils.unzipFile(zipFilePath, unzipFilesPath)\n            }\n            e.onSuccess(true)\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(object : MySingleObserver<Boolean>() {\n                    override fun onSuccess(t: Boolean) {\n                        Restore.restore(unzipFilesPath, callBack)\n                    }\n                })\n    }\n\n    fun backUpWebDav(path: String) {\n        try {\n            if (initWebDav()) {\n                val paths = arrayListOf(*Backup.backupFileNames)\n                for (i in 0 until paths.size) {\n                    paths[i] = path + File.separator + paths[i]\n                }\n                FileHelp.deleteFile(zipFilePath)\n                if (ZipUtils.zipFiles(paths, zipFilePath)) {\n                    WebDav(getWebDavUrl() + \"YueDu\").makeAsDir()\n                    val putUrl = getWebDavUrl() + \"YueDu/backup\" +\n                            SimpleDateFormat(\"yyyy-MM-dd\", Locale.getDefault())\n                                    .format(Date(System.currentTimeMillis())) + \".zip\"\n                    WebDav(putUrl).upload(zipFilePath)\n                }\n            }\n        } catch (e: Exception) {\n            Handler(Looper.getMainLooper()).post {\n                MApplication.getInstance().toast(\"WebDav\\n${e.localizedMessage}\")\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/BookSourceManager.java",
    "content": "package com.kunfei.bookshelf.model;\n\nimport android.database.Cursor;\nimport android.text.TextUtils;\n\nimport androidx.annotation.Nullable;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.dao.BookSourceBeanDao;\nimport com.kunfei.bookshelf.help.SourceHelp;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders;\nimport com.kunfei.bookshelf.model.impl.IHttpGetApi;\nimport com.kunfei.bookshelf.utils.GsonUtils;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\n/**\n * Created by GKF on 2017/12/15.\n * 所有书源\n */\n\npublic class BookSourceManager {\n\n    public static List<BookSourceBean> getSelectedBookSource() {\n        return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                .where(BookSourceBeanDao.Properties.Enable.eq(true))\n                .orderRaw(BookSourceBeanDao.Properties.Weight.columnName + \" DESC\")\n                .orderAsc(BookSourceBeanDao.Properties.SerialNumber)\n                .list();\n    }\n\n    public static List<BookSourceBean> getAllBookSource() {\n        return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                .orderRaw(getBookSourceSort())\n                .orderAsc(BookSourceBeanDao.Properties.SerialNumber)\n                .list();\n    }\n\n    public static List<BookSourceBean> getSelectedBookSourceBySerialNumber() {\n        return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                .where(BookSourceBeanDao.Properties.Enable.eq(true))\n                .orderAsc(BookSourceBeanDao.Properties.SerialNumber)\n                .list();\n    }\n\n    public static List<BookSourceBean> getAllBookSourceBySerialNumber() {\n        return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                .orderAsc(BookSourceBeanDao.Properties.SerialNumber)\n                .list();\n    }\n\n    public static List<BookSourceBean> getEnableSourceByGroup(String group) {\n        return DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                .where(BookSourceBeanDao.Properties.Enable.eq(true))\n                .where(BookSourceBeanDao.Properties.BookSourceGroup.like(\"%\" + group + \"%\"))\n                .orderRaw(BookSourceBeanDao.Properties.Weight.columnName + \" DESC\")\n                .list();\n    }\n\n    @Nullable\n    public static BookSourceBean getBookSourceByUrl(String url) {\n        if (url == null) return null;\n        return DbHelper.getDaoSession().getBookSourceBeanDao().load(url);\n    }\n\n    public static void removeBookSource(BookSourceBean sourceBean) {\n        if (sourceBean == null) return;\n        DbHelper.getDaoSession().getBookSourceBeanDao().delete(sourceBean);\n    }\n\n    public static String getBookSourceSort() {\n        switch (MApplication.getConfigPreferences().getInt(\"SourceSort\", 0)) {\n            case 1:\n                return BookSourceBeanDao.Properties.Weight.columnName + \" DESC\";\n            case 2:\n                return BookSourceBeanDao.Properties.BookSourceName.columnName + \" COLLATE LOCALIZED ASC\";\n            default:\n                return BookSourceBeanDao.Properties.SerialNumber.columnName + \" ASC\";\n        }\n    }\n\n    public static void addBookSource(List<BookSourceBean> bookSourceBeans) {\n        for (BookSourceBean bookSourceBean : bookSourceBeans) {\n            addBookSource(bookSourceBean);\n        }\n    }\n\n    public static void addBookSource(BookSourceBean bookSourceBean) {\n        if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl()))\n            return;\n        if (bookSourceBean.getBookSourceUrl().endsWith(\"/\")) {\n            bookSourceBean.setBookSourceUrl(bookSourceBean.getBookSourceUrl().replaceAll(\"/+$\", \"\"));\n        }\n        BookSourceBean temp = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                .where(BookSourceBeanDao.Properties.BookSourceUrl.eq(bookSourceBean.getBookSourceUrl())).unique();\n        if (temp != null) {\n            bookSourceBean.setSerialNumber(temp.getSerialNumber());\n        }\n        if (bookSourceBean.getSerialNumber() < 0) {\n            bookSourceBean.setSerialNumber((int) (DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder().count() + 1));\n        }\n        DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean);\n    }\n\n    public static void saveBookSource(BookSourceBean bookSourceBean) {\n        if (bookSourceBean != null) {\n            DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean);\n        }\n    }\n\n    public static Single<Boolean> toTop(BookSourceBean sourceBean) {\n        return Single.create((SingleOnSubscribe<Boolean>) e -> {\n            List<BookSourceBean> beanList = getAllBookSourceBySerialNumber();\n            for (int i = 0; i < beanList.size(); i++) {\n                beanList.get(i).setSerialNumber(i + 1);\n            }\n            sourceBean.setSerialNumber(0);\n            DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(beanList);\n            DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(sourceBean);\n            e.onSuccess(true);\n        }).compose(RxUtils::toSimpleSingle);\n    }\n\n    public static List<String> getEnableGroupList() {\n        List<String> groupList = new ArrayList<>();\n        String sql = \"SELECT DISTINCT \"\n                + BookSourceBeanDao.Properties.BookSourceGroup.columnName\n                + \" FROM \" + BookSourceBeanDao.TABLENAME\n                + \" WHERE \" + BookSourceBeanDao.Properties.Enable.name + \" = 1\";\n        Cursor cursor = DbHelper.getDaoSession().getDatabase().rawQuery(sql, null);\n        if (!cursor.moveToFirst()) return groupList;\n        do {\n            String group = cursor.getString(0);\n            if (TextUtils.isEmpty(group) || TextUtils.isEmpty(group.trim())) continue;\n            for (String item : group.split(\"\\\\s*[,;，；]\\\\s*\")) {\n                if (TextUtils.isEmpty(item) || groupList.contains(item)) continue;\n                groupList.add(item);\n            }\n        } while (cursor.moveToNext());\n        Collections.sort(groupList);\n        return groupList;\n    }\n\n    public static List<String> getGroupList() {\n        List<String> groupList = new ArrayList<>();\n        String sql = \"SELECT DISTINCT \" + BookSourceBeanDao.Properties.BookSourceGroup.columnName + \" FROM \" + BookSourceBeanDao.TABLENAME;\n        Cursor cursor = DbHelper.getDaoSession().getDatabase().rawQuery(sql, null);\n        if (!cursor.moveToFirst()) return groupList;\n        do {\n            String group = cursor.getString(0);\n            if (TextUtils.isEmpty(group) || TextUtils.isEmpty(group.trim())) continue;\n            for (String item : group.split(\"\\\\s*[,;，；]\\\\s*\")) {\n                if (TextUtils.isEmpty(item) || groupList.contains(item)) continue;\n                groupList.add(item);\n            }\n        } while (cursor.moveToNext());\n        Collections.sort(groupList);\n        return groupList;\n    }\n\n    public static Observable<List<BookSourceBean>> importSource(String string) {\n        if (StringUtils.isTrimEmpty(string)) return null;\n        string = string.trim();\n        if (NetworkUtils.isIPv4Address(string)) {\n            string = String.format(\"http://%s:65501\", string);\n        }\n        if (StringUtils.isJsonType(string)) {\n            return importBookSourceFromJson(string.trim())\n                    .compose(RxUtils::toSimpleSingle);\n        }else if(StringUtils.isCompressJsonType(string)){\n            return importBookSourceFromJson(StringUtils.unCompressJson(string))\n                    .compose(RxUtils::toSimpleSingle);\n        }\n        if (NetworkUtils.isUrl(string)) {\n            return BaseModelImpl.getInstance().getRetrofitString(StringUtils.getBaseUrl(string), \"utf-8\")\n                    .create(IHttpGetApi.class)\n                    .get(string, AnalyzeHeaders.getDefaultHeader())\n                    .flatMap(rsp -> importBookSourceFromJson(rsp.body()))\n                    .compose(RxUtils::toSimpleSingle);\n        }\n        return Observable.error(new Exception(\"不是Json或Url格式\"));\n    }\n\n    private static Observable<List<BookSourceBean>> importBookSourceFromJson(String json) {\n        return Observable.create(e -> {\n            List<BookSourceBean> bookSourceBeans = new ArrayList<>();\n            if (StringUtils.isJsonArray(json)) {\n                try {\n                    bookSourceBeans = GsonUtils.parseJArray(json, BookSourceBean.class);\n                    for (BookSourceBean bookSourceBean : bookSourceBeans) {\n                        if (bookSourceBean.containsGroup(\"删除\")) {\n                            DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                                    .where(BookSourceBeanDao.Properties.BookSourceUrl.eq(bookSourceBean.getBookSourceUrl()))\n                                    .buildDelete().executeDeleteWithoutDetachingEntities();\n                        } else {\n                            try {\n                                new URL(bookSourceBean.getBookSourceUrl());\n                                bookSourceBean.setSerialNumber(0);\n                                SourceHelp.INSTANCE.insertBookSource(bookSourceBean);\n                            } catch (Exception exception) {\n                                DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                                        .where(BookSourceBeanDao.Properties.BookSourceUrl.eq(bookSourceBean.getBookSourceUrl()))\n                                        .buildDelete().executeDeleteWithoutDetachingEntities();\n                            }\n                        }\n                    }\n                    e.onNext(bookSourceBeans);\n                    e.onComplete();\n                    return;\n                } catch (Exception ignored) {\n                }\n            }\n            if (StringUtils.isJsonObject(json)) {\n                try {\n                    BookSourceBean bookSourceBean = GsonUtils.parseJObject(json, BookSourceBean.class);\n                    addBookSource(bookSourceBean);\n                    bookSourceBeans.add(bookSourceBean);\n                    e.onNext(bookSourceBeans);\n                    e.onComplete();\n                    return;\n                } catch (Exception ignored) {\n                }\n            }\n            e.onError(new Throwable(\"格式不对\"));\n        });\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/Exceptions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.model\n\nclass AppException(msg: String) : Exception(msg)\n\n/**\n *\n */\nclass NoStackTraceException(msg: String) : Exception(msg) {\n\n    override fun fillInStackTrace(): Throwable {\n        return this\n    }\n\n}\n\n/**\n * 目录为空\n */\nclass TocEmptyException(msg: String) : Exception(msg) {\n\n    override fun fillInStackTrace(): Throwable {\n        return this\n    }\n\n}\n\n/**\n * 内容为空\n */\nclass ContentEmptyException(msg: String) : Exception(msg) {\n\n    override fun fillInStackTrace(): Throwable {\n        return this\n    }\n\n}\n\n/**\n * 并发限制\n */\nclass ConcurrentException(msg: String, val waitTime: Long) : Exception(msg) {\n\n    override fun fillInStackTrace(): Throwable {\n        return this\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/ImportBookModel.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.model;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.LocBookShelfBean;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\n\nimport java.io.File;\n\nimport io.reactivex.Observable;\n\nimport static com.kunfei.bookshelf.utils.StringUtils.getString;\n\npublic class ImportBookModel extends BaseModelImpl {\n\n    public static ImportBookModel getInstance() {\n        return new ImportBookModel();\n    }\n\n    public Observable<LocBookShelfBean> importBook(final File file) {\n        return Observable.create(e -> {\n            //判断文件是否存在\n\n            boolean isNew = false;\n\n            BookShelfBean bookShelfBean = BookshelfHelp.getBook(file.getAbsolutePath());\n            if (bookShelfBean == null) {\n                isNew = true;\n                bookShelfBean = new BookShelfBean();\n                bookShelfBean.setHasUpdate(true);\n                bookShelfBean.setFinalDate(System.currentTimeMillis());\n                bookShelfBean.setDurChapter(0);\n                bookShelfBean.setDurChapterPage(0);\n                bookShelfBean.setGroup(3);\n                bookShelfBean.setTag(BookShelfBean.LOCAL_TAG);\n                bookShelfBean.setNoteUrl(file.getAbsolutePath());\n                bookShelfBean.setAllowUpdate(false);\n\n                BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean();\n                String fileName = file.getName();\n                int lastDotIndex = file.getName().lastIndexOf(\".\");\n                if (lastDotIndex > 0)\n                    fileName = fileName.substring(0, lastDotIndex);\n                int authorIndex = fileName.indexOf(\"作者\");\n                if (authorIndex != -1) {\n                    bookInfoBean.setAuthor(fileName.substring(authorIndex));\n                    fileName = fileName.substring(0, authorIndex).trim();\n                } else {\n                    bookInfoBean.setAuthor(\"\");\n                }\n                int smhStart = fileName.indexOf(\"《\");\n                int smhEnd = fileName.indexOf(\"》\");\n                if (smhStart != -1 && smhEnd != -1) {\n                    bookInfoBean.setName(fileName.substring(smhStart + 1, smhEnd));\n                } else {\n                    bookInfoBean.setName(fileName);\n                }\n                bookInfoBean.setFinalRefreshData(file.lastModified());\n                bookInfoBean.setCoverUrl(\"\");\n                bookInfoBean.setNoteUrl(file.getAbsolutePath());\n                bookInfoBean.setTag(BookShelfBean.LOCAL_TAG);\n                bookInfoBean.setOrigin(getString(R.string.local));\n\n                DbHelper.getDaoSession().getBookInfoBeanDao().insertOrReplace(bookInfoBean);\n                DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean);\n            }\n            e.onNext(new LocBookShelfBean(isNew, bookShelfBean));\n            e.onComplete();\n        });\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/ReplaceRuleManager.java",
    "content": "package com.kunfei.bookshelf.model;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\nimport com.kunfei.bookshelf.dao.ReplaceRuleBeanDao;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeHeaders;\nimport com.kunfei.bookshelf.model.impl.IHttpGetApi;\nimport com.kunfei.bookshelf.utils.GsonUtils;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\n/**\n * Created by GKF on 2018/2/12.\n * 替换规则管理\n */\n\npublic class ReplaceRuleManager {\n    private static List<ReplaceRuleBean> replaceRuleBeansEnabled;\n\n    public static List<ReplaceRuleBean> getEnabled() {\n        if (replaceRuleBeansEnabled == null) {\n            replaceRuleBeansEnabled = DbHelper.getDaoSession()\n                    .getReplaceRuleBeanDao().queryBuilder()\n                    .where(ReplaceRuleBeanDao.Properties.Enable.eq(true))\n                    .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber)\n                    .list();\n        }\n        return replaceRuleBeansEnabled;\n    }\n\n    // 合并广告话术规则\n    public static Single<Boolean> mergeAdRules(ReplaceRuleBean replaceRuleBean) {\n\n\n        String rule = formateAdRule(replaceRuleBean.getRegex());\n\n/*        String summary=replaceRuleBean.getReplaceSummary();\n        if(summary==null)\n            summary=\"\";\n        String sumary_pre=summary.split(\"-\")[0];*/\n\n        int sn = replaceRuleBean.getSerialNumber();\n        if (sn == 0) {\n            sn = (int) (DbHelper.getDaoSession().getReplaceRuleBeanDao().queryBuilder().count() + 1);\n            replaceRuleBean.setSerialNumber(sn);\n        }\n\n        List<ReplaceRuleBean> list = DbHelper.getDaoSession()\n                .getReplaceRuleBeanDao().queryBuilder()\n                .where(ReplaceRuleBeanDao.Properties.Enable.eq(true))\n                .where(ReplaceRuleBeanDao.Properties.ReplaceSummary.eq(replaceRuleBean.getReplaceSummary()))\n                .where(ReplaceRuleBeanDao.Properties.SerialNumber.notEq(sn))\n                .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber)\n                .list();\n        if (list.size() < 1) {\n            replaceRuleBean.setRegex(rule);\n            return saveData(replaceRuleBean);\n        } else {\n            StringBuffer buffer = new StringBuffer(rule);\n            for (ReplaceRuleBean li : list) {\n                buffer.append('\\n');\n                buffer.append(li.getRegex());\n//                    buffer.append(formateAdRule(rule.getRegex()));\n            }\n            replaceRuleBean.setRegex(formateAdRule(buffer.toString()));\n\n            return Single.create((SingleOnSubscribe<Boolean>) emitter -> {\n\n                DbHelper.getDaoSession().getReplaceRuleBeanDao().insertOrReplace(replaceRuleBean);\n                for (ReplaceRuleBean li : list) {\n                    DbHelper.getDaoSession().getReplaceRuleBeanDao().delete(li);\n                }\n                refreshDataS();\n                emitter.onSuccess(true);\n            }).compose(RxUtils::toSimpleSingle);\n\n        }\n    }\n\n    // 把输入的规则进行预处理（分段、排序、去重）。保存的是普通多行文本。\n    public static String formateAdRule(String rule) {\n\n        if (rule == null)\n            return \"\";\n        String result = rule.trim();\n        if (result.length() < 1)\n            return \"\";\n\n        String string = rule\n//                用中文中的.视为。进行分段\n                .replaceAll(\"(?<=([^a-zA-Z\\\\p{P}]{4,8}))\\\\.+(?![^a-zA-Z\\\\p{P}]{4,8})\",\"\\n\")\n//                用常见的适合分段的标点进行分段，句首句尾除外\n//                .replaceAll(\"([^\\\\p{P}\\n^])([…,，:：？。！?!~<>《》【】（）()]+)([^\\\\p{P}\\n$])\", \"$1\\n$3\")\n//                表达式无法解决句尾连续多个符号的问题\n//                .replaceAll(\"[…,，:：？。！?!~<>《》【】（）()]+(?!\\\\s*\\n|$)\", \"\\n\")\n                .replaceAll(\"(?<![\\\\p{P}\\n^])([…,，:：？。！?!~<>《》【】（）()]+)(?![\\\\p{P}\\n$])\", \"\\n\")\n\n                ;\n\n        String[] lines = string.split(\"\\n\");\n        List<String> list = new ArrayList<>();\n\n        for (String s : lines) {\n            s = s.trim()\n//                    .replaceAll(\"\\\\s+\", \"\\\\s\")\n            ;\n            if (!list.contains(s)) {\n                list.add(s);\n            }\n        }\n        Collections.sort(list);\n        StringBuffer buffer = new StringBuffer(rule.length() + 1);\n        for (int i = 0; i < list.size(); i++) {\n            buffer.append('\\n');\n            buffer.append(list.get(i));\n        }\n        return buffer.toString().trim();\n    }\n\n    public static Single<List<ReplaceRuleBean>> getAll() {\n        return Single.create((SingleOnSubscribe<List<ReplaceRuleBean>>) emitter -> emitter.onSuccess(DbHelper.getDaoSession()\n                .getReplaceRuleBeanDao().queryBuilder()\n                .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber)\n                .list())).compose(RxUtils::toSimpleSingle);\n    }\n\n    public static Single<Boolean> saveData(ReplaceRuleBean replaceRuleBean) {\n        return Single.create((SingleOnSubscribe<Boolean>) emitter -> {\n            if (replaceRuleBean.getSerialNumber() == 0) {\n                replaceRuleBean.setSerialNumber((int) (DbHelper.getDaoSession().getReplaceRuleBeanDao().queryBuilder().count() + 1));\n            }\n            DbHelper.getDaoSession().getReplaceRuleBeanDao().insertOrReplace(replaceRuleBean);\n            refreshDataS();\n            emitter.onSuccess(true);\n        }).compose(RxUtils::toSimpleSingle);\n    }\n\n    public static void delData(ReplaceRuleBean replaceRuleBean) {\n        DbHelper.getDaoSession().getReplaceRuleBeanDao().delete(replaceRuleBean);\n        refreshDataS();\n    }\n\n    public static void addDataS(List<ReplaceRuleBean> replaceRuleBeans) {\n        if (replaceRuleBeans != null && replaceRuleBeans.size() > 0) {\n            DbHelper.getDaoSession().getReplaceRuleBeanDao().insertOrReplaceInTx(replaceRuleBeans);\n            refreshDataS();\n        }\n    }\n\n    public static void delDataS(List<ReplaceRuleBean> replaceRuleBeans) {\n        for (ReplaceRuleBean replaceRuleBean : replaceRuleBeans) {\n            DbHelper.getDaoSession().getReplaceRuleBeanDao().delete(replaceRuleBean);\n        }\n        refreshDataS();\n    }\n\n    private static void refreshDataS() {\n        replaceRuleBeansEnabled = DbHelper.getDaoSession()\n                .getReplaceRuleBeanDao().queryBuilder()\n                .where(ReplaceRuleBeanDao.Properties.Enable.eq(true))\n                .orderAsc(ReplaceRuleBeanDao.Properties.SerialNumber)\n                .list();\n    }\n\n    public static Observable<Boolean> importReplaceRule(String text) {\n        if (TextUtils.isEmpty(text)) return null;\n        text = text.trim();\n        if (text.length() == 0) return null;\n        if (StringUtils.isJsonType(text)) {\n            return importReplaceRuleO(text)\n                    .compose(RxUtils::toSimpleSingle);\n        }\n        if (NetworkUtils.isUrl(text)) {\n            return BaseModelImpl.getInstance().getRetrofitString(StringUtils.getBaseUrl(text), \"utf-8\")\n                    .create(IHttpGetApi.class)\n                    .get(text, AnalyzeHeaders.getDefaultHeader())\n                    .flatMap(rsp -> importReplaceRuleO(rsp.body()))\n                    .compose(RxUtils::toSimpleSingle);\n        }\n        return Observable.error(new Exception(\"不是Json或Url格式\"));\n    }\n\n    private static Observable<Boolean> importReplaceRuleO(String json) {\n        return Observable.create(e -> {\n            try {\n                List<ReplaceRuleBean> replaceRuleBeans = GsonUtils.parseJArray(json, ReplaceRuleBean.class);\n                addDataS(replaceRuleBeans);\n                e.onNext(true);\n            } catch (Exception e1) {\n                e1.printStackTrace();\n                e.onNext(false);\n            }\n            e.onComplete();\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/SavedSource.java",
    "content": "package com.kunfei.bookshelf.model;\n\nimport com.kunfei.bookshelf.bean.BookSourceBean;\n\npublic class SavedSource {\n    public static SavedSource Instance = new SavedSource();\n\n    private String bookName;\n    private long saveTime;\n    private String sourceUrl;\n\n    private SavedSource() {\n        this.bookName = \"\";\n        saveTime = 0;\n    }\n\n    public String getBookName() {\n        return this.bookName;\n    }\n\n    public void setBookName(String bookName) {\n        this.bookName = bookName;\n    }\n\n    public long getSaveTime() {\n        return saveTime;\n    }\n\n    public void setSaveTime(long saveTime) {\n        this.saveTime = saveTime;\n    }\n\n    public BookSourceBean getBookSource() {\n        if (sourceUrl == null) {\n            return null;\n        }\n        return BookSourceManager.getBookSourceByUrl(sourceUrl);\n    }\n\n    public void setBookSource(BookSourceBean bookSource) {\n        if (bookSource != null) {\n            this.sourceUrl = bookSource.getBookSourceUrl();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/SearchBookModel.java",
    "content": "package com.kunfei.bookshelf.model;\n\nimport android.os.Handler;\nimport android.os.Looper;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\nimport io.reactivex.Observer;\nimport io.reactivex.Scheduler;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * Created by GKF on 2018/1/16.\n * 搜索\n */\n\npublic class SearchBookModel {\n    private Handler handler = new Handler(Looper.getMainLooper());\n    private ExecutorService executorService;\n    private Scheduler scheduler;\n    private long startThisSearchTime;\n    private List<SearchEngine> searchEngineS = new ArrayList<>();\n    private int threadsNum;\n    private int search_result_filter_grade;\n    private int page = 0;\n    private int searchEngineIndex;\n    private int searchSuccessNum;\n    private CompositeDisposable compositeDisposable;\n    private OnSearchListener searchListener;\n\n    public SearchBookModel(OnSearchListener searchListener) {\n        this(searchListener, BookSourceManager.getSelectedBookSource());\n    }\n\n    public SearchBookModel(OnSearchListener searchListener, List<BookSourceBean> sourceBeanList) {\n        this.searchListener = searchListener;\n        threadsNum = MApplication.getConfigPreferences().getInt(MApplication.getInstance().getString(R.string.pk_threads_num), 6);\n        executorService = Executors.newFixedThreadPool(threadsNum);\n        scheduler = Schedulers.from(executorService);\n        compositeDisposable = new CompositeDisposable();\n        search_result_filter_grade = MApplication.getConfigPreferences().getInt(MApplication.getInstance().getString(R.string.pk_search_result_filter_grade), 0);\n        if (sourceBeanList == null) {\n            initSearchEngineS(BookSourceManager.getSelectedBookSource());\n        } else {\n            initSearchEngineS(sourceBeanList);\n        }\n    }\n\n    /**\n     * 搜索引擎初始化\n     */\n    public void initSearchEngineS(@NonNull List<BookSourceBean> sourceBeanList) {\n        searchEngineS.clear();\n        for (BookSourceBean bookSourceBean : sourceBeanList) {\n            if (bookSourceBean.getEnable()) {\n                SearchEngine se = new SearchEngine();\n                se.setTag(bookSourceBean.getBookSourceUrl());\n                se.setHasMore(true);\n                searchEngineS.add(se);\n            }\n        }\n    }\n\n    public void searchReNew() {\n        compositeDisposable.dispose();\n        compositeDisposable = new CompositeDisposable();\n        page = 0;\n        for (SearchEngine searchEngine : searchEngineS) {\n            searchEngine.setHasMore(true);\n        }\n    }\n\n    public void stopSearch() {\n        compositeDisposable.dispose();\n        compositeDisposable = new CompositeDisposable();\n        handler.post(() -> {\n            searchListener.refreshFinish(true);\n            searchListener.loadMoreFinish(true);\n        });\n    }\n\n    private void searchBookError(Throwable throwable) {\n        compositeDisposable.dispose();\n        compositeDisposable = new CompositeDisposable();\n        handler.post(() -> {\n            searchListener.refreshFinish(true);\n            searchListener.loadMoreFinish(true);\n            searchListener.searchBookError(throwable);\n        });\n    }\n\n    public void onDestroy() {\n        stopSearch();\n        executorService.shutdown();\n    }\n\n    public void setSearchTime(long searchTime) {\n        this.startThisSearchTime = searchTime;\n    }\n\n    public void search(final String content, final long searchTime, List<BookShelfBean> bookShelfS, Boolean fromError) {\n        if (searchTime != startThisSearchTime) {\n            return;\n        }\n        if (!fromError) {\n            page++;\n        }\n        if (page == 0) {\n            page = 1;\n        }\n        if (page == 1) {\n            handler.post(() -> searchListener.refreshSearchBook());\n        }\n        if (searchEngineS.size() == 0) {\n            searchBookError(new Throwable(\"没有选中任何书源\"));\n            return;\n        }\n        searchSuccessNum = 0;\n        searchEngineIndex = -1;\n        for (int i = 0; i < threadsNum; i++) {\n            searchOnEngine(content, bookShelfS, searchTime);\n        }\n    }\n\n    private synchronized void searchOnEngine(final String content, List<BookShelfBean> bookShelfS, final long searchTime) {\n        if (searchTime != startThisSearchTime) {\n            return;\n        }\n        searchEngineIndex++;\n        long startTime = System.currentTimeMillis();\n        if (searchEngineIndex < searchEngineS.size()) {\n            final SearchEngine searchEngine = searchEngineS.get(searchEngineIndex);\n            if (searchEngine.getHasMore()) {\n                WebBookModel.getInstance()\n                        .searchBook(content, page, searchEngine.getTag())\n                        .subscribeOn(scheduler)\n                        .observeOn(AndroidSchedulers.mainThread())\n                        .subscribe(new Observer<List<SearchBookBean>>() {\n                            @Override\n                            public void onSubscribe(Disposable d) {\n                                compositeDisposable.add(d);\n                            }\n\n                            @Override\n                            public void onNext(List<SearchBookBean> searchBookBeans) {\n                                if (searchTime == startThisSearchTime) {\n                                    searchSuccessNum++;\n                                    if (searchBookBeans.size() > 0) {\n                                        for (SearchBookBean temp : searchBookBeans) {\n                                            int searchTime = (int) (System.currentTimeMillis() - startTime) / 1000;\n                                            temp.setSearchTime(searchTime);\n                                            for (BookShelfBean bookShelfBean : bookShelfS) {\n                                                if (Objects.equals(bookShelfBean.getNoteUrl(), temp.getNoteUrl())) {\n                                                    temp.setIsCurrentSource(true);\n                                                    break;\n                                                }\n                                            }\n                                        }\n\n                                            if(search_result_filter_grade>0){\n                                                for(int index=0;index<searchBookBeans.size();index++){\n                                                    SearchBookBean temp=searchBookBeans.get(index);\n                                                    String r=temp.getName()+temp.getAuthor();\n                                                    char[] s=content.replaceAll(\"\\\\s\",\"\").toCharArray();\n                                                    int j=9-search_result_filter_grade;\n\n                                                    for(int i=0;i<s.length;i++){\n                                                        if(r.indexOf(s[i])<0){\n                                                            j--;\n                                                        }\n                                                        if(j<0){\n//                                                            Log.d(\"search_result_filter=\"+search_result_filter_grade,\"search=\"+content+\", result=\"+r);\n                                                            searchBookBeans.remove(index);\n                                                            index--;\n                                                            break;\n                                                        }\n                                                    }\n                                                }\n\n                                        }else{\n//                                                Log.d(\"search_result_filter=\"+search_result_filter_grade,\"search=\"+content);\n                                            }\n\n                                        searchListener.loadMoreSearchBook(searchBookBeans);\n                                    } else {\n                                        searchEngine.setHasMore(false);\n                                    }\n                                    searchOnEngine(content, bookShelfS, searchTime);\n                                }\n                            }\n\n                            @Override\n                            public void onError(Throwable e) {\n                                e.printStackTrace();\n                                searchEngine.setHasMore(false);\n                                searchOnEngine(content, bookShelfS, searchTime);\n                            }\n\n                            @Override\n                            public void onComplete() {\n\n                            }\n                        });\n            } else {\n                searchOnEngine(content, bookShelfS, searchTime);\n            }\n        } else {\n            if (searchEngineIndex >= searchEngineS.size() + threadsNum - 1) {\n                if (searchSuccessNum == 0 && searchListener.getItemCount() == 0) {\n                    if (page == 1) {\n                        searchBookError(new Throwable(\"未搜索到内容\"));\n                    } else {\n                        searchBookError(new Throwable(\"未搜索到更多内容\"));\n                    }\n                } else {\n                    if (page == 1) {\n                        handler.post(() -> searchListener.refreshFinish(false));\n                    }\n                    for (SearchEngine engine : searchEngineS) {\n                        if (engine.hasMore) {\n                            handler.post(() -> searchListener.loadMoreFinish(false));\n                            return;\n                        }\n                    }\n                    handler.post(() -> searchListener.loadMoreFinish(true));\n                }\n            }\n        }\n    }\n\n    public int getPage() {\n        return page;\n    }\n\n    public void setPage(int page) {\n        this.page = page;\n    }\n\n    public interface OnSearchListener {\n        void refreshSearchBook();\n\n        void refreshFinish(Boolean isAll);\n\n        void loadMoreFinish(Boolean isAll);\n\n        void loadMoreSearchBook(List<SearchBookBean> searchBookBeanList);\n\n        void searchBookError(Throwable throwable);\n\n        int getItemCount();\n    }\n\n    private static class SearchEngine {\n        private String tag;\n        private Boolean hasMore;\n\n        public String getTag() {\n            return tag;\n        }\n\n        public void setTag(String tag) {\n            this.tag = tag;\n        }\n\n        Boolean getHasMore() {\n            return hasMore;\n        }\n\n        void setHasMore(Boolean hasMore) {\n            this.hasMore = hasMore;\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/TxtChapterRuleManager.java",
    "content": "package com.kunfei.bookshelf.model;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.bean.TxtChapterRuleBean;\nimport com.kunfei.bookshelf.dao.TxtChapterRuleBeanDao;\nimport com.kunfei.bookshelf.utils.GsonUtils;\nimport com.kunfei.bookshelf.utils.IOUtils;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class TxtChapterRuleManager {\n\n    public static List<TxtChapterRuleBean> getAll() {\n        List<TxtChapterRuleBean> beans = DbHelper.getDaoSession().getTxtChapterRuleBeanDao().loadAll();\n        if (beans.isEmpty()) {\n            return getDefault();\n        }\n        return beans;\n    }\n\n    public static List<TxtChapterRuleBean> getEnabled() {\n        List<TxtChapterRuleBean> beans = DbHelper.getDaoSession().getTxtChapterRuleBeanDao().queryBuilder()\n                .where(TxtChapterRuleBeanDao.Properties.Enable.eq(true))\n                .list();\n        if (beans.isEmpty()) {\n            return getAll();\n        }\n        return beans;\n    }\n\n    public static List<String> enabledRuleList() {\n        List<TxtChapterRuleBean> beans = getEnabled();\n        List<String> ruleList = new ArrayList<>();\n        for (TxtChapterRuleBean chapterRuleBean : beans) {\n            ruleList.add(chapterRuleBean.getRule());\n        }\n        return ruleList;\n    }\n\n    public static List<TxtChapterRuleBean> getDefault() {\n        String json = null;\n        try {\n            InputStream inputStream = MApplication.getInstance().getAssets().open(\"txtChapterRule.json\");\n            json = IOUtils.toString(inputStream);\n            inputStream.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        List<TxtChapterRuleBean> ruleBeanList = GsonUtils.parseJArray(json, TxtChapterRuleBean.class);\n        if (ruleBeanList != null) {\n            DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplaceInTx(ruleBeanList);\n            return ruleBeanList;\n        }\n        return new ArrayList<>();\n    }\n\n    public static void del(TxtChapterRuleBean txtChapterRuleBean) {\n        DbHelper.getDaoSession().getTxtChapterRuleBeanDao().delete(txtChapterRuleBean);\n    }\n\n    public static void del(List<TxtChapterRuleBean> ruleBeanList) {\n        for (TxtChapterRuleBean ruleBean : ruleBeanList) {\n            del(ruleBean);\n        }\n    }\n\n    public static void save(TxtChapterRuleBean txtChapterRuleBean) {\n        if (txtChapterRuleBean.getSerialNumber() == null) {\n            txtChapterRuleBean.setSerialNumber((int) DbHelper.getDaoSession().getTxtChapterRuleBeanDao().queryBuilder().count());\n        }\n        DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplace(txtChapterRuleBean);\n    }\n\n    public static void save(List<TxtChapterRuleBean> txtChapterRuleBeans) {\n        DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplaceInTx(txtChapterRuleBeans);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/UpLastChapterModel.java",
    "content": "package com.kunfei.bookshelf.model;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.text.TextUtils;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.dao.SearchBookBeanDao;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.Observer;\nimport io.reactivex.Scheduler;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * 更新换源列表里最新章节\n */\npublic class UpLastChapterModel {\n    public static UpLastChapterModel model;\n    private CompositeDisposable compositeDisposable;\n    private ExecutorService executorService;\n    private Scheduler scheduler;\n    private Handler handler = new Handler(Looper.getMainLooper());\n    private List<SearchBookBean> searchBookBeanList;\n    private int upIndex;\n\n    public static UpLastChapterModel getInstance() {\n        if (model == null) {\n            model = new UpLastChapterModel();\n        }\n        return model;\n    }\n\n    private UpLastChapterModel() {\n        executorService = Executors.newFixedThreadPool(5);\n        scheduler = Schedulers.from(executorService);\n        compositeDisposable = new CompositeDisposable();\n        searchBookBeanList = new ArrayList<>();\n    }\n\n    public void startUpdate() {\n        if (!MApplication.getConfigPreferences().getBoolean(\"upChangeSourceLastChapter\", false))\n            return;\n        if (compositeDisposable.size() > 0) return;\n        List<SearchBookBean> beanList = new ArrayList<>();\n        Observable.create((ObservableOnSubscribe<BookShelfBean>) e -> {\n            List<BookShelfBean> bookShelfBeans = BookshelfHelp.getAllBook();\n            for (BookShelfBean bookShelfBean : bookShelfBeans) {\n                if (!Objects.equals(bookShelfBean.getTag(), BookShelfBean.LOCAL_TAG)) {\n                    e.onNext(bookShelfBean);\n                }\n            }\n            e.onComplete();\n        }).flatMap(this::findSearchBookBean)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<SearchBookBean>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(SearchBookBean searchBookBean) {\n                        beanList.add(searchBookBean);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                    }\n\n                    @Override\n                    public void onComplete() {\n                        startUpdate(beanList);\n                    }\n                });\n    }\n\n    public synchronized void startUpdate(List<SearchBookBean> beanList) {\n        compositeDisposable.dispose();\n        executorService.shutdown();\n        executorService = Executors.newFixedThreadPool(5);\n        scheduler = Schedulers.from(executorService);\n        compositeDisposable = new CompositeDisposable();\n        this.searchBookBeanList = beanList;\n        upIndex = -1;\n        for (int i = 0; i < 5; i++) {\n            doUpdate();\n        }\n    }\n\n    private synchronized void doUpdate() {\n        upIndex++;\n        if (upIndex < searchBookBeanList.size()) {\n            toBookshelf(searchBookBeanList.get(upIndex))\n                    .flatMap(this::getChapterList)\n                    .flatMap(this::saveSearchBookBean)\n                    .subscribeOn(scheduler)\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .subscribe(new Observer<SearchBookBean>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            compositeDisposable.add(d);\n                            handler.postDelayed(() -> {\n                                if (!d.isDisposed()) {\n                                    d.dispose();\n                                    doUpdate();\n                                }\n                            }, 20 * 1000);\n                        }\n\n                        @Override\n                        public void onNext(SearchBookBean searchBookBean) {\n                            RxBus.get().post(RxBusTag.UP_SEARCH_BOOK, searchBookBean);\n                            doUpdate();\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            doUpdate();\n                        }\n\n                        @Override\n                        public void onComplete() {\n\n                        }\n                    });\n        }\n    }\n\n    private void stopUp() {\n        if (compositeDisposable != null && !compositeDisposable.isDisposed()) {\n            compositeDisposable.dispose();\n        }\n        compositeDisposable = new CompositeDisposable();\n    }\n\n    public static void destroy() {\n        if (model != null) {\n            model.stopUp();\n            model.executorService.shutdownNow();\n            model = null;\n        }\n    }\n\n    private Observable<SearchBookBean> findSearchBookBean(BookShelfBean bookShelf) {\n        return Observable.create(e -> {\n            List<SearchBookBean> searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder()\n                    .where(SearchBookBeanDao.Properties.Name.eq(bookShelf.getBookInfoBean().getName())).list();\n            for (SearchBookBean searchBookBean : searchBookBeans) {\n                BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(searchBookBean.getTag());\n                if (sourceBean == null) {\n                    DbHelper.getDaoSession().getSearchBookBeanDao().delete(searchBookBean);\n                } else if (System.currentTimeMillis() - searchBookBean.getUpTime() > 1000 * 60 * 60\n                        && sourceBean.getEnable()) {\n                    e.onNext(searchBookBean);\n                }\n            }\n            e.onComplete();\n        });\n    }\n\n    private Observable<BookShelfBean> toBookshelf(SearchBookBean searchBookBean) {\n        return Observable.create(e -> {\n            BookShelfBean bookShelfBean = BookshelfHelp.getBookFromSearchBook(searchBookBean);\n            e.onNext(bookShelfBean);\n            e.onComplete();\n        });\n    }\n\n    private Observable<List<BookChapterBean>> getChapterList(BookShelfBean bookShelfBean) {\n        if (TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getChapterUrl())) {\n            return WebBookModel.getInstance().getBookInfo(bookShelfBean)\n                    .flatMap(bookShelf -> WebBookModel.getInstance().getChapterList(bookShelf));\n        } else {\n            return WebBookModel.getInstance().getChapterList(bookShelfBean);\n        }\n    }\n\n    private Observable<SearchBookBean> saveSearchBookBean(List<BookChapterBean> chapterBeanList) {\n        return Observable.create(e -> {\n            BookChapterBean chapterBean = chapterBeanList.get(chapterBeanList.size() - 1);\n            SearchBookBean searchBookBean = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder()\n                    .where(SearchBookBeanDao.Properties.NoteUrl.eq(chapterBean.getNoteUrl()))\n                    .unique();\n            if (searchBookBean != null) {\n                searchBookBean.setLastChapter(chapterBean.getDurChapterName());\n                searchBookBean.setAddTime(System.currentTimeMillis());\n                DbHelper.getDaoSession().getSearchBookBeanDao().insertOrReplace(searchBookBean);\n                e.onNext(searchBookBean);\n            }\n            e.onComplete();\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/WebBookModel.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.model;\n\nimport android.annotation.SuppressLint;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.bean.BaseChapterBean;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.content.WebBook;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observable;\n\nimport static android.text.TextUtils.isEmpty;\nimport static com.kunfei.bookshelf.constant.AppConstant.TIME_OUT;\n\npublic class WebBookModel {\n\n    public static WebBookModel getInstance() {\n        return new WebBookModel();\n    }\n\n    /**\n     * 网络请求并解析书籍信息\n     * return BookShelfBean\n     */\n    public Observable<BookShelfBean> getBookInfo(BookShelfBean bookShelfBean) {\n        return WebBook.getInstance(bookShelfBean.getTag())\n                .getBookInfo(bookShelfBean)\n                .timeout(TIME_OUT, TimeUnit.SECONDS);\n    }\n\n    /**\n     * 网络解析图书目录\n     * return BookShelfBean\n     */\n    public Observable<List<BookChapterBean>> getChapterList(final BookShelfBean bookShelfBean) {\n        return WebBook.getInstance(bookShelfBean.getTag())\n                .getChapterList(bookShelfBean)\n                .flatMap((chapterList) -> upChapterList(bookShelfBean, chapterList))\n                .timeout(TIME_OUT, TimeUnit.SECONDS);\n    }\n\n    /**\n     * 章节缓存\n     */\n    public Observable<BookContentBean> getBookContent(BookShelfBean bookShelfBean, BaseChapterBean chapterBean, BaseChapterBean nextChapterBean) {\n        return WebBook.getInstance(chapterBean.getTag())\n                .getBookContent(chapterBean, nextChapterBean, bookShelfBean)\n                .flatMap((bookContentBean -> saveContent(bookShelfBean.getBookInfoBean(), chapterBean, bookContentBean)))\n                .timeout(TIME_OUT, TimeUnit.SECONDS);\n    }\n\n    /**\n     * 搜索\n     */\n    public Observable<List<SearchBookBean>> searchBook(String content, int page, String tag) {\n        return WebBook.getInstance(tag)\n                .searchBook(content, page)\n                .timeout(TIME_OUT, TimeUnit.SECONDS);\n    }\n\n    /**\n     * 发现页\n     */\n    public Observable<List<SearchBookBean>> findBook(String url, int page, String tag) {\n        return WebBook.getInstance(tag)\n                .findBook(url, page)\n                .timeout(TIME_OUT, TimeUnit.SECONDS);\n    }\n\n    /**\n     * 更新目录\n     */\n    private Observable<List<BookChapterBean>> upChapterList(BookShelfBean bookShelfBean, List<BookChapterBean> chapterList) {\n        return Observable.create(e -> {\n            for (int i = 0; i < chapterList.size(); i++) {\n                BookChapterBean chapter = chapterList.get(i);\n                chapter.setDurChapterIndex(i);\n                chapter.setTag(bookShelfBean.getTag());\n                chapter.setNoteUrl(bookShelfBean.getNoteUrl());\n            }\n            if (bookShelfBean.getChapterListSize() < chapterList.size()) {\n                bookShelfBean.setHasUpdate(true);\n                bookShelfBean.setFinalRefreshData(System.currentTimeMillis());\n                bookShelfBean.getBookInfoBean().setFinalRefreshData(System.currentTimeMillis());\n            }\n            if (!chapterList.isEmpty()) {\n                bookShelfBean.setChapterListSize(chapterList.size());\n                bookShelfBean.setDurChapter(Math.min(bookShelfBean.getDurChapter(), bookShelfBean.getChapterListSize() - 1));\n                bookShelfBean.setDurChapterName(chapterList.get(bookShelfBean.getDurChapter()).getDurChapterName());\n                bookShelfBean.setLastChapterName(chapterList.get(chapterList.size() - 1).getDurChapterName());\n            }\n            e.onNext(chapterList);\n            e.onComplete();\n        });\n    }\n\n    /**\n     * 保存章节\n     */\n    @SuppressLint(\"DefaultLocale\")\n    private Observable<BookContentBean> saveContent(BookInfoBean infoBean, BaseChapterBean chapterBean, BookContentBean bookContentBean) {\n        return Observable.create(e -> {\n            bookContentBean.setNoteUrl(chapterBean.getNoteUrl());\n            if (isEmpty(bookContentBean.getDurChapterContent())) {\n                e.onError(new Throwable(\"下载章节出错\"));\n            } else if (infoBean.isAudio()) {\n                bookContentBean.setTimeMillis(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1));\n                DbHelper.getDaoSession().getBookContentBeanDao().insertOrReplace(bookContentBean);\n                e.onNext(bookContentBean);\n            } else if (BookshelfHelp.saveChapterInfo(infoBean.getName() + \"-\" + chapterBean.getTag(), chapterBean.getDurChapterIndex(),\n                    chapterBean.getDurChapterName(), bookContentBean.getDurChapterContent())) {\n                RxBus.get().post(RxBusTag.CHAPTER_CHANGE, chapterBean);\n                e.onNext(bookContentBean);\n            } else {\n                e.onError(new Throwable(\"保存章节出错\"));\n            }\n            e.onComplete();\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByJSonPath.java",
    "content": "package com.kunfei.bookshelf.model.analyzeRule;\n\nimport android.text.TextUtils;\n\nimport com.jayway.jsonpath.JsonPath;\nimport com.jayway.jsonpath.ReadContext;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class AnalyzeByJSonPath {\n    private static final Pattern jsonRulePattern = Pattern.compile(\"(?<=\\\\{)\\\\$\\\\..+?(?=\\\\})\");\n    private ReadContext ctx;\n\n    public AnalyzeByJSonPath parse(Object json) {\n        if (json instanceof String) {\n            ctx = JsonPath.parse((String) json);\n        } else {\n            ctx = JsonPath.parse(json);\n        }\n        return this;\n    }\n\n    public String getString(String rule) {\n        if (TextUtils.isEmpty(rule)) return null;\n        String result = \"\";\n        String[] rules;\n        String elementsType;\n\n        if (rule.contains(\"{$.\")) {\n            result = rule;\n            Matcher matcher = jsonRulePattern.matcher(rule);\n            while (matcher.find()) {\n                result = result.replace(String.format(\"{%s}\", matcher.group()), getString(matcher.group().trim()));\n            }\n            return result;\n        }\n\n        if (rule.contains(\"&&\")) {\n            rules = rule.split(\"&&\");\n            elementsType = \"&\";\n        } else {\n            rules = rule.split(\"\\\\|\\\\|\");\n            elementsType = \"|\";\n        }\n\n        if (rules.length == 1) {\n            try {\n                Object object = ctx.read(rule);\n                if (object instanceof List) {\n                    StringBuilder builder = new StringBuilder();\n                    //noinspection rawtypes\n                    for (Object o : (List) object) {\n                        builder.append(o).append(\"\\n\");\n                    }\n                    result = builder.toString().replaceAll(\"\\\\n$\", \"\");\n                } else {\n                    result = String.valueOf(object);\n                }\n            } catch (Exception e) {\n                if (!rule.startsWith(\"$.\")) {\n                    return rule;\n                }\n            }\n            return result;\n        } else {\n            List<String> textS = new ArrayList<>();\n            for (String rl : rules) {\n                String temp = getString(rl);\n                if (!TextUtils.isEmpty(temp)) {\n                    textS.add(temp);\n                    if (elementsType.equals(\"|\")) {\n                        break;\n                    }\n                }\n            }\n            return TextUtils.join(\",\", textS).trim();\n        }\n    }\n\n    List<String> getStringList(String rule) {\n        List<String> result = new ArrayList<>();\n        if (TextUtils.isEmpty(rule)) return result;\n        String[] rules;\n        String elementsType;\n        if (rule.contains(\"&&\")) {\n            rules = rule.split(\"&&\");\n            elementsType = \"&\";\n        } else if (rule.contains(\"%%\")) {\n            rules = rule.split(\"%%\");\n            elementsType = \"%\";\n        } else {\n            rules = rule.split(\"\\\\|\\\\|\");\n            elementsType = \"|\";\n        }\n        if (rules.length == 1) {\n            if (!rule.contains(\"{$.\")) {\n                try {\n                    Object object = ctx.read(rule);\n                    if (object == null) return result;\n                    if (object instanceof List) {\n                        //noinspection rawtypes\n                        for (Object o : ((List) object))\n                            result.add(String.valueOf(o));\n                    } else {\n                        result.add(String.valueOf(object));\n                    }\n                } catch (Exception ignored) {\n                }\n            } else {\n                Matcher matcher = jsonRulePattern.matcher(rule);\n                while (matcher.find()) {\n                    List<String> stringList = getStringList(matcher.group());\n                    for (String s : stringList) {\n                        result.add(rule.replace(String.format(\"{%s}\", matcher.group()), s));\n                    }\n                }\n            }\n        } else {\n            List<List<String>> results = new ArrayList<>();\n            for (String rl : rules) {\n                List<String> temp = getStringList(rl);\n                if (temp != null && !temp.isEmpty()) {\n                    results.add(temp);\n                    if (temp.size() > 0 && elementsType.equals(\"|\")) {\n                        break;\n                    }\n                }\n            }\n            if (results.size() > 0) {\n                if (\"%\".equals(elementsType)) {\n                    for (int i = 0; i < results.get(0).size(); i++) {\n                        for (List<String> temp : results) {\n                            if (i < temp.size()) {\n                                result.add(temp.get(i));\n                            }\n                        }\n                    }\n                } else {\n                    for (List<String> temp : results) {\n                        result.addAll(temp);\n                    }\n                }\n            }\n        }\n        return result;\n    }\n\n    Object getObject(String rule) {\n        return ctx.read(rule);\n    }\n\n    List<Object> getList(String rule) {\n        List<Object> result = new ArrayList<>();\n        if (TextUtils.isEmpty(rule)) return result;\n        String elementsType;\n        String[] rules;\n        if (rule.contains(\"&&\")) {\n            rules = rule.split(\"&&\");\n            elementsType = \"&\";\n        } else if (rule.contains(\"%%\")) {\n            rules = rule.split(\"%%\");\n            elementsType = \"%\";\n        } else {\n            rules = rule.split(\"\\\\|\\\\|\");\n            elementsType = \"|\";\n        }\n        if (rules.length == 1) {\n            try {\n                return ctx.read(rules[0]);\n            } catch (Exception e) {\n                return null;\n            }\n        } else {\n            List<List<Object>> results = new ArrayList<>();\n            for (String rl : rules) {\n                List<Object> temp = getList(rl);\n                if (temp != null && !temp.isEmpty()) {\n                    results.add(temp);\n                    if (temp.size() > 0 && elementsType.equals(\"|\")) {\n                        break;\n                    }\n                }\n            }\n            if (results.size() > 0) {\n                if (\"%\".equals(elementsType)) {\n                    for (int i = 0; i < results.get(0).size(); i++) {\n                        for (List<Object> temp : results) {\n                            if (i < temp.size()) {\n                                result.add(temp.get(i));\n                            }\n                        }\n                    }\n                } else {\n                    for (List<Object> temp : results) {\n                        result.addAll(temp);\n                    }\n                }\n            }\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByJSoup.java",
    "content": "package com.kunfei.bookshelf.model.analyzeRule;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.nodes.TextNode;\nimport org.jsoup.select.Collector;\nimport org.jsoup.select.Elements;\nimport org.jsoup.select.Evaluator;\nimport org.seimicrawler.xpath.JXNode;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static android.text.TextUtils.isEmpty;\n\n/**\n * Created by GKF on 2018/1/25.\n * 书源规则解析\n */\n\npublic class AnalyzeByJSoup {\n    private Element element;\n\n    public AnalyzeByJSoup parse(Object doc) {\n        if (doc instanceof Element) {\n            element = (Element) doc;\n        } else if (doc instanceof JXNode) {\n            JXNode jxNode = (JXNode) doc;\n            if (jxNode.isElement()) {\n                element = jxNode.asElement();\n            } else {\n                element = Jsoup.parse(jxNode.value().toString());\n            }\n        } else {\n            element = Jsoup.parse(doc.toString());\n        }\n        return this;\n    }\n\n    /**\n     * 获取列表\n     */\n    Elements getElements(String rule) {\n        return getElements(element, rule);\n    }\n\n    /**\n     * 合并内容列表,得到内容\n     */\n    String getString(String ruleStr) {\n        if (isEmpty(ruleStr)) {\n            return null;\n        }\n        List<String> textS = getStringList(ruleStr);\n        if (textS.size() == 0) {\n            return null;\n        }\n        return TextUtils.join(\",\", textS).trim();\n    }\n\n    /**\n     * 获取一个字符串\n     **/\n    String getString0(String ruleStr) {\n        List<String> urlList = getStringList(ruleStr);\n        if (!urlList.isEmpty()) {\n            return urlList.get(0);\n        }\n        return \"\";\n    }\n\n    /**\n     * 获取所有内容列表\n     */\n    List<String> getStringList(String ruleStr) {\n        List<String> textS = new ArrayList<>();\n        if (isEmpty(ruleStr)) {\n            return textS;\n        }\n        //拆分规则\n        SourceRule sourceRule = new SourceRule(ruleStr);\n        if (isEmpty(sourceRule.elementsRule)) {\n            textS.add(element.data());\n        } else {\n            String elementsType;\n            String[] ruleStrS;\n            if (sourceRule.elementsRule.contains(\"&\")) {\n                elementsType = \"&\";\n                ruleStrS = sourceRule.elementsRule.split(\"&+\");\n            } else if (sourceRule.elementsRule.contains(\"%%\")) {\n                elementsType = \"%\";\n                ruleStrS = sourceRule.elementsRule.split(\"%%\");\n            } else {\n                elementsType = \"|\";\n                if (sourceRule.isCss) {\n                    ruleStrS = sourceRule.elementsRule.split(\"\\\\|\\\\|\");\n                } else {\n                    ruleStrS = sourceRule.elementsRule.split(\"\\\\|+\");\n                }\n            }\n            List<List<String>> results = new ArrayList<>();\n            for (String ruleStrX : ruleStrS) {\n                List<String> temp;\n                if (sourceRule.isCss) {\n                    int lastIndex = ruleStrX.lastIndexOf('@');\n                    temp = getResultLast(element.select(ruleStrX.substring(0, lastIndex)), ruleStrX.substring(lastIndex + 1));\n                } else {\n                    temp = getResultList(ruleStrX);\n                }\n                if (temp != null && !temp.isEmpty()) {\n                    results.add(temp);\n                    if (!results.isEmpty() && elementsType.equals(\"|\")) {\n                        break;\n                    }\n                }\n            }\n            if (results.size() > 0) {\n                if (\"%\".equals(elementsType)) {\n                    for (int i = 0; i < results.get(0).size(); i++) {\n                        for (List<String> temp : results) {\n                            if (i < temp.size()) {\n                                textS.add(temp.get(i));\n                            }\n                        }\n                    }\n                } else {\n                    for (List<String> temp : results) {\n                        textS.addAll(temp);\n                    }\n                }\n            }\n        }\n        if (!isEmpty(sourceRule.replaceRegex)) {\n            List<String> tempList = new ArrayList<>(textS);\n            textS.clear();\n            for (String text : tempList) {\n                text = text.replaceAll(sourceRule.replaceRegex, sourceRule.replacement);\n                if (text.length() > 0) {\n                    textS.add(text);\n                }\n            }\n        }\n        return textS;\n    }\n\n    /**\n     * 获取Elements\n     */\n    private Elements getElements(Element temp, String rule) {\n        Elements elements = new Elements();\n        if (temp == null || isEmpty(rule)) {\n            return elements;\n        }\n        SourceRule sourceRule = new SourceRule(rule);\n        String elementsType;\n        String[] ruleStrS;\n        if (sourceRule.elementsRule.contains(\"&\")) {\n            elementsType = \"&\";\n            ruleStrS = sourceRule.elementsRule.split(\"&+\");\n        } else if (sourceRule.elementsRule.contains(\"%\")) {\n            elementsType = \"%\";\n            ruleStrS = sourceRule.elementsRule.split(\"%+\");\n        } else {\n            elementsType = \"|\";\n            if (sourceRule.isCss) {\n                ruleStrS = sourceRule.elementsRule.split(\"\\\\|\\\\|\");\n            } else {\n                ruleStrS = sourceRule.elementsRule.split(\"\\\\|+\");\n            }\n        }\n        List<Elements> elementsList = new ArrayList<>();\n        if (sourceRule.isCss) {\n            for (String ruleStr : ruleStrS) {\n                Elements tempS = temp.select(ruleStr);\n                elementsList.add(tempS);\n                if (tempS.size() > 0 && elementsType.equals(\"|\")) {\n                    break;\n                }\n            }\n        } else {\n            for (String ruleStr : ruleStrS) {\n                Elements tempS = getElementsSingle(temp, ruleStr);\n                elementsList.add(tempS);\n                if (tempS.size() > 0 && elementsType.equals(\"|\")) {\n                    break;\n                }\n            }\n        }\n        if (elementsList.size() > 0) {\n            if (\"%\".equals(elementsType)) {\n                for (int i = 0; i < elementsList.get(0).size(); i++) {\n                    for (Elements es : elementsList) {\n                        if (i < es.size()) {\n                            elements.add(es.get(i));\n                        }\n                    }\n                }\n            } else {\n                for (Elements es : elementsList) {\n                    elements.addAll(es);\n                }\n            }\n        }\n        return elements;\n    }\n\n    private Elements filterElements(Elements elements, String[] rules) {\n        if (rules == null || rules.length < 2) return elements;\n        Elements selectedEls = new Elements();\n        for (Element ele : elements) {\n            boolean isOk = false;\n            switch (rules[0]) {\n                case \"class\":\n                    isOk = ele.getElementsByClass(rules[1]).size() > 0;\n                    break;\n                case \"id\":\n                    isOk = ele.getElementById(rules[1]) != null;\n                    break;\n                case \"tag\":\n                    isOk = ele.getElementsByTag(rules[1]).size() > 0;\n                    break;\n                case \"text\":\n                    isOk = ele.getElementsContainingOwnText(rules[1]).size() > 0;\n                    break;\n            }\n            if (isOk) {\n                selectedEls.add(ele);\n            }\n        }\n        return selectedEls;\n    }\n\n    /**\n     * 获取Elements按照一个规则\n     */\n    private Elements getElementsSingle(Element temp, String rule) {\n        Elements elements = new Elements();\n        try {\n            String[] rs = rule.trim().split(\"@\");\n            if (rs.length > 1) {\n                elements.add(temp);\n                for (String rl : rs) {\n                    Elements es = new Elements();\n                    for (Element et : elements) {\n                        es.addAll(getElements(et, rl));\n                    }\n                    elements.clear();\n                    elements.addAll(es);\n                }\n            } else {\n                String[] rulePcx = rule.split(\"!\");\n                String[] rulePc = rulePcx[0].trim().split(\">\");\n                String[] rules = rulePc[0].trim().split(\"\\\\.\");\n                String[] filterRules = null;\n                boolean needFilterElements = rulePc.length > 1 && !isEmpty(rulePc[1].trim());\n                if (needFilterElements) {\n                    filterRules = rulePc[1].trim().split(\"\\\\.\");\n                    filterRules[0] = filterRules[0].trim();\n                    List<String> validKeys = Arrays.asList(\"class\", \"id\", \"tag\", \"text\");\n                    if (filterRules.length < 2 || !validKeys.contains(filterRules[0]) || isEmpty(filterRules[1].trim())) {\n                        needFilterElements = false;\n                    }\n                    filterRules[1] = filterRules[1].trim();\n                }\n                switch (rules[0]) {\n                    case \"children\":\n                        Elements children = temp.children();\n                        if (needFilterElements)\n                            children = filterElements(children, filterRules);\n                        elements.addAll(children);\n                        break;\n                    case \"class\":\n                        Elements elementsByClass = temp.getElementsByClass(rules[1]);\n                        if (rules.length == 3) {\n                            int index = Integer.parseInt(rules[2]);\n                            if (index < 0) {\n                                elements.add(elementsByClass.get(elementsByClass.size() + index));\n                            } else {\n                                elements.add(elementsByClass.get(index));\n                            }\n                        } else {\n                            if (needFilterElements)\n                                elementsByClass = filterElements(elementsByClass, filterRules);\n                            elements.addAll(elementsByClass);\n                        }\n                        break;\n                    case \"tag\":\n                        Elements elementsByTag = temp.getElementsByTag(rules[1]);\n                        if (rules.length == 3) {\n                            int index = Integer.parseInt(rules[2]);\n                            if (index < 0) {\n                                elements.add(elementsByTag.get(elementsByTag.size() + index));\n                            } else {\n                                elements.add(elementsByTag.get(index));\n                            }\n                        } else {\n                            if (needFilterElements)\n                                elementsByTag = filterElements(elementsByTag, filterRules);\n                            elements.addAll(elementsByTag);\n                        }\n                        break;\n                    case \"id\":\n                        Elements elementsById = Collector.collect(new Evaluator.Id(rules[1]), temp);\n                        if (rules.length == 3) {\n                            int index = Integer.parseInt(rules[2]);\n                            if (index < 0) {\n                                elements.add(elementsById.get(elementsById.size() + index));\n                            } else {\n                                elements.add(elementsById.get(index));\n                            }\n                        } else {\n                            if (needFilterElements)\n                                elementsById = filterElements(elementsById, filterRules);\n                            elements.addAll(elementsById);\n                        }\n                        break;\n                    case \"text\":\n                        Elements elementsByText = temp.getElementsContainingOwnText(rules[1]);\n                        if (needFilterElements)\n                            elementsByText = filterElements(elementsByText, filterRules);\n                        elements.addAll(elementsByText);\n                        break;\n                    default:\n                        elements.addAll(temp.select(rulePcx[0]));\n                }\n                if (rulePcx.length > 1) {\n                    String[] rulePcs = rulePcx[1].split(\":\");\n                    for (String pc : rulePcs) {\n                        int pcInt = Integer.parseInt(pc);\n                        if (pcInt < 0 && elements.size() + pcInt >= 0) {\n                            elements.set(elements.size() + pcInt, null);\n                        } else if (Integer.parseInt(pc) < elements.size()) {\n                            elements.set(Integer.parseInt(pc), null);\n                        }\n                    }\n                    Elements es = new Elements();\n                    es.add(null);\n                    elements.removeAll(es);\n                }\n            }\n        } catch (Exception ignore) {\n        }\n        return elements;\n    }\n\n    /**\n     * 获取内容列表\n     */\n    private List<String> getResultList(String ruleStr) {\n        if (isEmpty(ruleStr)) {\n            return null;\n        }\n        Elements elements = new Elements();\n        elements.add(element);\n        String[] rules = ruleStr.split(\"@\");\n        for (int i = 0; i < rules.length - 1; i++) {\n            Elements es = new Elements();\n            for (Element elt : elements) {\n                es.addAll(getElementsSingle(elt, rules[i]));\n            }\n            elements.clear();\n            elements = es;\n        }\n        if (elements.isEmpty()) {\n            return null;\n        }\n        return getResultLast(elements, rules[rules.length - 1]);\n    }\n\n    /**\n     * 根据最后一个规则获取内容\n     */\n    private List<String> getResultLast(Elements elements, String lastRule) {\n        List<String> textS = new ArrayList<>();\n        List<String> cText = new ArrayList<>();\n        try {\n            switch (lastRule) {\n                case \"text\":\n                    for (Element element : elements) {\n                        String text = element.text();\n                        cText.add(text);\n                    }\n                    textS.add(TextUtils.join(\"\\n\", cText));\n                    break;\n                case \"textNodes\":\n                    for (Element element : elements) {\n                        List<TextNode> contentEs = element.textNodes();\n                        for (int i = 0; i < contentEs.size(); i++) {\n                            String temp = contentEs.get(i).text().trim();\n                            if (!isEmpty(temp)) {\n                                cText.add(temp);\n                            }\n                        }\n                    }\n                    textS.add(TextUtils.join(\"\\n\", cText));\n                    break;\n                case \"ownText\":\n                    for (Element element : elements) {\n                        cText.add(element.ownText());\n                    }\n                    textS.add(TextUtils.join(\"\\n\", cText));\n                    break;\n                case \"html\":\n                    elements.select(\"script, style\").remove();\n                    String html = elements.html();\n                    textS.add(html);\n                    break;\n                case \"all\":\n                    textS.add(elements.outerHtml());\n                    break;\n                default:\n                    for (Element element : elements) {\n                        String url = element.attr(lastRule);\n                        if (!TextUtils.isEmpty(url) && !textS.contains(url)) {\n                            textS.add(url);\n                        }\n                    }\n            }\n        } catch (Exception ignore) {\n        }\n        return textS;\n    }\n\n    class SourceRule {\n        boolean isCss = false;\n        String elementsRule;\n        String replaceRegex = \"\";\n        String replacement = \"\";\n\n        SourceRule(String ruleStr) {\n            if (StringUtils.startWithIgnoreCase(ruleStr, \"@CSS:\")) {\n                isCss = true;\n                elementsRule = ruleStr.substring(5).trim();\n                return;\n            }\n            String[] ruleStrS;\n            //分离正则表达式\n            ruleStrS = ruleStr.trim().split(\"#\");\n            elementsRule = ruleStrS[0];\n            if (ruleStrS.length > 1) {\n                replaceRegex = ruleStrS[1];\n            }\n            if (ruleStrS.length > 2) {\n                replacement = ruleStrS[2];\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByRegex.java",
    "content": "package com.kunfei.bookshelf.model.analyzeRule;\n\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.model.content.Debug;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport static android.text.TextUtils.isEmpty;\n\npublic class AnalyzeByRegex {\n\n    // 纯java模式正则表达式获取书籍详情信息\n    public static void getInfoOfRegex(String res, String[] regs, int index,\n                                      BookShelfBean bookShelfBean, AnalyzeRule analyzer, BookSourceBean bookSourceBean, String tag) throws Exception {\n        Matcher resM = Pattern.compile(regs[index]).matcher(res);\n        String baseUrl = bookShelfBean.getNoteUrl();\n        // 创建详情信息存储容器\n        BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean();\n        // 判断规则是否有效,当搜索列表规则无效时跳过详情页处理\n        if (!resM.find()) {\n            Debug.printLog(tag, \"└详情预处理失败,跳过详情页解析\");\n            Debug.printLog(tag, \"┌获取目录网址\");\n            bookInfoBean.setChapterUrl(baseUrl);\n            bookInfoBean.setChapterListHtml(res);\n            Debug.printLog(tag, \"└\" + baseUrl);\n            return;\n        }\n        // 判断索引的规则是最后一个规则\n        if (index + 1 == regs.length) {\n            // 获取规则列表\n            HashMap<String, String> ruleMap = new HashMap<>();\n            ruleMap.put(\"BookName\", bookSourceBean.getRuleBookName());\n            ruleMap.put(\"BookAuthor\", bookSourceBean.getRuleBookAuthor());\n            ruleMap.put(\"BookKind\", bookSourceBean.getRuleBookKind());\n            ruleMap.put(\"LastChapter\", bookSourceBean.getRuleBookLastChapter());\n            ruleMap.put(\"Introduce\", bookSourceBean.getRuleIntroduce());\n            ruleMap.put(\"CoverUrl\", bookSourceBean.getRuleCoverUrl());\n            ruleMap.put(\"ChapterUrl\", bookSourceBean.getRuleChapterUrl());\n            // 分离规则参数\n            List<String> ruleName = new ArrayList<>();\n            List<List<String>> ruleParams = new ArrayList<>();  // 创建规则参数容器\n            List<List<Integer>> ruleTypes = new ArrayList<>();  // 创建规则类型容器\n            List<Boolean> hasVarParams = new ArrayList<>();     // 创建put&get标志容器\n            for (String key : ruleMap.keySet()) {\n                String val = ruleMap.get(key);\n                ruleName.add(key);\n                hasVarParams.add(!TextUtils.isEmpty(val) && (val.contains(\"@put\") || val.contains(\"@get\")));\n                List<String> ruleParam = new ArrayList<>();\n                List<Integer> ruleType = new ArrayList<>();\n                AnalyzeByRegex.splitRegexRule(val, ruleParam, ruleType);\n                ruleParams.add(ruleParam);\n                ruleTypes.add(ruleType);\n            }\n            // 提取规则内容\n            HashMap<String, String> ruleVal = new HashMap<>();\n            StringBuilder infoVal = new StringBuilder();\n            for (int i = ruleParams.size(); i-- > 0; ) {\n                List<String> ruleParam = ruleParams.get(i);\n                List<Integer> ruleType = ruleTypes.get(i);\n                infoVal.setLength(0);\n                for (int j = ruleParam.size(); j-- > 0; ) {\n                    int regType = ruleType.get(j);\n                    if (regType > 0) {\n                        infoVal.insert(0, resM.group(regType));\n                    } else if (regType < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                        infoVal.insert(0, resM.group(ruleParam.get(j)));\n                    } else {\n                        infoVal.insert(0, ruleParam.get(j));\n                    }\n                }\n                ruleVal.put(ruleName.get(i), hasVarParams.get(i) ? AnalyzeByRegex.checkKeys(infoVal.toString(), analyzer) : infoVal.toString());\n            }\n            // 保存详情信息\n            if (!isEmpty(ruleVal.get(\"BookName\")))\n                bookInfoBean.setName(StringUtils.formatHtml(ruleVal.get(\"BookName\")));\n            if (!isEmpty(ruleVal.get(\"BookAuthor\")))\n                bookInfoBean.setAuthor(StringUtils.formatHtml(ruleVal.get(\"BookAuthor\")));\n            if (!isEmpty(ruleVal.get(\"LastChapter\"))) bookShelfBean.setLastChapterName(ruleVal.get(\"LastChapter\"));\n            if (!isEmpty(ruleVal.get(\"Introduce\")))\n                bookInfoBean.setIntroduce(StringUtils.formatHtml(ruleVal.get(\"Introduce\")));\n            if (!isEmpty(ruleVal.get(\"CoverUrl\"))) bookInfoBean.setCoverUrl(ruleVal.get(\"CoverUrl\"));\n            if (!isEmpty(ruleVal.get(\"ChapterUrl\"))) bookInfoBean.setChapterUrl(NetworkUtils.getAbsoluteURL(baseUrl, ruleVal.get(\"ChapterUrl\")));\n            else bookInfoBean.setChapterUrl(baseUrl);\n            //如果目录页和详情页相同,暂存页面内容供获取目录用\n            if (bookInfoBean.getChapterUrl().equals(baseUrl)) bookInfoBean.setChapterListHtml(res);\n            // 输出调试信息\n            Debug.printLog(tag, \"└详情预处理完成\");\n            Debug.printLog(tag, \"┌获取书籍名称\");\n            Debug.printLog(tag, \"└\" + bookInfoBean.getName());\n            Debug.printLog(tag, \"┌获取作者名称\");\n            Debug.printLog(tag, \"└\" + bookInfoBean.getAuthor());\n            Debug.printLog(tag, \"┌获取最新章节\");\n            Debug.printLog(tag, \"└\" + bookShelfBean.getLastChapterName());\n            Debug.printLog(tag, \"┌获取简介内容\");\n            Debug.printLog(tag, 1, \"└\" + bookInfoBean.getIntroduce(), true, true);\n            Debug.printLog(tag, \"┌获取封面网址\");\n            Debug.printLog(tag, \"└\" + bookInfoBean.getCoverUrl());\n            Debug.printLog(tag, \"┌获取目录网址\");\n            Debug.printLog(tag, \"└\" + bookInfoBean.getChapterUrl());\n            Debug.printLog(tag, \"-详情页解析完成\");\n        } else {\n            StringBuilder result = new StringBuilder();\n            do {\n                result.append(resM.group());\n            } while (resM.find());\n            getInfoOfRegex(result.toString(), regs, ++index, bookShelfBean, analyzer, bookSourceBean, tag);\n        }\n    }\n\n    // 正则表达式解析规则数据的通用方法(暂未使用,技术储备型代码)\n    public static void getInfoByRegex(String res, String[] regList, int regIndex,\n                                      HashMap<String, String> ruleMap, final List<HashMap<String, String>> ruleVals) throws Exception {\n        Matcher resM = Pattern.compile(regList[regIndex]).matcher(res);\n        // 判断规则是否有效\n        if (!resM.find()) {\n            return;\n        }\n        // 判断索引规则是否为最后一个\n        if (regIndex + 1 == regList.length) {\n            // 分离规则参数\n            List<String> ruleName = new ArrayList<>();\n            List<List<String>> ruleParams = new ArrayList<>(); // 创建规则参数容器\n            List<List<Integer>> ruleTypes = new ArrayList<>(); // 创建规则类型容器\n            List<Boolean> hasVarParams = new ArrayList<>(); // 创建put&get标志容器\n            for (String key : ruleMap.keySet()) {\n                String val = ruleMap.get(key);\n                ruleName.add(key);\n                hasVarParams.add(val.contains(\"@put\") || val.contains(\"@get\"));\n                List<String> ruleParam = new ArrayList<>();\n                List<Integer> ruleType = new ArrayList<>();\n                splitRegexRule(val, ruleParam, ruleType);\n                ruleParams.add(ruleParam);\n                ruleTypes.add(ruleType);\n            }\n            // 提取规则结果\n            do {\n                HashMap<String, String> ruleVal = new HashMap<>();\n                StringBuilder infoVal = new StringBuilder();\n                for (int i = ruleParams.size(); i-- > 0; ) {\n                    List<String> ruleParam = ruleParams.get(i);\n                    List<Integer> ruleType = ruleTypes.get(i);\n                    infoVal.setLength(0);\n                    for (int j = ruleParam.size(); j-- > 0; ) {\n                        int regType = ruleType.get(j);\n                        if (regType > 0) {\n                            if (j == 0 && Objects.equals(ruleName.get(0), \"ruleChapterName\")) {\n                                infoVal.insert(0, resM.group(regType) == null ? \"\" : \"\\uD83D\\uDD12\");\n                            } else {\n                                infoVal.insert(0, resM.group(regType));\n                            }\n                        } else if (regType < 0) {\n                            if (j == 0 && Objects.equals(ruleName.get(0), \"ruleChapterName\")) {\n                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                                    infoVal.insert(0, resM.group(ruleParam.get(j)) == null ? \"\" : \"\\uD83D\\uDD12\");\n                                }\n                            } else {\n                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                                    infoVal.insert(0, resM.group(ruleParam.get(j)));\n                                }\n                            }\n                        } else {\n                            infoVal.insert(0, ruleParam.get(j));\n                        }\n                    }\n                    ruleVal.put(ruleName.get(i), infoVal.toString());\n                }\n                ruleVals.add(ruleVal);\n            } while (resM.find());\n        } else {\n            StringBuilder result = new StringBuilder();\n            do {\n                result.append(resM.group(0));\n            } while (resM.find());\n            getInfoByRegex(result.toString(), regList, ++regIndex, ruleMap, ruleVals);\n        }\n    }\n\n    // 拆分正则表达式替换规则(如:$\\d{1,2}或${name}) /*注意:千万别用正则表达式拆分字符串,效率太低了!*/\n    public static void splitRegexRule(String str, final List<String> ruleParam, final List<Integer> ruleType) throws Exception {\n        if (TextUtils.isEmpty(str)) {\n            ruleParam.add(\"\");\n            ruleType.add(0);\n            return;\n        }\n        int index = 0, start = 0, len = str.length();\n        while (index < len) {\n            if (str.charAt(index) == '$') {\n                if (str.charAt(index + 1) == '{') {\n                    if (index > start) {\n                        ruleParam.add(str.substring(start, index));\n                        ruleType.add(0);\n                        start = index;\n                    }\n                    for (index += 2; index < len; index++) {\n                        if (str.charAt(index) == '}') {\n                            ruleParam.add(str.substring(start + 2, index));\n                            ruleType.add(-1);\n                            start = ++index;\n                            break;\n                        } else if (str.charAt(index) == '$' || str.charAt(index) == '@') {\n                            break;\n                        }\n                    }\n                } else if ((str.charAt(index + 1) >= '0') && (str.charAt(index + 1) <= '9')) {\n                    if (index > start) {\n                        ruleParam.add(str.substring(start, index));\n                        ruleType.add(0);\n                        start = index;\n                    }\n                    if ((index + 2 < len) && (str.charAt(index + 2) >= '0') && (str.charAt(index + 2) <= '9')) {\n                        ruleParam.add(str.substring(start, index + 3));\n                        ruleType.add(string2Int(ruleParam.get(ruleParam.size() - 1)));\n                        start = index += 3;\n                    } else {\n                        ruleParam.add(str.substring(start, index + 2));\n                        ruleType.add(string2Int(ruleParam.get(ruleParam.size() - 1)));\n                        start = index += 2;\n                    }\n                } else {\n                    ++index;\n                }\n            } else {\n                ++index;\n            }\n        }\n        if (index > start) {\n            ruleParam.add(str.substring(start, index));\n            ruleType.add(0);\n        }\n    }\n\n    // 存取字符串中的put&get参数\n    public static String checkKeys(String str, AnalyzeRule analyzer) throws Exception {\n        if (str.contains(\"@put:{\")) {\n            Matcher putMatcher = Pattern.compile(\"@put:\\\\{([^,]*):([^\\\\}]*)\\\\}\").matcher(str);\n            while (putMatcher.find()) {\n                str = str.replace(putMatcher.group(0), \"\");\n                analyzer.put(putMatcher.group(1), putMatcher.group(2));\n            }\n        }\n        if (str.contains(\"@get:{\")) {\n            Matcher getMatcher = Pattern.compile(\"@get:\\\\{([^\\\\}]*)\\\\}\").matcher(str);\n            while (getMatcher.find()) {\n                str = str.replace(getMatcher.group(), analyzer.get(getMatcher.group(1)));\n            }\n        }\n        return str;\n    }\n\n    // String数字转int数字的高效方法(利用ASCII值判断)\n    public static int string2Int(String s) {\n        int r = 0;\n        char n;\n        for (int i = 0, l = s.length(); i < l; i++) {\n            n = s.charAt(i);\n            if (n >= '0' && n <= '9') {\n                r = r * 10 + (n - 0x30); //'0-9'的ASCII值为0x30-0x39\n            }\n        }\n        return r;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeByXPath.java",
    "content": "package com.kunfei.bookshelf.model.analyzeRule;\n\nimport android.text.TextUtils;\n\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.select.Elements;\nimport org.seimicrawler.xpath.JXDocument;\nimport org.seimicrawler.xpath.JXNode;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class AnalyzeByXPath {\n    private JXDocument jxDocument;\n    private JXNode jxNode;\n\n    public AnalyzeByXPath parse(Object doc) {\n        if (doc instanceof JXNode) {\n            jxNode = (JXNode) doc;\n            if (!jxNode.isElement()) {\n                jxDocument = strToJXDocument(doc.toString());\n                jxNode = null;\n            }\n        } else if (doc instanceof Document) {\n            jxDocument = JXDocument.create((Document) doc);\n            jxNode = null;\n        } else if (doc instanceof Element) {\n            jxDocument = JXDocument.create(new Elements((Element) doc));\n            jxNode = null;\n        } else if (doc instanceof Elements) {\n            jxDocument = JXDocument.create((Elements) doc);\n            jxNode = null;\n        } else {\n            jxDocument = strToJXDocument(doc.toString());\n            jxNode = null;\n        }\n        return this;\n    }\n\n    private JXDocument strToJXDocument(String html) {\n        if (html.endsWith(\"</td>\")) {\n            html = String.format(\"<tr>%s</tr>\", html);\n        }\n        if (html.endsWith(\"</tr>\") || html.endsWith(\"</tbody>\")) {\n            html = String.format(\"<table>%s</table>\", html);\n        }\n        return JXDocument.create(html);\n    }\n\n    List<JXNode> getElements(String xPath) {\n        if (TextUtils.isEmpty(xPath)) {\n            return null;\n        }\n        List<JXNode> jxNodes = new ArrayList<>();\n        String elementsType;\n        String[] rules;\n        if (xPath.contains(\"&&\")) {\n            rules = xPath.split(\"&&\");\n            elementsType = \"&\";\n        } else if (xPath.contains(\"%%\")) {\n            rules = xPath.split(\"%%\");\n            elementsType = \"%\";\n        } else {\n            rules = xPath.split(\"\\\\|\\\\|\");\n            elementsType = \"|\";\n        }\n        if (rules.length == 1) {\n            if (jxNode != null) {\n                return jxNode.sel(rules[0]);\n            }\n            return jxDocument.selN(rules[0]);\n        } else {\n            List<List<JXNode>> results = new ArrayList<>();\n            for (String rl : rules) {\n                List<JXNode> temp = getElements(rl);\n                if (temp != null && !temp.isEmpty()) {\n                    results.add(temp);\n                    if (temp.size() > 0 && elementsType.equals(\"|\")) {\n                        break;\n                    }\n                }\n            }\n            if (results.size() > 0) {\n                if (\"%\".equals(elementsType)) {\n                    for (int i = 0; i < results.get(0).size(); i++) {\n                        for (List<JXNode> temp : results) {\n                            if (i < temp.size()) {\n                                jxNodes.add(temp.get(i));\n                            }\n                        }\n                    }\n                } else {\n                    for (List<JXNode> temp : results) {\n                        jxNodes.addAll(temp);\n                    }\n                }\n            }\n        }\n        return jxNodes;\n    }\n\n    List<String> getStringList(String xPath) {\n        List<String> result = new ArrayList<>();\n        String elementsType;\n        String[] rules;\n        if (xPath.contains(\"&&\")) {\n            rules = xPath.split(\"&&\");\n            elementsType = \"&\";\n        } else if (xPath.contains(\"%%\")) {\n            rules = xPath.split(\"%%\");\n            elementsType = \"%\";\n        } else {\n            rules = xPath.split(\"\\\\|\\\\|\");\n            elementsType = \"|\";\n        }\n        if (rules.length == 1) {\n            List<JXNode> jxNodes;\n            if (jxNode != null) {\n                jxNodes = jxNode.sel(xPath);\n            } else {\n                jxNodes = jxDocument.selN(xPath);\n            }\n            for (JXNode jxNode : jxNodes) {\n                /*if(jxNode.isString()){\n                    result.add(String.valueOf(jxNode));\n                }*/\n                result.add(String.valueOf(jxNode));\n            }\n            return result;\n        } else {\n            List<List<String>> results = new ArrayList<>();\n            for (String rl : rules) {\n                List<String> temp = getStringList(rl);\n                if (temp != null && !temp.isEmpty()) {\n                    results.add(temp);\n                    if (temp.size() > 0 && elementsType.equals(\"|\")) {\n                        break;\n                    }\n                }\n            }\n            if (results.size() > 0) {\n                if (\"%\".equals(elementsType)) {\n                    for (int i = 0; i < results.get(0).size(); i++) {\n                        for (List<String> temp : results) {\n                            if (i < temp.size()) {\n                                result.add(temp.get(i));\n                            }\n                        }\n                    }\n                } else {\n                    for (List<String> temp : results) {\n                        result.addAll(temp);\n                    }\n                }\n            }\n        }\n        return result;\n    }\n\n    public String getString(String rule) {\n        String[] rules;\n        String elementsType;\n        if (rule.contains(\"&&\")) {\n            rules = rule.split(\"&&\");\n            elementsType = \"&\";\n        } else {\n            rules = rule.split(\"\\\\|\\\\|\");\n            elementsType = \"|\";\n        }\n        if (rules.length == 1) {\n            List<JXNode> jxNodes;\n            if (jxNode != null) {\n                jxNodes = jxNode.sel(rule);\n            } else {\n                jxNodes = jxDocument.selN(rule);\n            }\n            if (jxNodes == null) return null;\n            return TextUtils.join(\",\", jxNodes);\n        } else {\n            List<String> textS = new ArrayList<>();\n            for (String rl : rules) {\n                String temp = getString(rl);\n                if (!TextUtils.isEmpty(temp)) {\n                    textS.add(temp);\n                    if (elementsType.equals(\"|\")) {\n                        break;\n                    }\n                }\n            }\n            return TextUtils.join(\",\", textS).trim();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeHeaders.java",
    "content": "package com.kunfei.bookshelf.model.analyzeRule;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.DEFAULT_USER_AGENT;\n\nimport android.content.SharedPreferences;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Created by GKF on 2018/3/2.\n * 解析Headers\n */\n\npublic class AnalyzeHeaders {\n    private static SharedPreferences preferences = MApplication.getConfigPreferences();\n\n    public static Map<String, String> getDefaultHeader() {\n        Map<String, String> headerMap = new HashMap<>();\n        headerMap.put(\"User-Agent\", getDefaultUserAgent());\n        return headerMap;\n    }\n\n    public static String getDefaultUserAgent() {\n        return preferences.getString(MApplication.getInstance().getString(R.string.pk_user_agent), DEFAULT_USER_AGENT);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeRule.java",
    "content": "package com.kunfei.bookshelf.model.analyzeRule;\n\nimport static android.text.TextUtils.isEmpty;\nimport static com.kunfei.bookshelf.constant.AppConstant.EXP_PATTERN;\nimport static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN;\nimport static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING;\nimport static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE;\nimport static com.kunfei.bookshelf.utils.NetworkUtils.headerPattern;\n\nimport android.annotation.SuppressLint;\n\nimport androidx.annotation.Keep;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.bean.BaseBookBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.help.JsExtensions;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport org.jsoup.nodes.Entities;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport javax.script.SimpleBindings;\n\n\n/**\n * Created by REFGD.\n * 统一解析接口\n */\n@Keep\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class AnalyzeRule implements JsExtensions {\n    private static final Pattern putPattern = Pattern.compile(\"@put:(\\\\{[^}]+?\\\\})\", Pattern.CASE_INSENSITIVE);\n    private static final Pattern getPattern = Pattern.compile(\"@get:\\\\{([^}]+?)\\\\}\", Pattern.CASE_INSENSITIVE);\n\n    private final BookSourceBean bookSource;\n    private BaseBookBean book;\n    private Object object;\n    private Boolean isJSON = false;\n    private String baseUrl = null;\n\n    private AnalyzeByXPath analyzeByXPath = null;\n    private AnalyzeByJSoup analyzeByJSoup = null;\n    private AnalyzeByJSonPath analyzeByJSonPath = null;\n\n    private boolean objectChangedXP = false;\n    private boolean objectChangedJS = false;\n    private boolean objectChangedJP = false;\n\n    public AnalyzeRule(BaseBookBean bookBean, BookSourceBean bookSourceBean) {\n        book = bookBean;\n        bookSource = bookSourceBean;\n    }\n\n    public void setBook(BaseBookBean book) {\n        this.book = book;\n    }\n\n    public AnalyzeRule setContent(Object body) {\n        return setContent(body, baseUrl);\n    }\n\n    public Object getContent() {\n        return object;\n    }\n\n    public AnalyzeRule setContent(Object body, String baseUrl) {\n        if (body == null) throw new AssertionError(\"Content cannot be null\");\n        isJSON = StringUtils.isJsonType(String.valueOf(body));\n        object = body;\n        if (baseUrl != null) {\n            this.baseUrl = headerPattern.matcher(baseUrl).replaceAll(\"\");\n        }\n        objectChangedXP = true;\n        objectChangedJS = true;\n        objectChangedJP = true;\n        return this;\n    }\n\n    public String getBaseUrl() {\n        return this.baseUrl;\n    }\n\n    /**\n     * 获取XPath解析类\n     */\n    private AnalyzeByXPath getAnalyzeByXPath(Object o) {\n        if (o != object) {\n            return new AnalyzeByXPath().parse(o);\n        }\n        return getAnalyzeByXPath();\n    }\n\n    private AnalyzeByXPath getAnalyzeByXPath() {\n        if (analyzeByXPath == null || objectChangedXP) {\n            analyzeByXPath = new AnalyzeByXPath();\n            analyzeByXPath.parse(object);\n            objectChangedXP = false;\n        }\n        return analyzeByXPath;\n    }\n\n    /**\n     * 获取JSOUP解析类\n     */\n    private AnalyzeByJSoup getAnalyzeByJSoup(Object o) {\n        if (o != object) {\n            return new AnalyzeByJSoup().parse(o);\n        }\n        return getAnalyzeByJSoup();\n    }\n\n    private AnalyzeByJSoup getAnalyzeByJSoup() {\n        if (analyzeByJSoup == null || objectChangedJS) {\n            analyzeByJSoup = new AnalyzeByJSoup();\n            analyzeByJSoup.parse(object);\n            objectChangedJS = false;\n        }\n        return analyzeByJSoup;\n    }\n\n    /**\n     * 获取JSON解析类\n     */\n    private AnalyzeByJSonPath getAnalyzeByJSonPath(Object o) {\n        if (o != object) {\n            return new AnalyzeByJSonPath().parse(o);\n        }\n        return getAnalyzeByJSonPath();\n    }\n\n    private AnalyzeByJSonPath getAnalyzeByJSonPath() {\n        if (analyzeByJSonPath == null || objectChangedJP) {\n            analyzeByJSonPath = new AnalyzeByJSonPath();\n            analyzeByJSonPath.parse(object);\n            objectChangedJP = false;\n        }\n        return analyzeByJSonPath;\n    }\n\n    /**\n     * 获取文本列表\n     */\n    public List<String> getStringList(String rule) throws Exception {\n        return getStringList(rule, false);\n    }\n\n    public List<String> getStringList(String rule, boolean isUrl) throws Exception {\n        if (isEmpty(rule)) return null;\n        List<SourceRule> ruleList = splitSourceRule(rule);\n        return getStringList(ruleList, isUrl);\n    }\n\n    @SuppressWarnings({\"unchecked\"})\n    public List<String> getStringList(List<SourceRule> ruleList, boolean isUrl) throws Exception {\n        Object result = null;\n        if (!ruleList.isEmpty()) result = object;\n        for (SourceRule rule : ruleList) {\n            if (!isEmpty(rule.rule)) {\n                switch (rule.mode) {\n                    case Js:\n                        result = evalJS(rule.rule, result);\n                        break;\n                    case JSon:\n                        result = getAnalyzeByJSonPath(result).getStringList(rule.rule);\n                        break;\n                    case XPath:\n                        result = getAnalyzeByXPath(result).getStringList(rule.rule);\n                        break;\n                    default:\n                        result = getAnalyzeByJSoup(result).getStringList(rule.rule);\n                }\n            }\n            if (!isEmpty(rule.replaceRegex) && result instanceof List) {\n                List<String> newList = new ArrayList<>();\n                //noinspection rawtypes\n                for (Object item : (List) result) {\n                    newList.add(replaceRegex(String.valueOf(item), rule));\n                }\n                result = newList;\n            } else if (!isEmpty(rule.replaceRegex)) {\n                result = replaceRegex(String.valueOf(result), rule);\n            }\n        }\n        if (result == null) return new ArrayList<>();\n        if (result instanceof String) {\n            result = Arrays.asList(StringUtils.formatHtml((String) result).split(\"\\n\"));\n        }\n        if (isUrl && !isEmpty(baseUrl)) {\n            List<String> urlList = new ArrayList<>();\n            //noinspection rawtypes\n            for (Object url : (List) result) {\n                String absoluteURL = NetworkUtils.getAbsoluteURL(baseUrl, String.valueOf(url));\n                if (!urlList.contains(absoluteURL) && !isEmpty(absoluteURL)) {\n                    urlList.add(absoluteURL);\n                }\n            }\n            return urlList;\n        }\n        return (List<String>) result;\n    }\n\n    /**\n     * 获取文本\n     */\n    public String getString(String rule) throws Exception {\n        return getString(rule, false);\n    }\n\n    public String getString(String ruleStr, boolean isUrl) throws Exception {\n        if (isEmpty(ruleStr)) return null;\n        List<SourceRule> ruleList = splitSourceRule(ruleStr);\n        return getString(ruleList, isUrl);\n    }\n\n    public String getString(List<SourceRule> ruleList) throws Exception {\n        return getString(ruleList, false);\n    }\n\n    public String getString(List<SourceRule> ruleList, boolean isUrl) throws Exception {\n        Object result = null;\n        if (!ruleList.isEmpty()) result = object;\n        for (SourceRule rule : ruleList) {\n            if (!StringUtils.isTrimEmpty(rule.rule)) {\n                switch (rule.mode) {\n                    case Js:\n                        result = evalJS(rule.rule, result);\n                        break;\n                    case JSon:\n                        result = getAnalyzeByJSonPath(result).getString(rule.rule);\n                        break;\n                    case XPath:\n                        result = getAnalyzeByXPath(result).getString(rule.rule);\n                        break;\n                    case Default:\n                        if (isUrl && !isEmpty(baseUrl)) {\n                            result = getAnalyzeByJSoup(result).getString0(rule.rule);\n                        } else {\n                            result = getAnalyzeByJSoup(result).getString(rule.rule);\n                        }\n                }\n            }\n            if (!isEmpty(rule.replaceRegex)) {\n                result = replaceRegex(String.valueOf(result), rule);\n            }\n        }\n        if (result == null) return \"\";\n        if (isUrl && !StringUtils.isTrimEmpty(baseUrl)) {\n            return NetworkUtils.getAbsoluteURL(baseUrl, Entities.unescape(String.valueOf(result)));\n        }\n        try {\n            return Entities.unescape(String.valueOf(result));\n        } catch (Exception e) {\n            return String.valueOf(result);\n        }\n    }\n\n    /**\n     * 获取Element\n     */\n    public Object getElement(String ruleStr) throws Exception {\n        List<SourceRule> ruleList = splitSourceRule(ruleStr);\n        Object result = object;\n        for (SourceRule rule : ruleList) {\n            switch (rule.mode) {\n                case Js:\n                    result = evalJS(rule.rule, result);\n                    break;\n                case JSon:\n                    result = getAnalyzeByJSonPath(result).getObject(rule.rule);\n                    break;\n                case XPath:\n                    result = getAnalyzeByXPath(result).getElements(rule.rule);\n                    break;\n                default:\n                    result = getAnalyzeByJSoup(result).getElements(rule.rule);\n            }\n            if (!isEmpty(rule.replaceRegex) && result instanceof String) {\n                result = replaceRegex(String.valueOf(result), rule);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 获取列表\n     */\n    @SuppressWarnings(\"unchecked\")\n    public List<Object> getElements(String ruleStr) throws Exception {\n        List<SourceRule> ruleList = splitSourceRule(ruleStr);\n        Object result = null;\n        if (!ruleList.isEmpty()) result = object;\n        for (SourceRule rule : ruleList) {\n            switch (rule.mode) {\n                case Js:\n                    result = evalJS(rule.rule, result);\n                    break;\n                case JSon:\n                    result = getAnalyzeByJSonPath(result).getList(rule.rule);\n                    break;\n                case XPath:\n                    result = getAnalyzeByXPath(result).getElements(rule.rule);\n                    break;\n                default:\n                    result = getAnalyzeByJSoup(result).getElements(rule.rule);\n            }\n            if (!isEmpty(rule.replaceRegex) && result instanceof String) {\n                result = replaceRegex(String.valueOf(result), rule);\n            }\n        }\n        if (result == null) {\n            return new ArrayList<>();\n        }\n        return (List<Object>) result;\n    }\n\n    /**\n     * 保存变量\n     */\n    private void putRule(Map<String, String> map) throws Exception {\n        for (Map.Entry<String, String> entry : map.entrySet()) {\n            if (book != null) {\n                book.putVariable(entry.getKey(), getString(entry.getValue()));\n            }\n        }\n    }\n\n    /**\n     * 分离并执行put规则\n     */\n    private String splitPutRule(String ruleStr) throws Exception {\n        Matcher putMatcher = putPattern.matcher(ruleStr);\n        while (putMatcher.find()) {\n            ruleStr = ruleStr.replace(putMatcher.group(), \"\");\n            Map<String, String> map = new Gson().fromJson(putMatcher.group(1), MAP_STRING);\n            putRule(map);\n        }\n        return ruleStr;\n    }\n\n    /**\n     * 替换@get\n     */\n    public String replaceGet(String ruleStr) {\n        Matcher getMatcher = getPattern.matcher(ruleStr);\n        while (getMatcher.find()) {\n            String value = \"\";\n            if (book != null && book.getVariableMap() != null) {\n                value = book.getVariableMap().get(getMatcher.group(1));\n                if (value == null) value = \"\";\n            }\n            ruleStr = ruleStr.replace(getMatcher.group(), value);\n        }\n        return ruleStr;\n    }\n\n    /**\n     * 正则替换\n     */\n    private String replaceRegex(String result, SourceRule rule) {\n        if (!isEmpty(rule.replaceRegex)) {\n            if (rule.replaceFirst) {\n                Pattern pattern = Pattern.compile(rule.replaceRegex);\n                Matcher matcher = pattern.matcher(String.valueOf(result));\n                if (matcher.find()) {\n                    result = matcher.group(0).replaceFirst(rule.replaceRegex, rule.replacement);\n                } else {\n                    result = \"\";\n                }\n            } else {\n                result = String.valueOf(result).replaceAll(rule.replaceRegex, rule.replacement);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 替换JS\n     */\n    @SuppressLint(\"DefaultLocale\")\n    private String replaceJs(String ruleStr) throws Exception {\n        if (ruleStr.contains(\"{{\") && ruleStr.contains(\"}}\")) {\n            Object jsEval;\n            StringBuffer sb = new StringBuffer(ruleStr.length());\n            Matcher expMatcher = EXP_PATTERN.matcher(ruleStr);\n            while (expMatcher.find()) {\n                jsEval = evalJS(expMatcher.group(1), object);\n                if (jsEval instanceof String) {\n                    expMatcher.appendReplacement(sb, (String) jsEval);\n                } else if (jsEval instanceof Double && ((Double) jsEval) % 1.0 == 0) {\n                    expMatcher.appendReplacement(sb, String.format(\"%.0f\", (Double) jsEval));\n                } else {\n                    expMatcher.appendReplacement(sb, String.valueOf(jsEval));\n                }\n            }\n            expMatcher.appendTail(sb);\n            ruleStr = sb.toString();\n        }\n        return ruleStr;\n    }\n\n    /**\n     * 分解规则生成规则列表\n     */\n    public List<SourceRule> splitSourceRule(String ruleStr) throws Exception {\n        List<SourceRule> ruleList = new ArrayList<>();\n        if (isEmpty(ruleStr)) return ruleList;\n        //检测Mode\n        Mode mode;\n        if (StringUtils.startWithIgnoreCase(ruleStr, \"@XPath:\")) {\n            mode = Mode.XPath;\n            ruleStr = ruleStr.substring(7);\n        } else if (StringUtils.startWithIgnoreCase(ruleStr, \"@JSon:\")) {\n            mode = Mode.JSon;\n            ruleStr = ruleStr.substring(6);\n        } else {\n            if (isJSON) {\n                mode = Mode.JSon;\n            } else {\n                mode = Mode.Default;\n            }\n        }\n        //分离put规则\n        ruleStr = splitPutRule(ruleStr);\n        //替换get值\n        ruleStr = replaceGet(ruleStr);\n        //替换js\n        ruleStr = replaceJs(ruleStr);\n        //拆分为列表\n        int start = 0;\n        String tmp;\n        Matcher jsMatcher = JS_PATTERN.matcher(ruleStr);\n        while (jsMatcher.find()) {\n            if (jsMatcher.start() > start) {\n                tmp = ruleStr.substring(start, jsMatcher.start()).replaceAll(\"\\n\", \"\").trim();\n                if (!isEmpty(tmp)) {\n                    ruleList.add(new SourceRule(tmp, mode));\n                }\n            }\n            ruleList.add(new SourceRule(jsMatcher.group(), Mode.Js));\n            start = jsMatcher.end();\n        }\n        if (ruleStr.length() > start) {\n            tmp = ruleStr.substring(start).replaceAll(\"\\n\", \"\").trim();\n            if (!isEmpty(tmp)) {\n                ruleList.add(new SourceRule(tmp, mode));\n            }\n        }\n        return ruleList;\n    }\n\n    /**\n     * 规则类\n     */\n    public static class SourceRule {\n        Mode mode;\n        String rule;\n        String replaceRegex = \"\";\n        String replacement = \"\";\n        boolean replaceFirst = false;\n\n        SourceRule(String ruleStr, Mode mainMode) {\n            this.mode = mainMode;\n            if (mode == Mode.Js) {\n                if (ruleStr.startsWith(\"<js>\")) {\n                    rule = ruleStr.substring(4, ruleStr.lastIndexOf(\"<\"));\n                } else {\n                    rule = ruleStr.substring(4);\n                }\n            } else {\n                if (StringUtils.startWithIgnoreCase(ruleStr, \"@XPath:\")) {\n                    mode = Mode.XPath;\n                    rule = ruleStr.substring(7);\n                } else if (StringUtils.startWithIgnoreCase(ruleStr, \"//\")) {//XPath特征很明显,无需配置单独的识别标头\n                    mode = Mode.XPath;\n                    rule = ruleStr;\n                } else if (StringUtils.startWithIgnoreCase(ruleStr, \"@JSon:\")) {\n                    mode = Mode.JSon;\n                    rule = ruleStr.substring(6);\n                } else if (ruleStr.startsWith(\"$.\")) {\n                    mode = Mode.JSon;\n                    rule = ruleStr;\n                } else {\n                    rule = ruleStr;\n                }\n                //分离正则表达式\n                String[] ruleStrS = rule.trim().split(\"##\");\n                rule = ruleStrS[0];\n                if (ruleStrS.length > 1) {\n                    replaceRegex = ruleStrS[1];\n                }\n                if (ruleStrS.length > 2) {\n                    replacement = ruleStrS[2];\n                }\n                if (ruleStrS.length > 3) {\n                    replaceFirst = true;\n                }\n            }\n        }\n\n    }\n\n    private enum Mode {\n        XPath, JSon, Default, Js\n    }\n\n    public String put(String key, String value) {\n        if (book != null) {\n            book.putVariable(key, value);\n        }\n        return value;\n    }\n\n    public String get(String key) {\n        if (book == null) {\n            return null;\n        }\n        if (book.getVariableMap() == null) {\n            return null;\n        }\n        return book.getVariableMap().get(key);\n    }\n\n    /**\n     * 执行JS\n     */\n    public Object evalJS(String jsStr, Object result) throws Exception {\n        SimpleBindings bindings = new SimpleBindings();\n        bindings.put(\"java\", this);\n        bindings.put(\"source\", bookSource);\n        bindings.put(\"result\", result);\n        bindings.put(\"baseUrl\", baseUrl);\n        return SCRIPT_ENGINE.eval(jsStr, bindings);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/analyzeRule/AnalyzeUrl.java",
    "content": "package com.kunfei.bookshelf.model.analyzeRule;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.EXP_PATTERN;\nimport static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN;\nimport static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING;\nimport static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE;\nimport static com.kunfei.bookshelf.utils.NetworkUtils.headerPattern;\n\nimport android.annotation.SuppressLint;\nimport android.text.TextUtils;\n\nimport androidx.annotation.Keep;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.help.JsExtensions;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.UrlEncoderUtils;\n\nimport java.net.URLEncoder;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport javax.script.SimpleBindings;\n\nimport okhttp3.MediaType;\nimport okhttp3.RequestBody;\n\n/**\n * Created by GKF on 2018/1/24.\n * 搜索URL规则解析\n */\n@Keep\npublic class AnalyzeUrl implements JsExtensions {\n    private static final Pattern pagePattern = Pattern.compile(\"(?<!@)\\\\{(.*?)\\\\}\");\n    private final BookSourceBean bookSource;\n    private String baseUrl;\n    private String ruleUrl;\n    private String url;\n    private String host;\n    private String urlPath;\n    private String queryStr;\n    private final Map<String, String> queryMap = new LinkedHashMap<>();\n    private final Map<String, String> headerMap = new HashMap<>();\n    private String charCode = null;\n    private UrlMode urlMode = UrlMode.DEFAULT;\n    private String jsonBody = null;\n    private final String searchKey;\n    private final int searchPage;\n\n    public AnalyzeUrl(String urlRule, Map<String, String> headerMap) throws Exception {\n        this(urlRule, null, headerMap);\n    }\n\n    public AnalyzeUrl(String urlRule, String baseUrl, Map<String, String> headerMap) throws Exception {\n        this(urlRule, baseUrl, null, headerMap);\n    }\n\n    public AnalyzeUrl(String urlRule, String baseUrl, BookSourceBean bookSource, Map<String, String> headerMap) throws Exception {\n        this(urlRule, baseUrl, bookSource, null, 1, headerMap);\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    public AnalyzeUrl(String urlRule, String baseUrl, BookSourceBean bookSource, final String key, final int page, Map<String, String> headerMap) throws Exception {\n        if (!TextUtils.isEmpty(baseUrl)) {\n            this.baseUrl = headerPattern.matcher(baseUrl).replaceAll(\"\");\n        }\n        this.bookSource = bookSource;\n        this.searchKey = key;\n        this.searchPage = page;\n        ruleUrl = urlRule;\n        //替换关键字\n        if (!StringUtils.isTrimEmpty(key)) {\n            // 处理searchKey=searchKey的情况\n            if (ruleUrl.matches(\"=[\\\\s{(]*searchKey\"))\n                ruleUrl = ruleUrl.replaceFirst(\"=[\\\\s{(]*searchKey\", \"=\" + key);\n            else\n                ruleUrl = ruleUrl.replace(\"searchKey\", key);\n        }\n        //判断是否有下一页\n        if (page > 1 && !ruleUrl.contains(\"searchPage\"))\n            throw new Exception(\"没有下一页\");\n        //替换js\n        ruleUrl = replaceJs(ruleUrl);\n        //解析Header\n        ruleUrl = analyzeHeader(ruleUrl, headerMap);\n        //分离编码规则\n        ruleUrl = splitCharCode(ruleUrl);\n        //设置页数\n        ruleUrl = analyzePage(ruleUrl, page);\n        //执行规则列表\n        List<String> ruleList = splitRule(ruleUrl);\n        for (String rule : ruleList) {\n            if (rule.startsWith(\"<js>\")) {\n                rule = rule.substring(4, rule.lastIndexOf(\"<\"));\n                ruleUrl = (String) evalJS(rule, ruleUrl);\n            } else if (rule.startsWith(\"@js:\")) {\n                rule = rule.substring(4);\n                ruleUrl = (String) evalJS(rule, ruleUrl);\n            } else {\n                ruleUrl = rule.replace(\"@result\", ruleUrl);\n            }\n        }\n        //分离post参数\n        String[] ruleUrlS = ruleUrl.split(\"@\");\n        if (ruleUrlS.length > 1) {\n            urlMode = UrlMode.POST;\n        } else {\n            //分离get参数\n            ruleUrlS = ruleUrlS[0].split(\"\\\\?\");\n            if (ruleUrlS.length > 1) {\n                urlMode = UrlMode.GET;\n            }\n        }\n        generateUrlPath(ruleUrlS[0]);\n        if (urlMode == UrlMode.GET) {\n            analyzeQuery(ruleUrlS[1]);\n        } else if (urlMode == UrlMode.POST) {\n            if (StringUtils.isJsonType(ruleUrlS[1])) {\n                jsonBody = ruleUrlS[1];\n            } else {\n                analyzeQuery(ruleUrlS[1]);\n            }\n        }\n    }\n\n    /**\n     * 解析Header\n     */\n    private String analyzeHeader(String ruleUrl, Map<String, String> headerMapF) {\n        if (headerMapF != null) {\n            headerMap.putAll(headerMapF);\n        }\n        Matcher matcher = headerPattern.matcher(ruleUrl);\n        if (matcher.find()) {\n            String find = matcher.group(0);\n            ruleUrl = ruleUrl.replace(find, \"\");\n            find = find.substring(8);\n            try {\n                Map<String, String> map = new Gson().fromJson(find, MAP_STRING);\n                headerMap.putAll(map);\n            } catch (Exception ignored) {\n            }\n        }\n        return ruleUrl;\n    }\n\n    /**\n     * 分离编码规则\n     */\n    private String splitCharCode(String rule) {\n        String[] ruleUrlS = rule.split(\"\\\\|\");\n        if (ruleUrlS.length > 1) {\n            if (!TextUtils.isEmpty(ruleUrlS[1])) {\n                String[] qtS = ruleUrlS[1].split(\"&\");\n                for (String qt : qtS) {\n                    String[] gz = qt.split(\"=\");\n                    if (gz[0].equals(\"char\")) {\n                        charCode = gz[1];\n                    }\n                }\n            }\n        }\n        return ruleUrlS[0];\n    }\n\n    /**\n     * 解析页数\n     */\n    private String analyzePage(String ruleUrl, final Integer searchPage) {\n        if (searchPage == null) return ruleUrl;\n        Matcher matcher = pagePattern.matcher(ruleUrl);\n        while (matcher.find()) {\n            String[] pages = matcher.group(1).split(\",\");\n            if (searchPage <= pages.length) {\n                ruleUrl = ruleUrl.replace(matcher.group(), pages[searchPage - 1].trim());\n            } else {\n                ruleUrl = ruleUrl.replace(matcher.group(), pages[pages.length - 1].trim());\n            }\n        }\n        return ruleUrl.replace(\"searchPage-1\", String.valueOf(searchPage - 1))\n                .replace(\"searchPage+1\", String.valueOf(searchPage + 1))\n                .replace(\"searchPage\", String.valueOf(searchPage));\n    }\n\n    /**\n     * 替换js\n     */\n    @SuppressLint(\"DefaultLocale\")\n    private String replaceJs(String ruleUrl) throws Exception {\n        if (ruleUrl.contains(\"{{\") && ruleUrl.contains(\"}}\")) {\n            Object jsEval;\n            StringBuffer sb = new StringBuffer(ruleUrl.length());\n            Matcher expMatcher = EXP_PATTERN.matcher(ruleUrl);\n            while (expMatcher.find()) {\n                jsEval = evalJS(expMatcher.group(1), ruleUrl);\n                if (jsEval instanceof String) {\n                    expMatcher.appendReplacement(sb, (String) jsEval);\n                } else if (jsEval instanceof Double && ((Double) jsEval) % 1.0 == 0) {\n                    expMatcher.appendReplacement(sb, String.format(\"%.0f\", (Double) jsEval));\n                } else {\n                    expMatcher.appendReplacement(sb, String.valueOf(jsEval));\n                }\n            }\n            expMatcher.appendTail(sb);\n            ruleUrl = sb.toString();\n        }\n        return ruleUrl;\n    }\n\n    /**\n     * 解析QueryMap\n     */\n    private void analyzeQuery(String allQuery) throws Exception {\n        queryStr = allQuery;\n        String[] queryS = allQuery.split(\"&\");\n        for (String query : queryS) {\n            String[] queryM = query.split(\"=\");\n            String value = queryM.length > 1 ? queryM[1] : \"\";\n            if (TextUtils.isEmpty(charCode)) {\n                if (UrlEncoderUtils.hasUrlEncoded(value)) {\n                    queryMap.put(queryM[0], value);\n                } else {\n                    queryMap.put(queryM[0], URLEncoder.encode(value, \"UTF-8\"));\n                }\n            } else if (charCode.equals(\"escape\")) {\n                queryMap.put(queryM[0], StringUtils.escape(value));\n            } else {\n                queryMap.put(queryM[0], URLEncoder.encode(value, charCode));\n            }\n        }\n    }\n\n    /**\n     * 拆分规则\n     */\n    private List<String> splitRule(String ruleStr) {\n        List<String> ruleList = new ArrayList<>();\n        Matcher jsMatcher = JS_PATTERN.matcher(ruleStr);\n        int start = 0;\n        String tmp;\n        while (jsMatcher.find()) {\n            if (jsMatcher.start() > start) {\n                tmp = ruleStr.substring(start, jsMatcher.start()).replaceAll(\"\\n\", \"\").trim();\n                if (!TextUtils.isEmpty(tmp)) {\n                    ruleList.add(tmp);\n                }\n            }\n            ruleList.add(jsMatcher.group());\n            start = jsMatcher.end();\n        }\n        if (ruleStr.length() > start) {\n            tmp = ruleStr.substring(start).replaceAll(\"\\n\", \"\").trim();\n            if (!TextUtils.isEmpty(tmp)) {\n                ruleList.add(tmp);\n            }\n        }\n        return ruleList;\n    }\n\n    /**\n     * 分解URL\n     */\n    private void generateUrlPath(String ruleUrl) {\n        url = NetworkUtils.getAbsoluteURL(baseUrl, ruleUrl);\n        host = StringUtils.getBaseUrl(url);\n        urlPath = url.substring(host.length());\n    }\n\n    /**\n     * 执行JS\n     */\n    private Object evalJS(String jsStr, Object result) throws Exception {\n        SimpleBindings bindings = new SimpleBindings();\n        bindings.put(\"java\", this);\n        bindings.put(\"baseUrl\", baseUrl);\n        bindings.put(\"searchPage\", searchPage);\n        bindings.put(\"searchKey\", searchKey);\n        bindings.put(\"source\", bookSource);\n        bindings.put(\"result\", result);\n        return SCRIPT_ENGINE.eval(jsStr, bindings);\n    }\n\n    public String getCharCode() {\n        return charCode;\n    }\n\n    public String getHost() {\n        return host;\n    }\n\n    public String getPath() {\n        return urlPath;\n    }\n\n    public String getRuleUrl() {\n        return ruleUrl;\n    }\n\n    public String getUrl() {\n        return url;\n    }\n\n    public Map<String, String> getQueryMap() {\n        return queryMap;\n    }\n\n    public Map<String, String> getHeaderMap() {\n        return headerMap;\n    }\n\n    public String getQueryStr() {\n        return queryStr;\n    }\n\n    public String getJsonBody() {\n        return jsonBody;\n    }\n\n    public byte[] getPostData() {\n        StringBuilder builder = new StringBuilder();\n        Set<String> keys = queryMap.keySet();\n        for (String key : keys) {\n            builder.append(String.format(\"%s=%s&\", key, queryMap.get(key)));\n        }\n        builder.deleteCharAt(builder.lastIndexOf(\"&\"));\n        return builder.toString().getBytes();\n    }\n\n    public RequestBody getPostBody() {\n        MediaType mediaType = MediaType.parse(\"application/json; charset=UTF-8\");\n        return RequestBody.create(mediaType, jsonBody);\n    }\n\n    public UrlMode getUrlMode() {\n        return urlMode;\n    }\n\n    public enum UrlMode {\n        GET, POST, DEFAULT\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/content/BookChapterList.java",
    "content": "package com.kunfei.bookshelf.model.content;\n\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.WebChapterBean;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeByRegex;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl;\nimport com.kunfei.bookshelf.model.task.AnalyzeNextUrlTask;\n\nimport org.jsoup.nodes.Element;\nimport org.mozilla.javascript.NativeObject;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport io.reactivex.Emitter;\nimport io.reactivex.Observable;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport retrofit2.Response;\n\npublic class BookChapterList {\n    private String tag;\n    private BookSourceBean bookSourceBean;\n    private List<WebChapterBean> webChapterBeans;\n    private boolean dx = false;\n    private boolean analyzeNextUrl;\n    private CompositeDisposable compositeDisposable;\n    private String chapterListUrl;\n\n    BookChapterList(String tag, BookSourceBean bookSourceBean, boolean analyzeNextUrl) {\n        this.tag = tag;\n        this.bookSourceBean = bookSourceBean;\n        this.analyzeNextUrl = analyzeNextUrl;\n    }\n\n    public Observable<List<BookChapterBean>> analyzeChapterList(final String s, final BookShelfBean bookShelfBean, Map<String, String> headerMap) {\n        return Observable.create(e -> {\n            if (TextUtils.isEmpty(s)) {\n                e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_chapter_list_error) + bookShelfBean.getBookInfoBean().getChapterUrl()));\n                return;\n            } else {\n                Debug.printLog(tag, 1, \"┌成功获取目录页\", analyzeNextUrl);\n                Debug.printLog(tag, 1, \"└\" + bookShelfBean.getBookInfoBean().getChapterUrl(), analyzeNextUrl);\n            }\n            bookShelfBean.setTag(tag);\n            AnalyzeRule analyzer = new AnalyzeRule(bookShelfBean, bookSourceBean);\n            String ruleChapterList = bookSourceBean.getRuleChapterList();\n            if (ruleChapterList != null && ruleChapterList.startsWith(\"-\")) {\n                dx = true;\n                ruleChapterList = ruleChapterList.substring(1);\n            }\n            chapterListUrl = bookShelfBean.getBookInfoBean().getChapterUrl();\n            WebChapterBean webChapterBean = analyzeChapterList(s, chapterListUrl, ruleChapterList, analyzeNextUrl, analyzer, dx);\n            final List<BookChapterBean> chapterList = webChapterBean.getData();\n\n            final List<String> chapterUrlS = new ArrayList<>(webChapterBean.getNextUrlList());\n            if (chapterUrlS.isEmpty() || !analyzeNextUrl) {\n                finish(chapterList, e);\n            }\n            //下一页为单页\n            else if (chapterUrlS.size() == 1) {\n                List<String> usedUrl = new ArrayList<>();\n                usedUrl.add(bookShelfBean.getBookInfoBean().getChapterUrl());\n                //循环获取直到下一页为空\n                Debug.printLog(tag, \"正在加载下一页\");\n                while (!chapterUrlS.isEmpty() && !usedUrl.contains(chapterUrlS.get(0))) {\n                    usedUrl.add(chapterUrlS.get(0));\n                    AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                            chapterUrlS.get(0), tag, bookSourceBean,\n                            bookSourceBean.getHeaderMap(true)\n                    );\n                    try {\n                        String body;\n                        Response<String> response = BaseModelImpl.getInstance().getResponseO(analyzeUrl)\n                                .blockingFirst();\n                        body = response.body();\n                        webChapterBean = analyzeChapterList(body, chapterUrlS.get(0), ruleChapterList, false, analyzer, dx);\n                        chapterList.addAll(webChapterBean.getData());\n                        chapterUrlS.clear();\n                        chapterUrlS.addAll(webChapterBean.getNextUrlList());\n                    } catch (Exception exception) {\n                        if (!e.isDisposed()) {\n                            e.onError(exception);\n                        }\n                    }\n                }\n                Debug.printLog(tag, \"下一页加载完成共\" + usedUrl.size() + \"页\");\n                finish(chapterList, e);\n            }\n            //下一页为多页\n            else {\n                Debug.printLog(tag, \"正在加载其它\" + chapterUrlS.size() + \"页\");\n                compositeDisposable = new CompositeDisposable();\n                webChapterBeans = new ArrayList<>();\n                AnalyzeNextUrlTask.Callback callback = new AnalyzeNextUrlTask.Callback() {\n                    @Override\n                    public void addDisposable(Disposable disposable) {\n                        compositeDisposable.add(disposable);\n                    }\n\n                    @Override\n                    public void analyzeFinish(WebChapterBean bean, List<BookChapterBean> chapterListBeans) {\n                        if (nextUrlFinish(bean, chapterListBeans)) {\n                            for (WebChapterBean chapterBean : webChapterBeans) {\n                                chapterList.addAll(chapterBean.getData());\n                            }\n                            Debug.printLog(tag, \"其它页加载完成,目录共\" + chapterList.size() + \"条\");\n                            finish(chapterList, e);\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable throwable) {\n                        compositeDisposable.dispose();\n                        e.onError(throwable);\n                    }\n                };\n                for (String url : chapterUrlS) {\n                    final WebChapterBean bean = new WebChapterBean(url);\n                    webChapterBeans.add(bean);\n                }\n                for (WebChapterBean bean : webChapterBeans) {\n                    BookChapterList bookChapterList =\n                            new BookChapterList(tag, bookSourceBean, false);\n                    AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                            bean.getUrl(), tag, bookSourceBean,\n                            bookSourceBean.getHeaderMap(true)\n                    );\n                    new AnalyzeNextUrlTask(bookChapterList, bean, bookShelfBean, headerMap)\n                            .setCallback(callback)\n                            .analyzeUrl(analyzeUrl);\n                }\n            }\n        });\n    }\n\n    private synchronized boolean nextUrlFinish(WebChapterBean webChapterBean, List<BookChapterBean> bookChapterBeans) {\n        webChapterBean.setData(bookChapterBeans);\n        for (WebChapterBean bean : webChapterBeans) {\n            if (bean.noData()) return false;\n        }\n        return true;\n    }\n\n    private void finish(List<BookChapterBean> chapterList, Emitter<List<BookChapterBean>> emitter) {\n        //去除重复,保留后面的,先倒序,从后面往前判断\n        if (!dx) {\n            Collections.reverse(chapterList);\n        }\n        LinkedHashSet<BookChapterBean> lh = new LinkedHashSet<>(chapterList);\n        chapterList = new ArrayList<>(lh);\n        Collections.reverse(chapterList);\n        Debug.printLog(tag, 1, \"-目录解析完成\", analyzeNextUrl);\n        emitter.onNext(chapterList);\n        emitter.onComplete();\n    }\n\n    private WebChapterBean analyzeChapterList(String s, String chapterUrl, String ruleChapterList,\n                                              boolean printLog, AnalyzeRule analyzer, boolean dx) throws Exception {\n        List<String> nextUrlList = new ArrayList<>();\n        analyzer.setContent(s, chapterUrl);\n        if (!TextUtils.isEmpty(bookSourceBean.getRuleChapterUrlNext()) && analyzeNextUrl) {\n            Debug.printLog(tag, 1, \"┌获取目录下一页网址\", printLog);\n            nextUrlList = analyzer.getStringList(bookSourceBean.getRuleChapterUrlNext(), true);\n            int thisUrlIndex = nextUrlList.indexOf(chapterUrl);\n            if (thisUrlIndex != -1) {\n                nextUrlList.remove(thisUrlIndex);\n            }\n            Debug.printLog(tag, 1, \"└\" + nextUrlList.toString(), printLog);\n        }\n\n        List<BookChapterBean> chapterBeans = new ArrayList<>();\n        Debug.printLog(tag, 1, \"┌解析目录列表\", printLog);\n        // 仅使用java正则表达式提取目录列表\n        if (ruleChapterList.startsWith(\":\")) {\n            ruleChapterList = ruleChapterList.substring(1);\n            regexChapter(s, ruleChapterList.split(\"&&\"), 0, analyzer, chapterBeans);\n            if (chapterBeans.size() == 0) {\n                Debug.printLog(tag, 1, \"└找到 0 个章节\", printLog);\n                return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList));\n            }\n        }\n        // 使用AllInOne规则模式提取目录列表\n        else if (ruleChapterList.startsWith(\"+\")) {\n            ruleChapterList = ruleChapterList.substring(1);\n            List<Object> collections = analyzer.getElements(ruleChapterList);\n            if (collections.size() == 0) {\n                Debug.printLog(tag, 1, \"└找到 0 个章节\", printLog);\n                return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList));\n            }\n            String nameRule = bookSourceBean.getRuleChapterName();\n            String linkRule = bookSourceBean.getRuleContentUrl();\n            String vipRule = bookSourceBean.getRuleChapterVip();\n            String payRule = bookSourceBean.getRuleChapterPay();\n            String name = \"\";\n            String link = \"\";\n            boolean isVip = false;\n            boolean isPay = false;\n            String vipResult = \"\";\n            String payResult = \"\";\n            for (Object object : collections) {\n                if (object instanceof NativeObject) {\n                    name = String.valueOf(((NativeObject) object).get(nameRule));\n                    link = String.valueOf(((NativeObject) object).get(linkRule));\n                    vipResult = String.valueOf(((NativeObject) object).get(vipRule));\n                    payResult = String.valueOf(((NativeObject) object).get(payRule));\n                } else if (object instanceof Element) {\n                    name = ((Element) object).text();\n                    link = ((Element) object).attr(linkRule);\n                }\n                if (!TextUtils.isEmpty(vipResult) && !vipResult.matches(\"\\\\s*(?i)(null|false|0)\\\\s*\")) {\n                    isVip = true;\n                }\n                if (!TextUtils.isEmpty(payResult) && !payResult.matches(\"\\\\s*(?i)(null|false|0)\\\\s*\")) {\n                    isPay = true;\n                }\n                addChapter(chapterBeans, name, link, isVip, isPay);\n            }\n        }\n        // 使用默认规则解析流程提取目录列表\n        else {\n            List<Object> collections = analyzer.getElements(ruleChapterList);\n            if (collections.size() == 0) {\n                Debug.printLog(tag, 1, \"└找到 0 个章节\", printLog);\n                return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList));\n            }\n            List<AnalyzeRule.SourceRule> nameRule = analyzer.splitSourceRule(bookSourceBean.getRuleChapterName());\n            List<AnalyzeRule.SourceRule> linkRule = analyzer.splitSourceRule(bookSourceBean.getRuleContentUrl());\n            List<AnalyzeRule.SourceRule> vipRule = analyzer.splitSourceRule(bookSourceBean.getRuleChapterVip());\n            List<AnalyzeRule.SourceRule> payRule = analyzer.splitSourceRule(bookSourceBean.getRuleChapterPay());\n            for (Object object : collections) {\n                analyzer.setContent(object, chapterUrl);\n                String name = analyzer.getString(nameRule);\n                String url = analyzer.getString(linkRule);\n                boolean isVip = false;\n                boolean isPay = false;\n                String vipResult = analyzer.getString(vipRule);\n                String payResult = analyzer.getString(payRule);\n                if (!TextUtils.isEmpty(vipResult) && !vipResult.matches(\"\\\\s*(?i)(null|false|0)\\\\s*\")) {\n                    isVip = true;\n                }\n                if (!TextUtils.isEmpty(payResult) && !payResult.matches(\"\\\\s*(?i)(null|false|0)\\\\s*\")) {\n                    isPay = true;\n                }\n                addChapter(chapterBeans, name, url, isVip, isPay);\n            }\n        }\n        Debug.printLog(tag, 1, \"└找到 \" + chapterBeans.size() + \" 个章节\", printLog);\n        BookChapterBean firstChapter;\n        if (dx) {\n            Debug.printLog(tag, 1, \"-倒序\", printLog);\n            firstChapter = chapterBeans.get(chapterBeans.size() - 1);\n        } else {\n            firstChapter = chapterBeans.get(0);\n        }\n        Debug.printLog(tag, 1, \"┌获取章节名称\", printLog);\n        Debug.printLog(tag, 1, \"└\" + firstChapter.getDurChapterName(), printLog);\n        Debug.printLog(tag, 1, \"┌获取章节网址\", printLog);\n        Debug.printLog(tag, 1, \"└\" + firstChapter.getDurChapterUrl(), printLog);\n        return new WebChapterBean(chapterBeans, new LinkedHashSet<>(nextUrlList));\n    }\n\n    private void addChapter(final List<BookChapterBean> chapterBeans,\n                            String name, String link, boolean isVip, boolean isPay\n    ) {\n        if (TextUtils.isEmpty(name)) return;\n        if (TextUtils.isEmpty(link)) link = chapterListUrl;\n        chapterBeans.add(new BookChapterBean(tag, name, link, isVip, isPay));\n    }\n\n    // region 纯java模式正则表达式获取目录列表\n    private void regexChapter(String str, String[] regex, int index, AnalyzeRule analyzer, final List<BookChapterBean> chapterBeans) throws Exception {\n        Matcher resM = Pattern.compile(regex[index]).matcher(str);\n        if (!resM.find()) {\n            return;\n        }\n        if (index + 1 == regex.length) {\n            // 获取解析规则\n            String nameRule = bookSourceBean.getRuleChapterName();\n            String linkRule = bookSourceBean.getRuleContentUrl();\n            if (TextUtils.isEmpty(nameRule) || TextUtils.isEmpty(linkRule)) return;\n            // 替换@get规则\n            nameRule = analyzer.replaceGet(bookSourceBean.getRuleChapterName());\n            linkRule = analyzer.replaceGet(bookSourceBean.getRuleContentUrl());\n            // 分离规则参数\n            List<String> nameParams = new ArrayList<>();\n            List<Integer> nameGroups = new ArrayList<>();\n            AnalyzeByRegex.splitRegexRule(nameRule, nameParams, nameGroups);\n            List<String> linkParams = new ArrayList<>();\n            List<Integer> linkGroups = new ArrayList<>();\n            AnalyzeByRegex.splitRegexRule(linkRule, linkParams, linkGroups);\n            // 是否包含VIP规则(hasVipRule>1 时视为包含vip规则)\n            int hasVipRule = 0;\n            for (int i = nameGroups.size(); i-- > 0; ) {\n                if (nameGroups.get(i) != 0) {\n                    ++hasVipRule;\n                }\n            }\n            String vipNameGroup = \"\";\n            int vipNumGroup = 0;\n            if ((nameGroups.get(0) != 0) && (hasVipRule > 1)) {\n                vipNumGroup = nameGroups.remove(0);\n                vipNameGroup = nameParams.remove(0);\n            }\n            // 创建结果缓存\n            StringBuilder cName = new StringBuilder();\n            StringBuilder cLink = new StringBuilder();\n            // 提取书籍目录\n            if (vipNumGroup != 0) {\n                do {\n                    cName.setLength(0);\n                    cLink.setLength(0);\n                    for (int i = nameParams.size(); i-- > 0; ) {\n                        if (nameGroups.get(i) > 0) {\n                            cName.insert(0, resM.group(nameGroups.get(i)));\n                        } else if (nameGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                            cName.insert(0, resM.group(nameParams.get(i)));\n                        } else {\n                            cName.insert(0, nameParams.get(i));\n                        }\n                    }\n                    if (vipNumGroup > 0) {\n                        cName.insert(0, resM.group(vipNumGroup) == null ? \"\" : \"\\uD83D\\uDD12\");\n                    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                        cName.insert(0, resM.group(vipNameGroup) == null ? \"\" : \"\\uD83D\\uDD12\");\n                    } else {\n                        cName.insert(0, vipNameGroup);\n                    }\n\n                    for (int i = linkParams.size(); i-- > 0; ) {\n                        if (linkGroups.get(i) > 0) {\n                            cLink.insert(0, resM.group(linkGroups.get(i)));\n                        } else if (linkGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                            cLink.insert(0, resM.group(linkParams.get(i)));\n                        } else {\n                            cLink.insert(0, linkParams.get(i));\n                        }\n                    }\n                    addChapter(chapterBeans, cName.toString(), cLink.toString(), false, false);\n                } while (resM.find());\n            } else {\n                do {\n                    cName.setLength(0);\n                    cLink.setLength(0);\n                    for (int i = nameParams.size(); i-- > 0; ) {\n                        if (nameGroups.get(i) > 0) {\n                            cName.insert(0, resM.group(nameGroups.get(i)));\n                        } else if (nameGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                            cName.insert(0, resM.group(nameParams.get(i)));\n                        } else {\n                            cName.insert(0, nameParams.get(i));\n                        }\n                    }\n\n                    for (int i = linkParams.size(); i-- > 0; ) {\n                        if (linkGroups.get(i) > 0) {\n                            cLink.insert(0, resM.group(linkGroups.get(i)));\n                        } else if (linkGroups.get(i) < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                            cLink.insert(0, resM.group(linkParams.get(i)));\n                        } else {\n                            cLink.insert(0, linkParams.get(i));\n                        }\n                    }\n                    addChapter(chapterBeans, cName.toString(), cLink.toString(), false, false);\n                } while (resM.find());\n            }\n        } else {\n            StringBuilder result = new StringBuilder();\n            do {\n                result.append(resM.group(0));\n            } while (resM.find());\n            regexChapter(result.toString(), regex, ++index, analyzer, chapterBeans);\n        }\n    }\n    // endregion\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/content/BookContent.java",
    "content": "package com.kunfei.bookshelf.model.content;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.bean.BaseChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.dao.BookChapterBeanDao;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Matcher;\n\nimport io.reactivex.Observable;\nimport retrofit2.Response;\n\nclass BookContent {\n    private String tag;\n    private BookSourceBean bookSourceBean;\n    private String ruleBookContent;\n    private String baseUrl;\n\n    BookContent(String tag, BookSourceBean bookSourceBean) {\n        this.tag = tag;\n        this.bookSourceBean = bookSourceBean;\n        ruleBookContent = bookSourceBean.getRuleBookContent();\n        if (ruleBookContent.startsWith(\"$\") && !ruleBookContent.startsWith(\"$.\")) {\n            ruleBookContent = ruleBookContent.substring(1);\n            Matcher jsMatcher = JS_PATTERN.matcher(ruleBookContent);\n            if (jsMatcher.find()) {\n                ruleBookContent = ruleBookContent.replace(jsMatcher.group(), \"\");\n            }\n        }\n    }\n\n    Observable<BookContentBean> analyzeBookContent(final Response<String> response, final BaseChapterBean chapterBean, final BaseChapterBean nextChapterBean, BookShelfBean bookShelfBean, Map<String, String> headerMap) {\n        baseUrl = NetworkUtils.getUrl(response);\n        return analyzeBookContent(response.body(), chapterBean, nextChapterBean, bookShelfBean, headerMap);\n    }\n\n    Observable<BookContentBean> analyzeBookContent(final String s, final BaseChapterBean chapterBean, final BaseChapterBean nextChapterBean, BookShelfBean bookShelfBean, Map<String, String> headerMap) {\n        return Observable.create(e -> {\n            if (TextUtils.isEmpty(s)) {\n                e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_content_error) + chapterBean.getDurChapterUrl()));\n                return;\n            }\n            if (TextUtils.isEmpty(baseUrl)) {\n                baseUrl = NetworkUtils.getAbsoluteURL(bookShelfBean.getBookInfoBean().getChapterUrl(), chapterBean.getDurChapterUrl());\n            }\n            if (StringUtils.isJsonType(s) && !MApplication.getInstance().getDonateHb()) {\n                e.onError(new VipThrowable());\n                e.onComplete();\n                return;\n            }\n            Debug.printLog(tag, \"┌成功获取正文页\");\n            Debug.printLog(tag, \"└\" + baseUrl);\n            BookContentBean bookContentBean = new BookContentBean();\n            bookContentBean.setDurChapterIndex(chapterBean.getDurChapterIndex());\n            bookContentBean.setDurChapterUrl(chapterBean.getDurChapterUrl());\n            bookContentBean.setTag(tag);\n            AnalyzeRule analyzer = new AnalyzeRule(bookShelfBean, bookSourceBean);\n            WebContentBean webContentBean = analyzeBookContent(analyzer, s, chapterBean.getDurChapterUrl(), baseUrl);\n            bookContentBean.setDurChapterContent(webContentBean.content);\n\n            /*\n             * 处理分页\n             */\n            if (!TextUtils.isEmpty(webContentBean.nextUrl)) {\n                List<String> usedUrlList = new ArrayList<>();\n                usedUrlList.add(chapterBean.getDurChapterUrl());\n                BaseChapterBean nextChapter;\n                if (nextChapterBean != null) {\n                    nextChapter = nextChapterBean;\n                } else {\n                    nextChapter = DbHelper.getDaoSession().getBookChapterBeanDao().queryBuilder()\n                            .where(BookChapterBeanDao.Properties.NoteUrl.eq(chapterBean.getNoteUrl()),\n                                    BookChapterBeanDao.Properties.DurChapterIndex.eq(chapterBean.getDurChapterIndex() + 1))\n                            .build().unique();\n                }\n\n                while (!TextUtils.isEmpty(webContentBean.nextUrl) && !usedUrlList.contains(webContentBean.nextUrl)) {\n                    usedUrlList.add(webContentBean.nextUrl);\n                    if (nextChapter != null &&\n                            NetworkUtils.getAbsoluteURL(\n                                    baseUrl, webContentBean.nextUrl\n                            ).equals(NetworkUtils.getAbsoluteURL(baseUrl, nextChapter.getDurChapterUrl()))\n                    ) {\n                        break;\n                    }\n                    AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                            webContentBean.nextUrl, tag, bookSourceBean,\n                            bookSourceBean.getHeaderMap(true)\n                    );\n                    try {\n                        String body;\n                        Response<String> response = BaseModelImpl.getInstance().getResponseO(analyzeUrl).blockingFirst();\n                        body = response.body();\n                        webContentBean = analyzeBookContent(analyzer, body, webContentBean.nextUrl, baseUrl);\n                        if (!TextUtils.isEmpty(webContentBean.content)) {\n                            bookContentBean.setDurChapterContent(bookContentBean.getDurChapterContent() + \"\\n\" + webContentBean.content);\n                        }\n                    } catch (Exception exception) {\n                        if (!e.isDisposed()) {\n                            e.onError(exception);\n                        }\n                    }\n                }\n            }\n            String replaceRule = bookSourceBean.getRuleBookContentReplace();\n            if (replaceRule != null && replaceRule.trim().length() > 0) {\n                analyzer.setContent(bookContentBean.getDurChapterContent());\n                bookContentBean.setDurChapterContent(analyzer.getString(replaceRule));\n            }\n            e.onNext(bookContentBean);\n            e.onComplete();\n        });\n    }\n\n    private WebContentBean analyzeBookContent(AnalyzeRule analyzer, final String s, final String chapterUrl, String baseUrl) throws Exception {\n        WebContentBean webContentBean = new WebContentBean();\n\n        analyzer.setContent(s, NetworkUtils.getAbsoluteURL(baseUrl, chapterUrl));\n        Debug.printLog(tag, 1, \"┌解析正文内容\");\n        if (ruleBookContent.equals(\"all\") || ruleBookContent.contains(\"@all\")) {\n            webContentBean.content = analyzer.getString(ruleBookContent);\n        }\n        else {\n            webContentBean.content = StringUtils.formatHtml(analyzer.getString(ruleBookContent));\n        }\n        Debug.printLog(tag, 1, \"└\" + webContentBean.content);\n        String nextUrlRule = bookSourceBean.getRuleContentUrlNext();\n        if (!TextUtils.isEmpty(nextUrlRule)) {\n            Debug.printLog(tag, 1, \"┌解析下一页url\");\n            webContentBean.nextUrl = analyzer.getString(nextUrlRule, true);\n            Debug.printLog(tag, 1, \"└\" + webContentBean.nextUrl);\n        }\n\n        return webContentBean;\n    }\n\n    private static class WebContentBean {\n        private String content;\n        private String nextUrl;\n\n        private WebContentBean() {\n\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/content/BookInfo.java",
    "content": "package com.kunfei.bookshelf.model.content;\n\nimport static android.text.TextUtils.isEmpty;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeByRegex;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport io.reactivex.Observable;\n\nclass BookInfo {\n    private String tag;\n    private String sourceName;\n    private BookSourceBean bookSourceBean;\n\n    BookInfo(String tag, String sourceName, BookSourceBean bookSourceBean) {\n        this.tag = tag;\n        this.sourceName = sourceName;\n        this.bookSourceBean = bookSourceBean;\n    }\n\n    Observable<BookShelfBean> analyzeBookInfo(String s, final BookShelfBean bookShelfBean) {\n        return Observable.create(e -> {\n            String baseUrl = bookShelfBean.getNoteUrl();\n\n            if (TextUtils.isEmpty(s)) {\n                e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_book_info_error) + baseUrl));\n                return;\n            } else {\n                Debug.printLog(tag, \"┌成功获取详情页\");\n                Debug.printLog(tag, \"└\" + baseUrl);\n            }\n            bookShelfBean.setTag(tag);\n\n            BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean();\n            bookInfoBean.setNoteUrl(baseUrl);   //id\n            bookInfoBean.setTag(tag);\n            bookInfoBean.setOrigin(sourceName);\n            bookInfoBean.setBookSourceType(bookSourceBean.getBookSourceType()); // 是否为有声读物\n\n            AnalyzeRule analyzer = new AnalyzeRule(bookShelfBean, bookSourceBean);\n            analyzer.setContent(s, baseUrl);\n\n            // 获取详情页预处理规则\n            String ruleInfoInit = bookSourceBean.getRuleBookInfoInit();\n            boolean isRegex = false;\n            if (!isEmpty(ruleInfoInit)) {\n                // 仅使用java正则表达式提取书籍详情\n                if (ruleInfoInit.startsWith(\":\")) {\n                    isRegex = true;\n                    ruleInfoInit = ruleInfoInit.substring(1);\n                    Debug.printLog(tag, \"┌详情信息预处理\");\n                    AnalyzeByRegex.getInfoOfRegex(s, ruleInfoInit.split(\"&&\"), 0, bookShelfBean, analyzer, bookSourceBean, tag);\n                } else {\n                    Object object = analyzer.getElement(ruleInfoInit);\n                    if (object != null) {\n                        analyzer.setContent(object);\n                    }\n                }\n            }\n            if (!isRegex) {\n                Debug.printLog(tag, \"┌详情信息预处理\");\n                Object object = analyzer.getElement(ruleInfoInit);\n                if (object != null) analyzer.setContent(object);\n                Debug.printLog(tag, \"└详情预处理完成\");\n\n                Debug.printLog(tag, \"┌获取书名\");\n                String bookName = StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookName()));\n                if (!isEmpty(bookName)) bookInfoBean.setName(bookName);\n                Debug.printLog(tag, \"└\" + bookName);\n\n                Debug.printLog(tag, \"┌获取作者\");\n                String bookAuthor = StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookAuthor()));\n                if (!isEmpty(bookAuthor)) bookInfoBean.setAuthor(bookAuthor);\n                Debug.printLog(tag, \"└\" + bookAuthor);\n\n                Debug.printLog(tag, \"┌获取分类\");\n                String bookKind = analyzer.getString(bookSourceBean.getRuleBookKind());\n                Debug.printLog(tag, 111, \"└\" + bookKind);\n\n                Debug.printLog(tag, \"┌获取最新章节\");\n                String bookLastChapter = analyzer.getString(bookSourceBean.getRuleBookLastChapter());\n                if (!isEmpty(bookLastChapter)) bookShelfBean.setLastChapterName(bookLastChapter);\n                Debug.printLog(tag, \"└\" + bookLastChapter);\n\n                Debug.printLog(tag, \"┌获取简介\");\n                String bookIntroduce = analyzer.getString(bookSourceBean.getRuleIntroduce());\n                if (!isEmpty(bookIntroduce)) bookInfoBean.setIntroduce(bookIntroduce);\n                Debug.printLog(tag, 1, \"└\" + bookIntroduce, true, true);\n\n                Debug.printLog(tag, \"┌获取封面\");\n                String bookCoverUrl = analyzer.getString(bookSourceBean.getRuleCoverUrl(), true);\n                if (!isEmpty(bookCoverUrl)) bookInfoBean.setCoverUrl(bookCoverUrl);\n                Debug.printLog(tag, \"└\" + bookCoverUrl);\n\n                Debug.printLog(tag, \"┌获取目录网址\");\n                String bookCatalogUrl = analyzer.getString(bookSourceBean.getRuleChapterUrl(), true);\n                if (isEmpty(bookCatalogUrl)) bookCatalogUrl = baseUrl;\n                bookInfoBean.setChapterUrl(bookCatalogUrl);\n                //如果目录页和详情页相同,暂存页面内容供获取目录用\n                if (bookCatalogUrl.equals(baseUrl)) bookInfoBean.setChapterListHtml(s);\n                Debug.printLog(tag, \"└\" + bookInfoBean.getChapterUrl());\n                bookShelfBean.setBookInfoBean(bookInfoBean);\n                Debug.printLog(tag, \"-详情页解析完成\");\n            }\n            e.onNext(bookShelfBean);\n            e.onComplete();\n        });\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/content/BookList.java",
    "content": "package com.kunfei.bookshelf.model.content;\n\nimport static android.text.TextUtils.isEmpty;\n\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeByRegex;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport org.mozilla.javascript.NativeObject;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport io.reactivex.Observable;\nimport retrofit2.Response;\n\nclass BookList {\n    private final String tag;\n    private final String sourceName;\n    private final BookSourceBean bookSourceBean;\n    private final boolean isFind;\n    //规则\n    private String ruleList;\n    private String ruleName;\n    private String ruleAuthor;\n    private String ruleKind;\n    private String ruleIntroduce;\n    private String ruleLastChapter;\n    private String ruleCoverUrl;\n    private String ruleNoteUrl;\n\n    BookList(String tag, String sourceName, BookSourceBean bookSourceBean, boolean isFind) {\n        this.tag = tag;\n        this.sourceName = sourceName;\n        this.bookSourceBean = bookSourceBean;\n        this.isFind = isFind;\n    }\n\n    Observable<List<SearchBookBean>> analyzeSearchBook(final Response<String> response) {\n        return Observable.create(e -> {\n            String baseUrl;\n            baseUrl = NetworkUtils.getUrl(response);\n            if (TextUtils.isEmpty(response.body())) {\n                e.onError(new Throwable(MApplication.getInstance().getString(R.string.get_web_content_error, baseUrl)));\n                return;\n            } else {\n                Debug.printLog(tag, \"┌成功获取搜索结果\");\n                Debug.printLog(tag, \"└\" + baseUrl);\n            }\n            String body = response.body();\n            List<SearchBookBean> books = new ArrayList<>();\n            AnalyzeRule analyzer = new AnalyzeRule(null, bookSourceBean);\n            analyzer.setContent(body, baseUrl);\n            //如果符合详情页url规则\n            if (!isEmpty(bookSourceBean.getRuleBookUrlPattern())\n                    && baseUrl.matches(bookSourceBean.getRuleBookUrlPattern())) {\n                Debug.printLog(tag, \">搜索结果为详情页\");\n                SearchBookBean item = getItem(analyzer, baseUrl);\n                if (item != null) {\n                    item.setBookInfoHtml(body);\n                    books.add(item);\n                }\n            } else {\n                initRule();\n                List<Object> collections;\n                boolean reverse = false;\n                boolean allInOne = false;\n                if (ruleList.startsWith(\"-\")) {\n                    reverse = true;\n                    ruleList = ruleList.substring(1);\n                }\n                // 仅使用java正则表达式提取书籍列表\n                if (ruleList.startsWith(\":\")) {\n                    ruleList = ruleList.substring(1);\n                    Debug.printLog(tag, \"┌解析搜索列表\");\n                    getBooksOfRegex(body, ruleList.split(\"&&\"), 0, analyzer, books);\n                } else {\n                    if (ruleList.startsWith(\"+\")) {\n                        allInOne = true;\n                        ruleList = ruleList.substring(1);\n                    }\n                    //获取列表\n                    Debug.printLog(tag, \"┌解析搜索列表\");\n                    collections = analyzer.getElements(ruleList);\n                    if (collections.size() == 0 && isEmpty(bookSourceBean.getRuleBookUrlPattern())) {\n                        Debug.printLog(tag, \"└搜索列表为空,当做详情页处理\");\n                        SearchBookBean item = getItem(analyzer, baseUrl);\n                        if (item != null) {\n                            item.setBookInfoHtml(body);\n                            books.add(item);\n                        }\n                    } else {\n                        Debug.printLog(tag, \"└找到 \" + collections.size() + \" 个匹配的结果\");\n                        if (allInOne) {\n                            for (int i = 0; i < collections.size(); i++) {\n                                Object object = collections.get(i);\n                                SearchBookBean item = getItemAllInOne(analyzer, object, baseUrl, i == 0);\n                                if (item != null) {\n                                    //如果网址相同则缓存\n                                    if (baseUrl.equals(item.getNoteUrl())) {\n                                        item.setBookInfoHtml(body);\n                                    }\n                                    books.add(item);\n                                }\n                            }\n                        } else {\n                            for (int i = 0; i < collections.size(); i++) {\n                                Object object = collections.get(i);\n                                analyzer.setContent(object, baseUrl);\n                                SearchBookBean item = getItemInList(analyzer, baseUrl, i == 0);\n                                if (item != null) {\n                                    //如果网址相同则缓存\n                                    if (baseUrl.equals(item.getNoteUrl())) {\n                                        item.setBookInfoHtml(body);\n                                    }\n                                    books.add(item);\n                                }\n                            }\n                        }\n                    }\n                }\n                if (books.size() > 1 && reverse) {\n                    Collections.reverse(books);\n                }\n            }\n            if (books.isEmpty()) {\n                e.onError(new Throwable(MApplication.getInstance().getString(R.string.no_book_name)));\n                return;\n            }\n            Debug.printLog(tag, \"-书籍列表解析结束\");\n            e.onNext(books);\n            e.onComplete();\n        });\n    }\n\n    private void initRule() {\n        if (isFind && !TextUtils.isEmpty(bookSourceBean.getRuleFindList())) {\n            ruleList = bookSourceBean.getRuleFindList();\n            ruleName = bookSourceBean.getRuleFindName();\n            ruleAuthor = bookSourceBean.getRuleFindAuthor();\n            ruleKind = bookSourceBean.getRuleFindKind();\n            ruleIntroduce = bookSourceBean.getRuleFindIntroduce();\n            ruleCoverUrl = bookSourceBean.getRuleFindCoverUrl();\n            ruleLastChapter = bookSourceBean.getRuleFindLastChapter();\n            ruleNoteUrl = bookSourceBean.getRuleFindNoteUrl();\n        } else {\n            ruleList = bookSourceBean.getRuleSearchList();\n            ruleName = bookSourceBean.getRuleSearchName();\n            ruleAuthor = bookSourceBean.getRuleSearchAuthor();\n            ruleKind = bookSourceBean.getRuleSearchKind();\n            ruleIntroduce = bookSourceBean.getRuleSearchIntroduce();\n            ruleCoverUrl = bookSourceBean.getRuleSearchCoverUrl();\n            ruleLastChapter = bookSourceBean.getRuleSearchLastChapter();\n            ruleNoteUrl = bookSourceBean.getRuleSearchNoteUrl();\n        }\n    }\n\n    /**\n     * 详情页\n     */\n    private SearchBookBean getItem(AnalyzeRule analyzer, String baseUrl) throws Exception {\n        SearchBookBean item = new SearchBookBean();\n        analyzer.setBook(item);\n        item.setTag(tag);\n        item.setOrigin(sourceName);\n        item.setNoteUrl(baseUrl);\n        // 获取详情页预处理规则\n        String ruleInfoInit = bookSourceBean.getRuleBookInfoInit();\n        if (!isEmpty(ruleInfoInit)) {\n            // 仅使用java正则表达式提取书籍详情\n            if (ruleInfoInit.startsWith(\":\")) {\n                ruleInfoInit = ruleInfoInit.substring(1);\n                Debug.printLog(tag, \"┌详情信息预处理\");\n                BookShelfBean bookShelfBean = new BookShelfBean();\n                bookShelfBean.setTag(tag);\n                bookShelfBean.setNoteUrl(baseUrl);\n                AnalyzeByRegex.getInfoOfRegex(String.valueOf(analyzer.getContent()), ruleInfoInit.split(\"&&\"), 0, bookShelfBean, analyzer, bookSourceBean, tag);\n                if (isEmpty(bookShelfBean.getBookInfoBean().getName())) return null;\n                item.setName(bookShelfBean.getBookInfoBean().getName());\n                item.setAuthor(bookShelfBean.getBookInfoBean().getAuthor());\n                item.setCoverUrl(bookShelfBean.getBookInfoBean().getCoverUrl());\n                item.setLastChapter(bookShelfBean.getLastChapterName());\n                item.setIntroduce(bookShelfBean.getBookInfoBean().getIntroduce());\n                return item;\n            } else {\n                Object object = analyzer.getElement(ruleInfoInit);\n                if (object != null) {\n                    analyzer.setContent(object);\n                }\n            }\n        }\n        Debug.printLog(tag, \">书籍网址:\" + baseUrl);\n        Debug.printLog(tag, \"┌获取书名\");\n        String bookName = StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookName()));\n        Debug.printLog(tag, \"└\" + bookName);\n        if (!TextUtils.isEmpty(bookName)) {\n            item.setName(bookName);\n            Debug.printLog(tag, \"┌获取作者\");\n            item.setAuthor(StringUtils.formatHtml(analyzer.getString(bookSourceBean.getRuleBookAuthor())));\n            Debug.printLog(tag, \"└\" + item.getAuthor());\n            Debug.printLog(tag, \"┌获取分类\");\n            item.setKind(analyzer.getString(bookSourceBean.getRuleBookKind()));\n            Debug.printLog(tag, 111, \"└\" + item.getKind());\n            Debug.printLog(tag, \"┌获取最新章节\");\n            item.setLastChapter(analyzer.getString(bookSourceBean.getRuleBookLastChapter()));\n            Debug.printLog(tag, \"└\" + item.getLastChapter());\n            Debug.printLog(tag, \"┌获取简介\");\n            item.setIntroduce(analyzer.getString(bookSourceBean.getRuleIntroduce()));\n            Debug.printLog(tag, 1, \"└\" + item.getIntroduce(), true, true);\n            Debug.printLog(tag, \"┌获取封面\");\n            item.setCoverUrl(analyzer.getString(bookSourceBean.getRuleCoverUrl(), true));\n            Debug.printLog(tag, \"└\" + item.getCoverUrl());\n            return item;\n        }\n        return null;\n    }\n\n    private SearchBookBean getItemAllInOne(AnalyzeRule analyzer, Object object, String baseUrl, boolean printLog) {\n        SearchBookBean item = new SearchBookBean();\n        analyzer.setBook(item);\n        NativeObject nativeObject = (NativeObject) object;\n        Debug.printLog(tag, 1, \"┌获取书名\", printLog);\n        String bookName = StringUtils.formatHtml(String.valueOf(nativeObject.get(ruleName)));\n        Debug.printLog(tag, 1, \"└\" + bookName, printLog);\n        if (!isEmpty(bookName)) {\n            item.setTag(tag);\n            item.setOrigin(sourceName);\n            item.setName(bookName);\n            Debug.printLog(tag, 1, \"┌获取作者\", printLog);\n            item.setAuthor(StringUtils.formatHtml(String.valueOf(nativeObject.get(ruleAuthor))));\n            Debug.printLog(tag, 1, \"└\" + item.getAuthor(), printLog);\n            Debug.printLog(tag, 1, \"┌获取分类\", printLog);\n            item.setKind(String.valueOf(nativeObject.get(ruleKind)));\n            Debug.printLog(tag, 111, \"└\" + item.getKind(), printLog);\n            Debug.printLog(tag, 1, \"┌获取最新章节\", printLog);\n            item.setLastChapter(String.valueOf(nativeObject.get(ruleLastChapter)));\n            Debug.printLog(tag, 1, \"└\" + item.getLastChapter(), printLog);\n            Debug.printLog(tag, 1, \"┌获取简介\", printLog);\n            item.setIntroduce(String.valueOf(nativeObject.get(ruleIntroduce)));\n            Debug.printLog(tag, 1, \"└\" + item.getIntroduce(), printLog, true);\n            Debug.printLog(tag, 1, \"┌获取封面\", printLog);\n            if (!isEmpty(ruleCoverUrl))\n                item.setCoverUrl(NetworkUtils.getAbsoluteURL(baseUrl, String.valueOf(nativeObject.get(ruleCoverUrl))));\n            Debug.printLog(tag, 1, \"└\" + item.getCoverUrl(), printLog);\n            Debug.printLog(tag, 1, \"┌获取书籍网址\", printLog);\n            String resultUrl = String.valueOf(nativeObject.get(ruleNoteUrl));\n            if (isEmpty(resultUrl)) resultUrl = baseUrl;\n            item.setNoteUrl(resultUrl);\n            Debug.printLog(tag, 1, \"└\" + item.getNoteUrl(), printLog);\n            return item;\n        }\n        return null;\n    }\n\n    private SearchBookBean getItemInList(AnalyzeRule analyzer, String baseUrl, boolean printLog) throws\n            Exception {\n        SearchBookBean item = new SearchBookBean();\n        analyzer.setBook(item);\n        Debug.printLog(tag, 1, \"┌获取书名\", printLog);\n        String bookName = StringUtils.formatHtml(analyzer.getString(ruleName));\n        Debug.printLog(tag, 1, \"└\" + bookName, printLog);\n        if (!TextUtils.isEmpty(bookName)) {\n            item.setTag(tag);\n            item.setOrigin(sourceName);\n            item.setName(bookName);\n            Debug.printLog(tag, 1, \"┌获取作者\", printLog);\n            item.setAuthor(StringUtils.formatHtml(analyzer.getString(ruleAuthor)));\n            Debug.printLog(tag, 1, \"└\" + item.getAuthor(), printLog);\n            Debug.printLog(tag, 1, \"┌获取分类\", printLog);\n            item.setKind(analyzer.getString(ruleKind));\n            Debug.printLog(tag, 111, \"└\" + item.getKind(), printLog);\n            Debug.printLog(tag, 1, \"┌获取最新章节\", printLog);\n            item.setLastChapter(analyzer.getString(ruleLastChapter));\n            Debug.printLog(tag, 1, \"└\" + item.getLastChapter(), printLog);\n            Debug.printLog(tag, 1, \"┌获取简介\", printLog);\n            item.setIntroduce(analyzer.getString(ruleIntroduce));\n            Debug.printLog(tag, 1, \"└\" + item.getIntroduce(), printLog, true);\n            Debug.printLog(tag, 1, \"┌获取封面\", printLog);\n            item.setCoverUrl(analyzer.getString(ruleCoverUrl, true));\n            Debug.printLog(tag, 1, \"└\" + item.getCoverUrl(), printLog);\n            Debug.printLog(tag, 1, \"┌获取书籍网址\", printLog);\n            String resultUrl = analyzer.getString(ruleNoteUrl, true);\n            if (isEmpty(resultUrl)) resultUrl = baseUrl;\n            item.setNoteUrl(resultUrl);\n            Debug.printLog(tag, 1, \"└\" + item.getNoteUrl(), printLog);\n            return item;\n        }\n        return null;\n    }\n\n    // 纯java模式正则表达式获取书籍列表\n    private void getBooksOfRegex(String res, String[] regs,\n                                 int index, AnalyzeRule analyzer, final List<SearchBookBean> books) throws Exception {\n        Matcher resM = Pattern.compile(regs[index]).matcher(res);\n        String baseUrl = analyzer.getBaseUrl();\n        // 判断规则是否有效,当搜索列表规则无效时当作详情页处理\n        if (!resM.find()) {\n            books.add(getItem(analyzer, baseUrl));\n            return;\n        }\n        // 判断索引的规则是最后一个规则\n        if (index + 1 == regs.length) {\n            // 获取规则列表\n            HashMap<String, String> ruleMap = new HashMap<>();\n            ruleMap.put(\"ruleName\", ruleName);\n            ruleMap.put(\"ruleAuthor\", ruleAuthor);\n            ruleMap.put(\"ruleKind\", ruleKind);\n            ruleMap.put(\"ruleLastChapter\", ruleLastChapter);\n            ruleMap.put(\"ruleIntroduce\", ruleIntroduce);\n            ruleMap.put(\"ruleCoverUrl\", ruleCoverUrl);\n            ruleMap.put(\"ruleNoteUrl\", ruleNoteUrl);\n            // 分离规则参数\n            List<String> ruleName = new ArrayList<>();\n            List<List<String>> ruleParams = new ArrayList<>();  // 创建规则参数容器\n            List<List<Integer>> ruleTypes = new ArrayList<>();  // 创建规则类型容器\n            List<Boolean> hasVarParams = new ArrayList<>();     // 创建put&get标志容器\n            for (String key : ruleMap.keySet()) {\n                String val = ruleMap.get(key);\n                ruleName.add(key);\n                hasVarParams.add(!TextUtils.isEmpty(val) && (val.contains(\"@put\") || val.contains(\"@get\")));\n                List<String> ruleParam = new ArrayList<>();\n                List<Integer> ruleType = new ArrayList<>();\n                AnalyzeByRegex.splitRegexRule(val, ruleParam, ruleType);\n                ruleParams.add(ruleParam);\n                ruleTypes.add(ruleType);\n            }\n            // 提取书籍列表\n            do {\n                // 新建书籍容器\n                SearchBookBean item = new SearchBookBean(tag, sourceName);\n                analyzer.setBook(item);\n                // 提取规则内容\n                HashMap<String, String> ruleVal = new HashMap<>();\n                StringBuilder infoVal = new StringBuilder();\n                for (int i = ruleParams.size(); i-- > 0; ) {\n                    List<String> ruleParam = ruleParams.get(i);\n                    List<Integer> ruleType = ruleTypes.get(i);\n                    infoVal.setLength(0);\n                    for (int j = ruleParam.size(); j-- > 0; ) {\n                        int regType = ruleType.get(j);\n                        if (regType > 0) {\n                            infoVal.insert(0, resM.group(regType));\n                        } else if (regType < 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                            infoVal.insert(0, resM.group(ruleParam.get(j)));\n                        } else {\n                            infoVal.insert(0, ruleParam.get(j));\n                        }\n                    }\n                    ruleVal.put(ruleName.get(i), hasVarParams.get(i) ? AnalyzeByRegex.checkKeys(infoVal.toString(), analyzer) : infoVal.toString());\n                }\n                // 保存当前节点的书籍信息\n                item.setSearchInfo(\n                        StringUtils.formatHtml(ruleVal.get(\"ruleName\")),        // 保存书名\n                        StringUtils.formatHtml(ruleVal.get(\"ruleAuthor\")),      // 保存作者\n                        ruleVal.get(\"ruleKind\"),        // 保存分类\n                        ruleVal.get(\"ruleLastChapter\"), // 保存终章\n                        ruleVal.get(\"ruleIntroduce\"),   // 保存简介\n                        ruleVal.get(\"ruleCoverUrl\"),    // 保存封面\n                        NetworkUtils.getAbsoluteURL(baseUrl, ruleVal.get(\"ruleNoteUrl\"))       // 保存详情\n                );\n                books.add(item);\n                // 判断搜索结果是否为详情页\n                if (books.size() == 1 && (isEmpty(ruleVal.get(\"ruleNoteUrl\")) || ruleVal.get(\"ruleNoteUrl\").equals(baseUrl))) {\n                    books.get(0).setNoteUrl(baseUrl);\n                    books.get(0).setBookInfoHtml(res);\n                    return;\n                }\n            } while (resM.find());\n            // 输出调试信息\n            Debug.printLog(tag, \"└找到 \" + books.size() + \" 个匹配的结果\");\n            Debug.printLog(tag, \"┌获取书名\");\n            Debug.printLog(tag, \"└\" + books.get(0).getName());\n            Debug.printLog(tag, \"┌获取作者\");\n            Debug.printLog(tag, \"└\" + books.get(0).getAuthor());\n            Debug.printLog(tag, \"┌获取分类\");\n            Debug.printLog(tag, 111, \"└\" + books.get(0).getKind());\n            Debug.printLog(tag, \"┌获取最新章节\");\n            Debug.printLog(tag, \"└\" + books.get(0).getLastChapter());\n            Debug.printLog(tag, \"┌获取简介\");\n            Debug.printLog(tag, 1, \"└\" + books.get(0).getIntroduce(), true, true);\n            Debug.printLog(tag, \"┌获取封面\");\n            Debug.printLog(tag, \"└\" + books.get(0).getCoverUrl());\n            Debug.printLog(tag, \"┌获取书籍\");\n            Debug.printLog(tag, \"└\" + books.get(0).getNoteUrl());\n        } else {\n            StringBuilder result = new StringBuilder();\n            do {\n                result.append(resM.group());\n            } while (resM.find());\n            getBooksOfRegex(result.toString(), regs, ++index, analyzer, books);\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/content/Debug.java",
    "content": "package com.kunfei.bookshelf.model.content;\n\nimport android.annotation.SuppressLint;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.UpLastChapterModel;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.TimeUtils;\n\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Objects;\n\nimport io.reactivex.Observer;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\n\npublic class Debug {\n    public static String SOURCE_DEBUG_TAG;\n    @SuppressLint(\"ConstantLocale\")\n    private static final DateFormat DEBUG_TIME_FORMAT = new SimpleDateFormat(\"[mm:ss.SSS]\", Locale.getDefault());\n    private static long startTime;\n\n    private static String getDoTime() {\n        return TimeUtils.millis2String(System.currentTimeMillis() - startTime, DEBUG_TIME_FORMAT);\n    }\n\n    public static void printLog(String tag, String msg) {\n        printLog(tag, 1, msg, true);\n    }\n\n    static void printLog(String tag, int state, String msg) {\n        printLog(tag, state, msg, true);\n    }\n\n    static void printLog(String tag, int state, String msg, boolean print) {\n        printLog(tag, state, msg, print, false);\n    }\n\n    public static void printLog(String tag, int state, String msg, boolean print, boolean formatHtml) {\n        if (print && Objects.equals(SOURCE_DEBUG_TAG, tag)) {\n            if (formatHtml) {\n                msg = StringUtils.formatHtml(msg);\n            }\n            if (state == 111) {\n                msg = msg.replace(\"\\n\", \",\");\n            }\n            msg = String.format(\"%s %s\", getDoTime(), msg);\n            RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, msg);\n        }\n    }\n\n    public static void newDebug(String tag, String key, @NonNull CompositeDisposable compositeDisposable) {\n        new Debug(tag, key, compositeDisposable);\n    }\n\n    private final CompositeDisposable compositeDisposable;\n\n    private Debug(String tag, String key, CompositeDisposable compositeDisposable) {\n        UpLastChapterModel.destroy();\n        startTime = System.currentTimeMillis();\n        SOURCE_DEBUG_TAG = tag;\n        this.compositeDisposable = compositeDisposable;\n        if (NetworkUtils.isUrl(key)) {\n            printLog(String.format(\"%s %s\", getDoTime(), \"⇒开始访问详情页:\" + key));\n            BookShelfBean bookShelfBean = new BookShelfBean();\n            bookShelfBean.setTag(Debug.SOURCE_DEBUG_TAG);\n            bookShelfBean.setNoteUrl(key);\n            bookShelfBean.setDurChapter(0);\n            bookShelfBean.setGroup(0);\n            bookShelfBean.setDurChapterPage(0);\n            bookShelfBean.setFinalDate(System.currentTimeMillis());\n            bookInfoDebug(bookShelfBean);\n        } else if (key.contains(\"::\")) {\n            String url = key.substring(key.indexOf(\"::\") + 2);\n            printLog(String.format(\"%s %s\", getDoTime(), \"⇒开始访问发现页:\" + url));\n            findDebug(url);\n        } else {\n            printLog(String.format(\"%s %s\", getDoTime(), \"⇒开始搜索关键字:\" + key));\n            searchDebug(key);\n        }\n    }\n\n    private void findDebug(String url) {\n        printLog(String.format(\"\\n%s ≡开始获取发现页\", getDoTime()));\n        WebBookModel.getInstance().findBook(url, 1, Debug.SOURCE_DEBUG_TAG)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<List<SearchBookBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @SuppressLint(\"DefaultLocale\")\n                    @Override\n                    public void onNext(List<SearchBookBean> searchBookBeans) {\n                        SearchBookBean searchBookBean = searchBookBeans.get(0);\n                        if (!TextUtils.isEmpty(searchBookBean.getNoteUrl())) {\n                            bookInfoDebug(BookshelfHelp.getBookFromSearchBook(searchBookBean));\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        printError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n\n    private void searchDebug(String key) {\n        printLog(String.format(\"\\n%s ≡开始获取搜索页\", getDoTime()));\n        WebBookModel.getInstance().searchBook(key, 1, Debug.SOURCE_DEBUG_TAG)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<List<SearchBookBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @SuppressLint(\"DefaultLocale\")\n                    @Override\n                    public void onNext(List<SearchBookBean> searchBookBeans) {\n                        SearchBookBean searchBookBean = searchBookBeans.get(0);\n                        if (!TextUtils.isEmpty(searchBookBean.getNoteUrl())) {\n                            bookInfoDebug(BookshelfHelp.getBookFromSearchBook(searchBookBean));\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        printError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n\n    private void bookInfoDebug(BookShelfBean bookShelfBean) {\n        printLog(String.format(\"\\n%s ≡开始获取详情页\", getDoTime()));\n        WebBookModel.getInstance().getBookInfo(bookShelfBean)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<BookShelfBean>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(BookShelfBean bookShelfBean) {\n                        bookChapterListDebug(bookShelfBean);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        printError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n\n    private void bookChapterListDebug(BookShelfBean bookShelfBean) {\n        printLog(String.format(\"\\n%s ≡开始获取目录页\", getDoTime()));\n        WebBookModel.getInstance().getChapterList(bookShelfBean)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<List<BookChapterBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @SuppressLint(\"DefaultLocale\")\n                    @Override\n                    public void onNext(List<BookChapterBean> chapterBeanList) {\n                        if (chapterBeanList.size() > 0) {\n                            BookChapterBean nextChapter = chapterBeanList.size() > 2 ? chapterBeanList.get(1) : null;\n                            bookContentDebug(bookShelfBean, chapterBeanList.get(0), nextChapter);\n                        } else {\n                            printError(\"获取到的目录为空\");\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        printError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n\n    private void bookContentDebug(BookShelfBean bookShelfBean, BookChapterBean bookChapterBean, BookChapterBean nextChapterBean) {\n        printLog(String.format(\"\\n%s ≡开始获取正文页\", getDoTime()));\n        WebBookModel.getInstance().getBookContent(bookShelfBean, bookChapterBean, nextChapterBean)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<BookContentBean>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(BookContentBean bookContentBean) {\n\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        printError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n                        finish();\n                    }\n                });\n    }\n\n    private void printLog(String log) {\n        RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, log);\n    }\n\n    private void printError(String msg) {\n        RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, msg);\n        finish();\n    }\n\n    private void finish() {\n        RxBus.get().post(RxBusTag.PRINT_DEBUG_LOG, \"finish\");\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/content/VipThrowable.java",
    "content": "package com.kunfei.bookshelf.model.content;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\n\npublic class VipThrowable extends Throwable {\n\n    private final static String tag = \"VIP_THROWABLE\";\n\n    VipThrowable() {\n        super(MApplication.getInstance().getString(R.string.donate_s));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/content/WebBook.java",
    "content": "package com.kunfei.bookshelf.model.content;\n\nimport static android.text.TextUtils.isEmpty;\nimport static com.kunfei.bookshelf.constant.AppConstant.JS_PATTERN;\nimport static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.bean.BaseChapterBean;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.help.JsExtensions;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.regex.Matcher;\n\nimport javax.script.SimpleBindings;\n\nimport io.reactivex.Observable;\nimport retrofit2.Response;\n\n/**\n * 默认检索规则\n */\npublic class WebBook extends BaseModelImpl implements JsExtensions {\n    private final String tag;\n    private String name;\n    private final BookSourceBean bookSourceBean;\n    private Map<String, String> headerMap;\n\n    public static WebBook getInstance(String tag) {\n        return new WebBook(tag);\n    }\n\n    private WebBook(String tag) {\n        this.tag = tag;\n        try {\n            URL url = new URL(tag);\n            name = url.getHost();\n        } catch (MalformedURLException e) {\n            name = tag;\n        }\n        bookSourceBean = BookSourceManager.getBookSourceByUrl(tag);\n        if (bookSourceBean != null) {\n            name = bookSourceBean.getBookSourceName();\n            headerMap = bookSourceBean.getHeaderMap(true);\n        }\n    }\n\n    /**\n     * 发现\n     */\n    public Observable<List<SearchBookBean>> findBook(String url, int page) {\n        if (bookSourceBean == null) {\n            return Observable.error(new NoSourceThrowable(tag));\n        }\n        BookList bookList = new BookList(tag, name, bookSourceBean, true);\n        try {\n            AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                    url, tag, bookSourceBean, null, page,\n                    bookSourceBean.getHeaderMap(true)\n            );\n            return getResponseO(analyzeUrl)\n                    .flatMap(response -> checkLogin(response, url, tag))\n                    .flatMap(bookList::analyzeSearchBook);\n        } catch (Exception e) {\n            return Observable.error(new Throwable(String.format(\"%s错误:%s\", url, e.getLocalizedMessage())));\n        }\n    }\n\n    /**\n     * 搜索\n     */\n    public Observable<List<SearchBookBean>> searchBook(String content, int page) {\n        if (bookSourceBean == null || isEmpty(bookSourceBean.getRuleSearchUrl())) {\n            return Observable.create(emitter -> {\n                emitter.onNext(new ArrayList<>());\n                emitter.onComplete();\n            });\n        }\n        BookList bookList = new BookList(tag, name, bookSourceBean, false);\n        try {\n            AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                    bookSourceBean.getRuleSearchUrl(),\n                    tag, bookSourceBean, content, page,\n                    bookSourceBean.getHeaderMap(true)\n            );\n            return getResponseO(analyzeUrl)\n                    .flatMap(response -> checkLogin(response, bookSourceBean.getRuleSearchUrl(), tag))\n                    .flatMap(bookList::analyzeSearchBook);\n        } catch (Exception e) {\n            return Observable.error(e);\n        }\n    }\n\n    /**\n     * 获取书籍信息\n     */\n    public Observable<BookShelfBean> getBookInfo(final BookShelfBean bookShelfBean) {\n        if (bookSourceBean == null) {\n            return Observable.error(new NoSourceThrowable(tag));\n        }\n        BookInfo bookInfo = new BookInfo(tag, name, bookSourceBean);\n        if (!TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getBookInfoHtml())) {\n            return bookInfo.analyzeBookInfo(bookShelfBean.getBookInfoBean().getBookInfoHtml(), bookShelfBean);\n        }\n        try {\n            AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                    bookShelfBean.getNoteUrl(), tag, bookSourceBean,\n                    bookSourceBean.getHeaderMap(true)\n            );\n            return getResponseO(analyzeUrl)\n                    .flatMap(response -> setCookie(response, tag))\n                    .flatMap(response -> checkLogin(response, bookShelfBean.getNoteUrl(), tag))\n                    .flatMap(response -> bookInfo.analyzeBookInfo(response.body(), bookShelfBean));\n        } catch (Exception e) {\n            return Observable.error(new Throwable(String.format(\"url错误:%s\", bookShelfBean.getNoteUrl())));\n        }\n    }\n\n    /**\n     * 获取目录\n     */\n    public Observable<List<BookChapterBean>> getChapterList(final BookShelfBean bookShelfBean) {\n        if (bookSourceBean == null) {\n            return Observable.error(new NoSourceThrowable(bookShelfBean.getBookInfoBean().getName()));\n        }\n        BookChapterList bookChapterList = new BookChapterList(tag, bookSourceBean, true);\n        if (!TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getChapterListHtml())) {\n            return bookChapterList.analyzeChapterList(bookShelfBean.getBookInfoBean().getChapterListHtml(), bookShelfBean, headerMap);\n        }\n        try {\n            AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                    bookShelfBean.getBookInfoBean().getChapterUrl(),\n                    bookShelfBean.getNoteUrl(), bookSourceBean,\n                    bookSourceBean.getHeaderMap(true)\n            );\n            return getResponseO(analyzeUrl)\n                    .flatMap(response -> setCookie(response, tag))\n                    .flatMap(stringResponse -> checkLogin(stringResponse, bookShelfBean.getBookInfoBean().getChapterUrl(), bookShelfBean.getNoteUrl()))\n                    .flatMap(response -> bookChapterList.analyzeChapterList(response.body(), bookShelfBean, headerMap));\n        } catch (Exception e) {\n            return Observable.error(new Throwable(String.format(\"url错误:%s\", bookShelfBean.getBookInfoBean().getChapterUrl())));\n        }\n    }\n\n    /**\n     * 获取正文\n     */\n    public Observable<BookContentBean> getBookContent(final BaseChapterBean chapterBean, final BaseChapterBean nextChapterBean, final BookShelfBean bookShelfBean) {\n        if (bookSourceBean == null) {\n            return Observable.error(new NoSourceThrowable(chapterBean.getTag()));\n        }\n        if (isEmpty(bookSourceBean.getRuleBookContent())) {\n            return Observable.create(emitter -> {\n                BookContentBean bookContentBean = new BookContentBean();\n                bookContentBean.setDurChapterContent(chapterBean.getDurChapterUrl());\n                bookContentBean.setDurChapterIndex(chapterBean.getDurChapterIndex());\n                bookContentBean.setTag(bookShelfBean.getTag());\n                bookContentBean.setDurChapterUrl(chapterBean.getDurChapterUrl());\n                emitter.onNext(bookContentBean);\n                emitter.onComplete();\n            });\n        }\n        BookContent bookContent = new BookContent(tag, bookSourceBean);\n        if (Objects.equals(chapterBean.getDurChapterUrl(), bookShelfBean.getBookInfoBean().getChapterUrl())\n                && !TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getChapterListHtml())) {\n            return bookContent.analyzeBookContent(bookShelfBean.getBookInfoBean().getChapterListHtml(), chapterBean, nextChapterBean, bookShelfBean, headerMap);\n        }\n        try {\n            AnalyzeUrl analyzeUrl = new AnalyzeUrl(\n                    chapterBean.getDurChapterUrl(),\n                    bookShelfBean.getBookInfoBean().getChapterUrl(),\n                    bookSourceBean,\n                    bookSourceBean.getHeaderMap(true));\n            String contentRule = bookSourceBean.getRuleBookContent();\n            if (contentRule.startsWith(\"$\") && !contentRule.startsWith(\"$.\")) {\n                //动态网页第一个js放到webView里执行\n                contentRule = contentRule.substring(1);\n                String js = null;\n                Matcher jsMatcher = JS_PATTERN.matcher(contentRule);\n                if (jsMatcher.find()) {\n                    js = jsMatcher.group();\n                    if (js.startsWith(\"<js>\")) {\n                        js = js.substring(4, js.lastIndexOf(\"<\"));\n                    } else {\n                        js = js.substring(4);\n                    }\n                }\n                return getAjaxString(analyzeUrl, tag, js)\n                        .flatMap(response -> bookContent.analyzeBookContent(response, chapterBean, nextChapterBean, bookShelfBean, headerMap));\n            } else {\n                return getResponseO(analyzeUrl)\n                        .flatMap(response -> setCookie(response, tag))\n                        .flatMap(stringResponse -> checkLogin(stringResponse, chapterBean.getDurChapterUrl(), bookShelfBean.getBookInfoBean().getChapterUrl()))\n                        .flatMap(response -> bookContent.analyzeBookContent(response, chapterBean, nextChapterBean, bookShelfBean, headerMap));\n            }\n        } catch (Exception e) {\n            return Observable.error(new Throwable(String.format(\"url错误:%s\", e.getLocalizedMessage())));\n        }\n    }\n\n    Observable<Response<String>> checkLogin(final Response<String> stringResponse, String url, String baseUrl) {\n        return Observable.create(emitter -> {\n            String checkJs = bookSourceBean.getLoginCheckJs();\n            if (!TextUtils.isEmpty(checkJs)) {\n                SimpleBindings bindings = new SimpleBindings();\n                bindings.put(\"source\", bookSourceBean);\n                bindings.put(\"url\", url);\n                bindings.put(\"java\", this);\n                bindings.put(\"result\", stringResponse);\n                bindings.put(\"baseUrl\", baseUrl);\n                @SuppressWarnings(\"unchecked\")\n                Response<String> res = (Response<String>) SCRIPT_ENGINE.eval(checkJs, bindings);\n                emitter.onNext(res);\n                return;\n            }\n            emitter.onNext(stringResponse);\n        });\n    }\n\n    public class NoSourceThrowable extends Throwable {\n        NoSourceThrowable(String tag) {\n            super(String.format(\"%s没有找到书源配置\", tag));\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/impl/IDownloadTask.java",
    "content": "package com.kunfei.bookshelf.model.impl;\n\nimport com.kunfei.bookshelf.bean.DownloadBookBean;\nimport com.kunfei.bookshelf.bean.DownloadChapterBean;\n\nimport io.reactivex.Scheduler;\n\npublic interface IDownloadTask {\n\n    int getId();\n\n    void startDownload(Scheduler scheduler);\n\n    void stopDownload();\n\n    boolean isDownloading();\n\n    boolean isFinishing();\n\n    DownloadBookBean getDownloadBook();\n\n    void onDownloadPrepared(DownloadBookBean downloadBook);\n\n    void onDownloadProgress(DownloadChapterBean chapterBean);\n\n    void onDownloadChange(DownloadBookBean downloadBook);\n\n    void onDownloadError(DownloadBookBean downloadBook);\n\n    void onDownloadComplete(DownloadBookBean downloadBook);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/impl/IHttpGetApi.java",
    "content": "package com.kunfei.bookshelf.model.impl;\n\nimport java.util.Map;\n\nimport io.reactivex.Observable;\nimport retrofit2.Response;\nimport retrofit2.http.GET;\nimport retrofit2.http.HeaderMap;\nimport retrofit2.http.QueryMap;\nimport retrofit2.http.Url;\n\n/**\n * Created by GKF on 2018/1/21.\n * get web content\n */\n\npublic interface IHttpGetApi {\n    @GET\n    Observable<Response<String>> get(@Url String url,\n                                     @HeaderMap Map<String, String> headers);\n\n    @GET\n    Observable<Response<String>> getMap(@Url String url,\n                                        @QueryMap(encoded = true) Map<String, String> queryMap,\n                                        @HeaderMap Map<String, String> headers);\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/impl/IHttpPostApi.java",
    "content": "package com.kunfei.bookshelf.model.impl;\n\nimport java.util.Map;\n\nimport io.reactivex.Observable;\nimport okhttp3.RequestBody;\nimport retrofit2.Response;\nimport retrofit2.http.Body;\nimport retrofit2.http.FieldMap;\nimport retrofit2.http.FormUrlEncoded;\nimport retrofit2.http.HeaderMap;\nimport retrofit2.http.POST;\nimport retrofit2.http.Url;\n\n/**\n * Created by GKF on 2018/1/29.\n * post\n */\n\npublic interface IHttpPostApi {\n\n    @FormUrlEncoded\n    @POST\n    Observable<Response<String>> postMap(@Url String url,\n                                         @FieldMap(encoded = true) Map<String, String> fieldMap,\n                                         @HeaderMap Map<String, String> headers);\n\n    @POST\n    Observable<Response<String>> postJson(@Url String url,\n                                          @Body RequestBody body,\n                                          @HeaderMap Map<String, String> headers);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/task/AnalyzeNextUrlTask.java",
    "content": "package com.kunfei.bookshelf.model.task;\n\nimport com.kunfei.bookshelf.base.BaseModelImpl;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.WebChapterBean;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl;\nimport com.kunfei.bookshelf.model.content.BookChapterList;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class AnalyzeNextUrlTask {\n    private WebChapterBean webChapterBean;\n    private Callback callback;\n    private BookShelfBean bookShelfBean;\n    private Map<String, String> headerMap;\n    private BookChapterList bookChapterList;\n\n    public AnalyzeNextUrlTask(BookChapterList bookChapterList, WebChapterBean webChapterBean, BookShelfBean bookShelfBean, Map<String, String> headerMap) {\n        this.bookChapterList = bookChapterList;\n        this.webChapterBean = webChapterBean;\n        this.bookShelfBean = bookShelfBean;\n        this.headerMap = headerMap;\n    }\n\n    public AnalyzeNextUrlTask setCallback(Callback callback) {\n        this.callback = callback;\n        return this;\n    }\n\n    public void analyzeUrl(AnalyzeUrl analyzeUrl) {\n        BaseModelImpl.getInstance().getResponseO(analyzeUrl)\n                .flatMap(stringResponse ->\n                        bookChapterList.analyzeChapterList(stringResponse.body(), bookShelfBean, headerMap))\n                .subscribeOn(Schedulers.io())\n                .observeOn(Schedulers.io())\n                .subscribe(new MyObserver<List<BookChapterBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        callback.addDisposable(d);\n                    }\n\n                    @Override\n                    public void onNext(List<BookChapterBean> bookChapterBeans) {\n                        callback.analyzeFinish(webChapterBean, bookChapterBeans);\n                    }\n\n                    @Override\n                    public void onError(Throwable throwable) {\n                        callback.onError(throwable);\n                    }\n                });\n    }\n\n    public interface Callback {\n        void addDisposable(Disposable disposable);\n\n        void analyzeFinish(WebChapterBean bean, List<BookChapterBean> bookChapterBeans);\n\n        void onError(Throwable throwable);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/task/CheckSourceTask.java",
    "content": "package com.kunfei.bookshelf.model.task;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule;\nimport com.kunfei.bookshelf.service.CheckSourceService;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\n\nimport javax.script.SimpleBindings;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.Observer;\nimport io.reactivex.Scheduler;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.Disposable;\n\npublic class CheckSourceTask {\n\n    private BookSourceBean sourceBean;\n    private Scheduler scheduler;\n    private CheckSourceService.CheckSourceListener checkSourceListener;\n\n    public CheckSourceTask(BookSourceBean sourceBean, Scheduler scheduler, CheckSourceService.CheckSourceListener checkSourceListener) {\n        this.sourceBean = sourceBean;\n        this.scheduler = scheduler;\n        this.checkSourceListener = checkSourceListener;\n    }\n\n    public void startCheck() {\n        if (!TextUtils.isEmpty(sourceBean.getRuleSearchUrl())) {\n            WebBookModel.getInstance().searchBook(\"我的\", 1, sourceBean.getBookSourceUrl())\n                    .subscribeOn(scheduler)\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .timeout(60, TimeUnit.SECONDS)\n                    .subscribe(new Observer<List<SearchBookBean>>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            checkSourceListener.compositeDisposableAdd(d);\n                        }\n\n                        @Override\n                        public void onNext(List<SearchBookBean> searchBookBeans) {\n                            if (searchBookBeans.isEmpty()) {\n                                checkFind();\n                            } else {\n                                sourceUnInvalid();\n                            }\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            checkFind();\n                        }\n\n                        @Override\n                        public void onComplete() {\n\n                        }\n                    });\n        } else {\n            checkFind();\n        }\n    }\n\n    private void checkFind() {\n        if (!TextUtils.isEmpty(sourceBean.getRuleFindUrl())) {\n            Observable.create((ObservableOnSubscribe<String>) emitter -> {\n                String[] kindA;\n                if (!TextUtils.isEmpty(sourceBean.getRuleFindUrl())) {\n                    if (sourceBean.getRuleFindUrl().startsWith(\"<js>\")) {\n                        String jsStr = sourceBean.getRuleFindUrl().substring(4, sourceBean.getRuleFindUrl().lastIndexOf(\"<\"));\n                        Object object = evalJS(jsStr, sourceBean.getBookSourceUrl(), sourceBean);\n                        kindA = object.toString().split(\"(&&|\\n)+\");\n                    } else {\n                        kindA = sourceBean.getRuleFindUrl().split(\"(&&|\\n)+\");\n                    }\n                    emitter.onNext(kindA[0].split(\"::\")[1]);\n                    emitter.onComplete();\n                }\n            }).flatMap(url -> WebBookModel.getInstance().findBook(url, 1, sourceBean.getBookSourceUrl()))\n                    .subscribeOn(scheduler)\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .timeout(60, TimeUnit.SECONDS)\n                    .subscribe(new Observer<List<SearchBookBean>>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            checkSourceListener.compositeDisposableAdd(d);\n                        }\n\n                        @Override\n                        public void onNext(List<SearchBookBean> searchBookBeans) {\n                            if (searchBookBeans.isEmpty()) {\n                                sourceInvalid();\n                            } else {\n                                sourceUnInvalid();\n                            }\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            sourceInvalid();\n                        }\n\n                        @Override\n                        public void onComplete() {\n\n                        }\n                    });\n        } else {\n            sourceInvalid();\n        }\n    }\n\n    private void sourceInvalid() {\n        sourceBean.addGroup(\"失效\");\n        sourceBean.setEnable(false);\n        sourceBean.setSerialNumber(10000 + checkSourceListener.getCheckIndex());\n        DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(sourceBean);\n        checkSourceListener.nextCheck();\n    }\n\n    private void sourceUnInvalid() {\n        if (sourceBean.containsGroup(\"失效\")) {\n            sourceBean.removeGroup(\"失效\");\n            DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(sourceBean);\n        }\n        checkSourceListener.nextCheck();\n    }\n\n    /**\n     * 执行JS\n     */\n    private Object evalJS(String jsStr, String baseUrl, BookSourceBean bookSourceBean) throws Exception {\n        SimpleBindings bindings = new SimpleBindings();\n        bindings.put(\"java\", new AnalyzeRule(null, bookSourceBean));\n        bindings.put(\"baseUrl\", baseUrl);\n        return SCRIPT_ENGINE.eval(jsStr, bindings);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/model/task/DownloadTaskImpl.java",
    "content": "package com.kunfei.bookshelf.model.task;\n\nimport android.text.TextUtils;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.DownloadBookBean;\nimport com.kunfei.bookshelf.bean.DownloadChapterBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.model.impl.IDownloadTask;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.Scheduler;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\npublic abstract class DownloadTaskImpl implements IDownloadTask {\n\n    private int id;\n\n    private boolean isDownloading = false;\n\n    private DownloadBookBean downloadBook;\n    private List<DownloadChapterBean> downloadChapters;\n\n    private boolean isLocked = false;\n\n    private CompositeDisposable disposables;\n\n    protected DownloadTaskImpl(int id, DownloadBookBean downloadBook) {\n        this.id = id;\n        this.downloadBook = downloadBook;\n        downloadChapters = new ArrayList<>();\n        disposables = new CompositeDisposable();\n\n        Observable.create((ObservableOnSubscribe<DownloadBookBean>) emitter -> {\n            List<BookChapterBean> chapterList = BookshelfHelp.getChapterList(downloadBook.getNoteUrl());\n            if (!chapterList.isEmpty()) {\n                for (int i = downloadBook.getStart(); i <= downloadBook.getEnd(); i++) {\n                    DownloadChapterBean chapter = new DownloadChapterBean();\n                    chapter.setBookName(downloadBook.getName());\n                    chapter.setDurChapterIndex(chapterList.get(i).getDurChapterIndex());\n                    chapter.setDurChapterName(chapterList.get(i).getDurChapterName());\n                    chapter.setDurChapterUrl(chapterList.get(i).getDurChapterUrl());\n                    chapter.setNoteUrl(chapterList.get(i).getNoteUrl());\n                    chapter.setTag(chapterList.get(i).getTag());\n                    if (!BookshelfHelp.isChapterCached(chapter.getBookName(), chapter.getTag(), chapter, false)) {\n                        downloadChapters.add(chapter);\n                    }\n                }\n            }\n            downloadBook.setDownloadCount(downloadChapters.size());\n            emitter.onNext(downloadBook);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<DownloadBookBean>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        disposables.add(d);\n                    }\n\n                    @Override\n                    public void onNext(DownloadBookBean downloadBook) {\n                        if (downloadBook.isValid()) {\n                            onDownloadPrepared(downloadBook);\n                            whenProgress(downloadChapters.get(0));\n                        } else {\n                            onDownloadComplete(downloadBook);\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        downloadBook.setValid(false);\n                        onDownloadError(downloadBook);\n                    }\n                });\n    }\n\n    @Override\n    public int getId() {\n        return id;\n    }\n\n    @Override\n    public void startDownload(Scheduler scheduler) {\n        if (isFinishing()) return;\n\n        if (disposables.isDisposed()) {\n            disposables = new CompositeDisposable();\n        }\n\n        isDownloading = true;\n\n        toDownload(scheduler);\n    }\n\n    @Override\n    public void stopDownload() {\n        if (!disposables.isDisposed()) {\n            disposables.dispose();\n        }\n\n        if (isDownloading) {\n            isDownloading = false;\n            onDownloadComplete(downloadBook);\n        }\n\n        if (!isFinishing()) {\n            downloadChapters.clear();\n        }\n\n    }\n\n    @Override\n    public boolean isDownloading() {\n        return isDownloading;\n    }\n\n    @Override\n    public boolean isFinishing() {\n        return downloadChapters.isEmpty();\n    }\n\n    @Override\n    public DownloadBookBean getDownloadBook() {\n        return downloadBook;\n    }\n\n    private synchronized void toDownload(Scheduler scheduler) {\n        if (isFinishing()) {\n            return;\n        }\n\n        if (!isLocked) {\n            getDownloadingChapter()\n                    .subscribe(new MyObserver<DownloadChapterBean>() {\n                        @Override\n                        public void onNext(DownloadChapterBean chapterBean) {\n                            if (chapterBean != null) {\n                                downloading(chapterBean, scheduler);\n                            } else {\n                                isLocked = true;\n                            }\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            onDownloadError(downloadBook);\n                        }\n                    });\n        }\n    }\n\n    /**\n     * @return 章节下载信息\n     */\n    private Observable<DownloadChapterBean> getDownloadingChapter() {\n        return Observable.create(emitter -> {\n            DownloadChapterBean next = null;\n            List<DownloadChapterBean> temp = new ArrayList<>(downloadChapters);\n            for (DownloadChapterBean data : temp) {\n                boolean cached = BookshelfHelp.isChapterCached(data.getBookName(), data.getTag(), data, false);\n                if (cached) {\n                    removeFromDownloadList(data);\n                } else {\n                    next = data;\n                    break;\n                }\n            }\n            emitter.onNext(next);\n        });\n    }\n\n    /**\n     * 下载\n     */\n    private synchronized void downloading(DownloadChapterBean chapter, Scheduler scheduler) {\n        whenProgress(chapter);\n        BookShelfBean bookShelfBean = BookshelfHelp.getBook(chapter.getNoteUrl());\n        Observable.create((ObservableOnSubscribe<DownloadChapterBean>) e -> {\n            if (!BookshelfHelp.isChapterCached(chapter.getBookName(), chapter.getTag(), chapter, false)) {\n                e.onNext(chapter);\n            } else {\n                e.onError(new Exception(\"cached\"));\n            }\n            e.onComplete();\n        })\n                .flatMap(result -> {\n                    return WebBookModel.getInstance().getBookContent(bookShelfBean, chapter, null);\n                })\n                .subscribeOn(scheduler)\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<BookContentBean>() {\n\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        disposables.add(d);\n                    }\n\n                    @Override\n                    public void onNext(BookContentBean bookContentBean) {\n                        RxBus.get().post(RxBusTag.CHAPTER_CHANGE, bookContentBean);\n                        removeFromDownloadList(chapter);\n                        whenNext(scheduler, true);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        removeFromDownloadList(chapter);\n                        if (TextUtils.equals(e.getMessage(), \"cached\")) {\n                            whenNext(scheduler, false);\n                        } else {\n                            whenError(scheduler);\n                        }\n                    }\n                });\n    }\n\n    /**\n     * 从下载列表移除\n     */\n    private synchronized void removeFromDownloadList(DownloadChapterBean chapterBean) {\n        downloadChapters.remove(chapterBean);\n    }\n\n    private void whenNext(Scheduler scheduler, boolean success) {\n        if (!isDownloading) {\n            return;\n        }\n\n        if (success) {\n            downloadBook.successCountAdd();\n        }\n        if (isFinishing()) {\n            stopDownload();\n            onDownloadComplete(downloadBook);\n        } else {\n            onDownloadChange(downloadBook);\n            toDownload(scheduler);\n        }\n    }\n\n    private void whenError(Scheduler scheduler) {\n        if (!isDownloading) {\n            return;\n        }\n\n        if (isFinishing()) {\n            stopDownload();\n            if (downloadBook.getSuccessCount() == 0) {\n                onDownloadError(downloadBook);\n            } else {\n                onDownloadComplete(downloadBook);\n            }\n        } else {\n            toDownload(scheduler);\n        }\n    }\n\n    private void whenProgress(DownloadChapterBean chapterBean) {\n        if (!isDownloading) {\n            return;\n        }\n        onDownloadProgress(chapterBean);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/BookDetailPresenter.java",
    "content": "package com.kunfei.bookshelf.presenter;\n\nimport android.content.Intent;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.OpenChapterBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.bean.TwoDataBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.ChangeSourceHelp;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.SavedSource;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.presenter.contract.BookDetailContract;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\n\npublic class BookDetailPresenter extends BasePresenterImpl<BookDetailContract.View> implements BookDetailContract.Presenter {\n    public final static int FROM_BOOKSHELF = 1;\n    public final static int FROM_SEARCH = 2;\n\n    private int openFrom;\n    private SearchBookBean searchBook;\n    private BookShelfBean bookShelf;\n    private List<BookChapterBean> chapterBeanList;\n    private Boolean inBookShelf = false;\n    private final CompositeDisposable compositeDisposable = new CompositeDisposable();\n    private Disposable changeSourceDisposable;\n\n    @Override\n    public void initData(Intent intent) {\n        openFrom = intent.getIntExtra(\"openFrom\", FROM_BOOKSHELF);\n        String key = intent.getStringExtra(\"data_key\");\n        if (openFrom == FROM_BOOKSHELF) {\n            bookShelf = (BookShelfBean) BitIntentDataManager.getInstance().getData(key);\n            if (bookShelf == null) {\n                String noteUrl = intent.getStringExtra(\"noteUrl\");\n                if (!TextUtils.isEmpty(noteUrl)) {\n                    bookShelf = BookshelfHelp.getBook(noteUrl);\n                }\n            }\n            if (bookShelf == null) {\n                mView.finish();\n                return;\n            }\n            inBookShelf = true;\n            searchBook = new SearchBookBean();\n            searchBook.setNoteUrl(bookShelf.getNoteUrl());\n            searchBook.setTag(bookShelf.getTag());\n            chapterBeanList = BookshelfHelp.getChapterList(bookShelf.getNoteUrl());\n        } else {\n            initBookFormSearch((SearchBookBean) BitIntentDataManager.getInstance().getData(key));\n        }\n    }\n\n    @Override\n    public void initBookFormSearch(SearchBookBean searchBookBean) {\n        if (searchBookBean == null) {\n            mView.finish();\n            return;\n        }\n        searchBook = searchBookBean;\n        inBookShelf = BookshelfHelp.isInBookShelf(searchBookBean.getNoteUrl());\n        bookShelf = BookshelfHelp.getBookFromSearchBook(searchBookBean);\n    }\n\n    @Override\n    public Boolean getInBookShelf() {\n        return inBookShelf;\n    }\n\n    @Override\n    public int getOpenFrom() {\n        return openFrom;\n    }\n\n    @Override\n    public SearchBookBean getSearchBook() {\n        return searchBook;\n    }\n\n    @Override\n    public BookShelfBean getBookShelf() {\n        return bookShelf;\n    }\n\n    @Override\n    public List<BookChapterBean> getChapterList() {\n        return chapterBeanList;\n    }\n\n    @Override\n    public void getBookShelfInfo() {\n        if (bookShelf == null) return;\n        if (BookShelfBean.LOCAL_TAG.equals(bookShelf.getTag())) return;\n        WebBookModel.getInstance().getBookInfo(bookShelf)\n                .flatMap(bookShelfBean -> WebBookModel.getInstance().getChapterList(bookShelfBean))\n                .flatMap(chapterBeans -> saveBookToShelfO(bookShelf, chapterBeans))\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new MyObserver<List<BookChapterBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(@NonNull List<BookChapterBean> bookChapterBeans) {\n                        chapterBeanList = bookChapterBeans;\n                        mView.updateView();\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                        mView.toast(e.getLocalizedMessage());\n                        mView.getBookShelfError();\n                    }\n                });\n    }\n\n    /**\n     * 保存数据\n     */\n    private Observable<List<BookChapterBean>> saveBookToShelfO(BookShelfBean bookShelfBean, List<BookChapterBean> chapterBeans) {\n        return Observable.create(e -> {\n            if (inBookShelf) {\n                BookshelfHelp.saveBookToShelf(bookShelfBean);\n                if (!chapterBeans.isEmpty()) {\n                    BookshelfHelp.delChapterList(bookShelfBean.getNoteUrl());\n                    DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeans);\n                }\n                RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelf);\n            }\n            e.onNext(chapterBeans);\n            e.onComplete();\n        });\n    }\n\n    @Override\n    public void addToBookShelf() {\n        if (bookShelf != null) {\n            Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n                BookshelfHelp.saveBookToShelf(bookShelf);\n                searchBook.setIsCurrentSource(true);\n                inBookShelf = true;\n                e.onNext(true);\n                e.onComplete();\n            }).compose(RxUtils::toSimpleSingle)\n                    .subscribe(new MyObserver<Boolean>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            compositeDisposable.add(d);\n                        }\n\n                        @Override\n                        public void onNext(@NonNull Boolean value) {\n                            if (value) {\n                                RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelf);\n                                mView.updateView();\n                            } else {\n                                mView.toast(\"放入书架失败!\");\n                            }\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            e.printStackTrace();\n                            mView.toast(\"放入书架失败!\");\n                        }\n                    });\n        }\n    }\n\n    @Override\n    public void removeFromBookShelf() {\n        if (bookShelf != null) {\n            Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n                BookshelfHelp.removeFromBookShelf(bookShelf);\n                searchBook.setIsCurrentSource(false);\n                inBookShelf = false;\n                e.onNext(true);\n                e.onComplete();\n            }).compose(RxUtils::toSimpleSingle)\n                    .subscribe(new MyObserver<Boolean>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            compositeDisposable.add(d);\n                        }\n\n                        @Override\n                        public void onNext(@NonNull Boolean value) {\n                            if (value) {\n                                RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf);\n                                mView.updateView();\n                            } else {\n                                mView.toast(\"删除书籍失败！\");\n                            }\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            e.printStackTrace();\n                            mView.toast(\"删除书籍失败！\");\n                        }\n                    });\n        }\n    }\n\n    /**\n     * 换源\n     */\n    @Override\n    public void changeBookSource(SearchBookBean searchBookBean) {\n        if (changeSourceDisposable != null && !changeSourceDisposable.isDisposed()) {\n            changeSourceDisposable.dispose();\n        }\n        searchBookBean.setName(bookShelf.getBookInfoBean().getName());\n        searchBookBean.setAuthor(bookShelf.getBookInfoBean().getAuthor());\n        ChangeSourceHelp.changeBookSource(searchBookBean, bookShelf)\n                .subscribe(new MyObserver<TwoDataBean<BookShelfBean, List<BookChapterBean>>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        super.onSubscribe(d);\n                        compositeDisposable.add(d);\n                        changeSourceDisposable = d;\n                    }\n\n                    @Override\n                    public void onNext(@NonNull TwoDataBean<BookShelfBean, List<BookChapterBean>> value) {\n                        RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf);\n                        RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value);\n                        bookShelf = value.getData1();\n                        chapterBeanList = value.getData2();\n                        mView.updateView();\n                        String tag = bookShelf.getTag();\n                        try {\n                            long currentTime = System.currentTimeMillis();\n                            String bookName = bookShelf.getBookInfoBean().getName();\n                            BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(tag);\n                            if (SavedSource.Instance.getBookSource() != null && currentTime - SavedSource.Instance.getSaveTime() < 60000 && SavedSource.Instance.getBookName().equals(bookName))\n                                SavedSource.Instance.getBookSource().increaseWeight(-450);\n                            BookSourceManager.saveBookSource(SavedSource.Instance.getBookSource());\n                            SavedSource.Instance.setBookName(bookName);\n                            SavedSource.Instance.setSaveTime(currentTime);\n                            SavedSource.Instance.setBookSource(bookSourceBean);\n                            assert bookSourceBean != null;\n                            bookSourceBean.increaseWeightBySelection();\n                            BookSourceManager.saveBookSource(bookSourceBean);\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                        mView.updateView();\n                        mView.toast(e.getMessage());\n                    }\n                });\n    }\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n        compositeDisposable.dispose();\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD,\n            tags = {@Tag(RxBusTag.HAD_ADD_BOOK), @Tag(RxBusTag.UPDATE_BOOK_PROGRESS)})\n    public void hadAddOrRemoveBook(BookShelfBean bookShelfBean) {\n        bookShelf = bookShelfBean;\n        mView.updateView();\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.SKIP_TO_CHAPTER)})\n    public void skipToChapter(OpenChapterBean openChapterBean) {\n        bookShelf.setDurChapter(openChapterBean.getChapterIndex());\n        bookShelf.setDurChapterPage(openChapterBean.getPageIndex());\n        if (inBookShelf) {\n            BookshelfHelp.saveBookToShelf(bookShelf);\n        }\n        mView.readBook();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/BookListPresenter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.presenter;\n\nimport android.os.AsyncTask;\n\nimport androidx.annotation.NonNull;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.DownloadBookBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.model.content.WebBook;\nimport com.kunfei.bookshelf.presenter.contract.BookListContract;\nimport com.kunfei.bookshelf.service.DownloadService;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.Observer;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\nimport timber.log.Timber;\n\npublic class BookListPresenter extends BasePresenterImpl<BookListContract.View> implements BookListContract.Presenter {\n    private int threadsNum = 6;\n    private int refreshIndex;\n    private List<BookShelfBean> bookShelfBeans;\n    private int group;\n    private boolean hasUpdate = false;\n    private final CompositeDisposable compositeDisposable = new CompositeDisposable();\n\n    @Override\n    public void queryBookShelf(final Boolean needRefresh, final int group) {\n        this.group = group;\n        if (needRefresh) {\n            hasUpdate = false;\n        }\n        Observable.create((ObservableOnSubscribe<List<BookShelfBean>>) e -> {\n            List<BookShelfBean> bookShelfList;\n            if (group == 0) {\n                bookShelfList = BookshelfHelp.getAllBook();\n            } else {\n                bookShelfList = BookshelfHelp.getBooksByGroup(group - 1);\n            }\n            e.onNext(bookShelfList == null ? new ArrayList<>() : bookShelfList);\n            e.onComplete();\n        })\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<List<BookShelfBean>>() {\n                    @Override\n                    public void onNext(@NonNull List<BookShelfBean> value) {\n                        bookShelfBeans = value;\n                        mView.refreshBookShelf(bookShelfBeans);\n                        if (needRefresh && NetworkUtils.isNetWorkAvailable()) {\n                            startRefreshBook();\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        Timber.d(e);\n                    }\n                });\n    }\n\n    private void downloadAll(int downloadNum, boolean onlyNew) {\n        if (bookShelfBeans == null || mView.getContext() == null) {\n            return;\n        }\n        AsyncTask.execute(() -> {\n            for (BookShelfBean bookShelfBean : new ArrayList<>(bookShelfBeans)) {\n                if (!bookShelfBean.getTag().equals(BookShelfBean.LOCAL_TAG) && (!onlyNew || bookShelfBean.getHasUpdate())) {\n                    List<BookChapterBean> chapterBeanList = BookshelfHelp.getChapterList(bookShelfBean.getNoteUrl());\n                    if (chapterBeanList.size() >= bookShelfBean.getDurChapter()) {\n                        for (int start = bookShelfBean.getDurChapter(); start < chapterBeanList.size(); start++) {\n                            if (!chapterBeanList.get(start).getHasCache(bookShelfBean.getBookInfoBean())) {\n                                DownloadBookBean downloadBook = new DownloadBookBean();\n                                downloadBook.setName(bookShelfBean.getBookInfoBean().getName());\n                                downloadBook.setNoteUrl(bookShelfBean.getNoteUrl());\n                                downloadBook.setCoverUrl(bookShelfBean.getBookInfoBean().getCoverUrl());\n                                downloadBook.setStart(start);\n                                downloadBook.setEnd(downloadNum > 0 ? Math.min(chapterBeanList.size() - 1, start + downloadNum - 1) : chapterBeanList.size() - 1);\n                                downloadBook.setFinalDate(System.currentTimeMillis());\n                                DownloadService.addDownload(mView.getContext(), downloadBook);\n                                break;\n                            }\n                        }\n                    }\n                }\n            }\n        });\n    }\n\n    private void startRefreshBook() {\n        if (mView.getContext() != null) {\n            threadsNum = mView.getPreferences().getInt(mView.getContext().getString(R.string.pk_threads_num), 6);\n            if (bookShelfBeans != null && bookShelfBeans.size() > 0) {\n                refreshIndex = -1;\n                for (int i = 1; i <= threadsNum; i++) {\n                    refreshBookshelf();\n                }\n            }\n        }\n    }\n\n    private synchronized void refreshBookshelf() {\n        refreshIndex++;\n        if (refreshIndex < bookShelfBeans.size()) {\n            BookShelfBean bookShelfBean = bookShelfBeans.get(refreshIndex);\n            if (!bookShelfBean.getTag().equals(BookShelfBean.LOCAL_TAG) && bookShelfBean.getAllowUpdate() && bookShelfBean.getGroup() != 3) {\n                int chapterNum = bookShelfBean.getChapterListSize();\n                bookShelfBean.setLoading(true);\n                mView.refreshBook(bookShelfBean.getNoteUrl());\n                WebBookModel.getInstance().getChapterList(bookShelfBean)\n                        .flatMap(chapterBeanList -> saveBookToShelfO(bookShelfBean, chapterBeanList))\n                        .compose(RxUtils::toSimpleSingle)\n                        .subscribe(new Observer<BookShelfBean>() {\n                            @Override\n                            public void onSubscribe(@NonNull Disposable d) {\n                                compositeDisposable.add(d);\n                            }\n\n                            @Override\n                            public void onNext(@NonNull BookShelfBean value) {\n                                if (value.getErrorMsg() != null) {\n                                    mView.toast(value.getErrorMsg());\n                                    value.setErrorMsg(null);\n                                }\n                                bookShelfBean.setLoading(false);\n                                if (chapterNum < bookShelfBean.getChapterListSize())\n                                    hasUpdate = true;\n                                mView.refreshBook(bookShelfBean.getNoteUrl());\n                                refreshBookshelf();\n                            }\n\n                            @Override\n                            public void onError(@NonNull Throwable e) {\n                                if (!(e instanceof WebBook.NoSourceThrowable)) {\n                                    bookShelfBean.setLoading(false);\n                                    mView.refreshBook(bookShelfBean.getNoteUrl());\n                                    refreshBookshelf();\n                                }\n                            }\n\n                            @Override\n                            public void onComplete() {\n\n                            }\n                        });\n            } else {\n                refreshBookshelf();\n            }\n        } else if (refreshIndex >= bookShelfBeans.size() + threadsNum - 1) {\n            if (hasUpdate && mView.getPreferences().getBoolean(mView.getContext().getString(R.string.pk_auto_download), false)) {\n                downloadAll(10, true);\n                hasUpdate = false;\n            }\n            queryBookShelf(false, group);\n        }\n    }\n\n    /**\n     * 保存数据\n     */\n    private Observable<BookShelfBean> saveBookToShelfO(BookShelfBean bookShelfBean, List<BookChapterBean> chapterBeanList) {\n        return Observable.create(e -> {\n            if (!chapterBeanList.isEmpty()) {\n                BookshelfHelp.delChapterList(bookShelfBean.getNoteUrl());\n                BookshelfHelp.saveBookToShelf(bookShelfBean);\n                DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeanList);\n            }\n            e.onNext(bookShelfBean);\n            e.onComplete();\n        });\n    }\n\n    /////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n        compositeDisposable.dispose();\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD,\n            tags = {@Tag(RxBusTag.HAD_ADD_BOOK), @Tag(RxBusTag.HAD_REMOVE_BOOK), @Tag(RxBusTag.UPDATE_BOOK_PROGRESS)})\n    public void hadAddOrRemoveBook(BookShelfBean bookShelfBean) {\n        queryBookShelf(false, group);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.UPDATE_GROUP)})\n    public void updateGroup(Integer group) {\n        this.group = group;\n        mView.updateGroup(group);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.REFRESH_BOOK_LIST)})\n    public void reFlashBookList(Boolean needRefresh) {\n        queryBookShelf(needRefresh, group);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.DOWNLOAD_ALL)})\n    public void downloadAll(Integer downloadNum) {\n        downloadAll(downloadNum, false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/BookSourcePresenter.java",
    "content": "package com.kunfei.bookshelf.presenter;\n\nimport android.annotation.SuppressLint;\nimport android.graphics.Color;\nimport android.os.AsyncTask;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.documentfile.provider.DocumentFile;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.DocumentHelper;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.presenter.contract.BookSourceContract;\nimport com.kunfei.bookshelf.service.CheckSourceService;\n\nimport java.io.File;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\nimport static android.app.Activity.RESULT_OK;\nimport static android.text.TextUtils.isEmpty;\n\n/**\n * Created by GKF on 2017/12/18.\n * 书源管理\n */\n\npublic class BookSourcePresenter extends BasePresenterImpl<BookSourceContract.View> implements BookSourceContract.Presenter {\n    private BookSourceBean delBookSource;\n    private Snackbar progressSnackBar;\n\n    @Override\n    public void saveData(BookSourceBean bookSourceBean) {\n        AsyncTask.execute(() -> DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean));\n    }\n\n    @Override\n    public void saveData(List<BookSourceBean> bookSourceBeans) {\n        AsyncTask.execute(() -> {\n            if (mView.getSort() == 0) {\n                for (int i = 1; i <= bookSourceBeans.size(); i++) {\n                    bookSourceBeans.get(i - 1).setSerialNumber(i);\n                }\n            }\n            DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(bookSourceBeans);\n        });\n    }\n\n    @Override\n    public void delData(BookSourceBean bookSourceBean) {\n        this.delBookSource = bookSourceBean;\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            DbHelper.getDaoSession().getBookSourceBeanDao().delete(bookSourceBean);\n            e.onNext(true);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean aBoolean) {\n                        mView.getSnackBar(delBookSource.getBookSourceName() + \"已删除\", Snackbar.LENGTH_LONG)\n                                .setAction(\"恢复\", view -> restoreBookSource(delBookSource))\n                                .setActionTextColor(Color.WHITE)\n                                .show();\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(\"删除失败\");\n                        mView.refreshBookSource();\n                    }\n                });\n    }\n\n    @Override\n    public void delData(List<BookSourceBean> bookSourceBeans) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            for (BookSourceBean sourceBean : bookSourceBeans) {\n                DbHelper.getDaoSession().getBookSourceBeanDao().delete(sourceBean);\n            }\n            e.onNext(true);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean aBoolean) {\n                        mView.toast(\"删除成功\");\n                        mView.refreshBookSource();\n                        mView.setResult(RESULT_OK);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(\"删除失败\");\n                    }\n                });\n    }\n\n    private void restoreBookSource(BookSourceBean bookSourceBean) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            BookSourceManager.addBookSource(bookSourceBean);\n            e.onNext(true);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean aBoolean) {\n                        mView.refreshBookSource();\n                        mView.setResult(RESULT_OK);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                    }\n                });\n    }\n\n    @Override\n    public void importBookSource(String text) {\n        mView.showSnackBar(\"正在导入书源\", Snackbar.LENGTH_INDEFINITE);\n        Observable<List<BookSourceBean>> observable = BookSourceManager.importSource(text);\n        if (observable != null) {\n            observable.subscribe(getImportObserver());\n        } else {\n            mView.showSnackBar(\"格式不对\", Snackbar.LENGTH_SHORT);\n        }\n    }\n\n    @Override\n    public void importBookSourceLocal(String path) {\n        if (TextUtils.isEmpty(path)) {\n            mView.toast(R.string.read_file_error);\n            return;\n        }\n        String json;\n        DocumentFile file;\n        try {\n            file = DocumentFile.fromFile(new File(path));\n        } catch (Exception e) {\n            mView.toast(path + \"无法打开！\");\n            return;\n        }\n        json = DocumentHelper.readString(file);\n        if (!isEmpty(json)) {\n            mView.showSnackBar(\"正在导入书源\", Snackbar.LENGTH_INDEFINITE);\n            importBookSource(json);\n        } else {\n            mView.toast(R.string.read_file_error);\n        }\n    }\n\n    private MyObserver<List<BookSourceBean>> getImportObserver() {\n        return new MyObserver<List<BookSourceBean>>() {\n            @SuppressLint(\"DefaultLocale\")\n            @Override\n            public void onNext(List<BookSourceBean> bookSourceBeans) {\n                if (bookSourceBeans.size() > 0) {\n                    mView.refreshBookSource();\n                    mView.showSnackBar(String.format(\"导入成功%d个书源\", bookSourceBeans.size()), Snackbar.LENGTH_SHORT);\n                    mView.setResult(RESULT_OK);\n                } else {\n                    mView.showSnackBar(\"格式不对\", Snackbar.LENGTH_SHORT);\n                }\n            }\n\n            @Override\n            public void onError(Throwable e) {\n                mView.showSnackBar(e.getMessage(), Snackbar.LENGTH_SHORT);\n            }\n        };\n    }\n\n    @Override\n    public void checkBookSource(List<BookSourceBean> sourceBeans) {\n        CheckSourceService.start(mView.getContext(), sourceBeans);\n    }\n\n    @Override\n    public void checkFindSource(List<BookSourceBean> sourceBeans) {\n        String TAG_FIND_SOUECE=\"发现\";\n\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            for (BookSourceBean sourceBean : sourceBeans) {\n                String rule=sourceBean.getRuleFindUrl();\n                if(rule==null)\n                    sourceBean.removeGroup(TAG_FIND_SOUECE);\n                else if(rule.trim().length()<1){\n                    sourceBean.removeGroup(TAG_FIND_SOUECE);\n                    sourceBean.setRuleFindUrl(null);\n                }else{\n                    sourceBean.addGroup(TAG_FIND_SOUECE);\n                }\n                DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(sourceBean);\n            }\n            e.onNext(true);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean aBoolean) {\n                        mView.toast(TAG_FIND_SOUECE+\"标签验校完成\");\n                        mView.refreshBookSource();\n                        mView.setResult(RESULT_OK);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(\"验校失败\");\n                    }\n                });\n    }\n\n    /////////////////////////////////////////////////\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n    }\n\n    /////////////////////RxBus////////////////////////\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.CHECK_SOURCE_STATE)})\n    public void upCheckSourceState(String msg) {\n        mView.refreshBookSource();\n        if (progressSnackBar == null) {\n            progressSnackBar = mView.getSnackBar(msg, Snackbar.LENGTH_INDEFINITE);\n            progressSnackBar.setActionTextColor(Color.WHITE);\n            progressSnackBar.setAction(mView.getContext().getString(R.string.cancel), view -> CheckSourceService.stop(mView.getContext()));\n        } else {\n            progressSnackBar.setText(msg);\n        }\n        if (!progressSnackBar.isShown()) {\n            progressSnackBar.show();\n        }\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.CHECK_SOURCE_FINISH)})\n    public void checkSourceFinish(String msg) {\n        mView.showSnackBar(msg, Snackbar.LENGTH_SHORT);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/ChoiceBookPresenter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.presenter;\n\nimport android.content.Intent;\n\nimport androidx.annotation.NonNull;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.presenter.contract.ChoiceBookContract;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.Observer;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class ChoiceBookPresenter extends BasePresenterImpl<ChoiceBookContract.View> implements ChoiceBookContract.Presenter {\n    private final CompositeDisposable compositeDisposable = new CompositeDisposable();\n    private final String tag;\n    private final String url;\n    private final String title;\n\n    private int page = 1;\n    private long startThisSearchTime;\n\n    public ChoiceBookPresenter(final Intent intent) {\n        url = intent.getStringExtra(\"url\");\n        title = intent.getStringExtra(\"title\");\n        tag = intent.getStringExtra(\"tag\");\n        Observable.create((ObservableOnSubscribe<List<BookShelfBean>>) e -> {\n            List<BookShelfBean> temp = DbHelper.getDaoSession().getBookShelfBeanDao().queryBuilder().list();\n            if (temp == null)\n                temp = new ArrayList<>();\n            e.onNext(temp);\n        }).subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new Observer<List<BookShelfBean>>() {\n                    @Override\n                    public void onSubscribe(@NonNull Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(@NonNull List<BookShelfBean> value) {\n                        initPage();\n                        toSearchBooks(null);\n                        mView.startRefreshAnim();\n                    }\n\n                    @Override\n                    public void onError(@NonNull Throwable e) {\n                        e.printStackTrace();\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n\n    @Override\n    public int getPage() {\n        return page;\n    }\n\n    @Override\n    public void initPage() {\n        this.page = 1;\n        this.startThisSearchTime = System.currentTimeMillis();\n    }\n\n    @Override\n    public void toSearchBooks(String key) {\n        final long tempTime = startThisSearchTime;\n        searchBook(tempTime);\n    }\n\n    private void searchBook(final long searchTime) {\n        WebBookModel.getInstance().findBook(url, page, tag)\n                .subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new Observer<List<SearchBookBean>>() {\n                    @Override\n                    public void onSubscribe(@NonNull Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(@NonNull List<SearchBookBean> value) {\n                        if (searchTime == startThisSearchTime) {\n                            if (page == 1) {\n                                mView.refreshSearchBook(value);\n                                mView.refreshFinish(value.size() <= 0);\n                            } else {\n                                mView.loadMoreSearchBook(value);\n                            }\n                            page++;\n                        }\n                    }\n\n                    @Override\n                    public void onError(@NonNull Throwable e) {\n                        e.printStackTrace();\n                        mView.searchBookError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n\n    @Override\n    public String getTitle() {\n        return title;\n    }\n\n    ////////////////////////////////////////////////////////////////////////////////////////////////////\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n        compositeDisposable.dispose();\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/FindBookPresenter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.presenter;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE;\n\nimport android.util.Pair;\nimport android.widget.Toast;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.FindKindBean;\nimport com.kunfei.bookshelf.bean.FindKindGroupBean;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeRule;\nimport com.kunfei.bookshelf.presenter.contract.FindBookContract;\nimport com.kunfei.bookshelf.utils.ACache;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport javax.script.SimpleBindings;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleObserver;\nimport io.reactivex.SingleOnSubscribe;\nimport io.reactivex.disposables.Disposable;\n\npublic class FindBookPresenter extends BasePresenterImpl<FindBookContract.View> implements FindBookContract.Presenter {\n    private Disposable disposable;\n    private AnalyzeRule analyzeRule;\n    private String findError = \"发现规则语法错误\";\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void initData() {\n        if (disposable != null) return;\n        ACache aCache = ACache.get(mView.getContext(), \"findCache\");\n        Single.create((SingleOnSubscribe<List<RecyclerViewData>>) e -> {\n            List<RecyclerViewData> group = new ArrayList<>();\n            boolean showAllFind = MApplication.getConfigPreferences().getBoolean(\"showAllFind\", true);\n            List<BookSourceBean> sourceBeans = new ArrayList<>(showAllFind ? BookSourceManager.getAllBookSourceBySerialNumber() : BookSourceManager.getSelectedBookSourceBySerialNumber());\n            for (BookSourceBean sourceBean : sourceBeans) {\n                Pair<FindKindGroupBean, List<FindKindBean>> pair = sourceBean.getFindList();\n                if (pair != null) {\n                    group.add(new RecyclerViewData(pair.first, pair.second, false));\n                }\n            }\n            e.onSuccess(group);\n        })\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new SingleObserver<List<RecyclerViewData>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        disposable = d;\n                    }\n\n                    @Override\n                    public void onSuccess(List<RecyclerViewData> recyclerViewData) {\n                        mView.upData(recyclerViewData);\n                        disposable.dispose();\n                        disposable = null;\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                        Toast.makeText(mView.getContext(), e.getMessage(), Toast.LENGTH_SHORT).show();\n                        disposable.dispose();\n                        disposable = null;\n                    }\n                });\n    }\n\n    /**\n     * 执行JS\n     */\n    private Object evalJS(String jsStr, String baseUrl, BookSourceBean bookSourceBean) throws Exception {\n        SimpleBindings bindings = new SimpleBindings();\n        bindings.put(\"java\", new AnalyzeRule(null, bookSourceBean));\n        bindings.put(\"baseUrl\", baseUrl);\n        return SCRIPT_ENGINE.eval(jsStr, bindings);\n    }\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n    }\n\n    @Override\n    public void detachView() {\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/ImportBookPresenter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.presenter;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.bookshelf.bean.LocBookShelfBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.model.ImportBookModel;\nimport com.kunfei.bookshelf.presenter.contract.ImportBookContract;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.io.File;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.Observer;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\n\npublic class ImportBookPresenter extends BasePresenterImpl<ImportBookContract.View> implements ImportBookContract.Presenter {\n\n    private CompositeDisposable compositeDisposable = new CompositeDisposable();\n\n    @Override\n    public void importBooks(List<File> books) {\n        Observable.fromIterable(books)\n                .flatMap(file -> ImportBookModel.getInstance().importBook(file))\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<LocBookShelfBean>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(LocBookShelfBean value) {\n                        if (value.getNew()) {\n                            RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value.getBookShelfBean());\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                        mView.addError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n                        mView.addSuccess();\n                    }\n                });\n    }\n\n\n    @Override\n    public void detachView() {\n        compositeDisposable.dispose();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/MainPresenter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.presenter;\n\nimport android.annotation.SuppressLint;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.dao.BookSourceBeanDao;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.presenter.contract.MainContract;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class MainPresenter extends BasePresenterImpl<MainContract.View> implements MainContract.Presenter {\n\n    /**\n     * @param bookUrls 如果不包含书源，一行为一本小说的地址。如果包含书源，只解析为一本数，以免url#{{书源}}中书源包含换行\n     */\n    @Override\n    public void addBookUrl(String bookUrls) {\n        bookUrls = bookUrls.trim();\n        if (TextUtils.isEmpty(bookUrls)) return;\n\n        String[] urls;\n        if (bookUrls.matches(\"[^\\n]+#\\\\{[\\\\s\\\\S]+\")) {\n            urls = new String[]{bookUrls};\n        } else {\n            urls = bookUrls.split(\"\\\\n\");\n        }\n\n        Observable.fromArray(urls)\n                .flatMap(this::addBookUrlO)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new MyObserver<BookShelfBean>() {\n                    @Override\n                    public void onNext(@NonNull BookShelfBean bookShelfBean) {\n                        getBook(bookShelfBean);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(e.getMessage());\n                    }\n                });\n    }\n\n    private Observable<BookShelfBean> addBookUrlO(String bookUrl) {\n        return Observable.create(e -> {\n            if (StringUtils.isTrimEmpty(bookUrl)) {\n                e.onComplete();\n                return;\n            }\n\n            String source = \"\";\n            String url = bookUrl;\n            if (url.replaceAll(\"(\\\\s|\\n)*\", \"\").matches(\"^.*(#\\\\{).*\")) {\n                String[] string = bookUrl.split(\"#\\\\{\", 2);\n                source = StringUtils.unCompressJson(string[1]);\n                if (StringUtils.isJsonType(source))\n                    url = string[0];\n                else\n                    source = \"\";\n            }\n\n            BookInfoBean temp = DbHelper.getDaoSession().getBookInfoBeanDao().load(url);\n            if (temp != null) {\n                e.onError(new Throwable(\"已在书架中\"));\n                return;\n            } else {\n                String baseUrl = StringUtils.getBaseUrl(url);\n                BookSourceBean bookSourceBean = DbHelper.getDaoSession().getBookSourceBeanDao().load(baseUrl);\n\n                // RuleBookUrlPattern推定  考虑有书源规则不完善，需要排除RuleBookUrlPatternt填写.*匹配全部url的情况\n                if (bookSourceBean == null) {\n                    List<BookSourceBean> sourceBeans = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                            .where(BookSourceBeanDao.Properties.RuleBookUrlPattern.isNotNull()\n                                    , BookSourceBeanDao.Properties.RuleBookUrlPattern.notEq(\"\")\n                                    , BookSourceBeanDao.Properties.RuleBookUrlPattern.notEq(\".*\")\n                            ).list();\n                    for (BookSourceBean sourceBean : sourceBeans) {\n                        if (url.matches(sourceBean.getRuleBookUrlPattern())) {\n                            bookSourceBean = sourceBean;\n                            break;\n                        }\n                    }\n                }\n\n                //BookSourceUrl推定  考虑有书源规则不完善，没有填写RuleBookUrlPattern的情况（但是通常会填写bookSourceUrl），因此需要做补充\n                if (bookSourceBean == null) {\n                    String siteUrl = url.replaceFirst(\"^(http://|https://)?(m\\\\.|www\\\\.|web\\\\.)?\", \"\").replaceFirst(\"/.*$\", \"\");\n                    List<BookSourceBean> sourceBeans = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                            .where(BookSourceBeanDao.Properties.BookSourceUrl.like(\"%\" + siteUrl + \"%\")).list();\n                    for (BookSourceBean sourceBean : sourceBeans) {\n                        //由于RuleBookUrlPattern推定排除了RuleBookUrlPattern为空或者匹配所有字符的情况，因此需要做过杀推定\n                        if (sourceBean.getRuleBookUrlPattern().equals(null)) {\n                            bookSourceBean = sourceBean;\n                            break;\n                        } else if (sourceBean.getRuleBookUrlPattern().replaceAll(\"\\\\s\", \"\").length() == 0) {\n                            bookSourceBean = sourceBean;\n                            break;\n                        }\n                        if (url.matches(sourceBean.getRuleBookUrlPattern())) {\n                            bookSourceBean = sourceBean;\n                            break;\n                        }\n                    }\n                }\n                BookShelfBean bookShelfBean = new BookShelfBean();\n                bookShelfBean.setNoteUrl(url);\n                if (bookSourceBean != null) {\n                    bookShelfBean.setTag(bookSourceBean.getBookSourceUrl());\n                    bookShelfBean.setDurChapter(0);\n                    bookShelfBean.setGroup(mView.getGroup() % 4);\n                    bookShelfBean.setDurChapterPage(0);\n                    bookShelfBean.setFinalDate(System.currentTimeMillis());\n                    e.onNext(bookShelfBean);\n                } else {\n                    if (source.length() > 10) {\n                        Observable<List<BookSourceBean>> observable = BookSourceManager.importSource(source);\n                        if (observable != null) {\n                            observable.subscribe(new MyObserver<List<BookSourceBean>>() {\n                                @SuppressLint(\"DefaultLocale\")\n                                @Override\n                                public void onNext(@NonNull List<BookSourceBean> bookSourceBeans) {\n                                    if (bookSourceBeans.size() == 1) {\n                                        BookSourceBean bean = (bookSourceBeans.get(0));\n//                                         BookShelfBean bookShelfBean = new BookShelfBean();\n                                        bookShelfBean.setTag(bean.getBookSourceUrl());\n//                                         bookShelfBean.setNoteUrl(url);\n                                        bookShelfBean.setDurChapter(0);\n                                        bookShelfBean.setGroup(mView.getGroup() % 4);\n                                        bookShelfBean.setDurChapterPage(0);\n                                        bookShelfBean.setFinalDate(System.currentTimeMillis());\n//                                        e.onNext(bookShelfBean);\n                                        getBook(bookShelfBean);\n                                    } else {\n                                        e.onError(new Throwable(\"未导入内嵌的书源-\" + bookSourceBeans.size()));\n                                    }\n                                }\n/*                                @Override\n                                public void onError(Throwable e) {\n                                    mView.toast(e.getLocalizedMessage());\n                                }*/\n                            });\n                        } else {\n                            e.onError(new Throwable(\"未找到内嵌的书源\"));\n                        }\n                    }\n\n                    e.onError(new Throwable(\"未找到对应书源\"));\n                    return;\n                }\n            }\n            e.onComplete();\n        });\n    }\n\n    private void getBook(BookShelfBean bookShelfBean) {\n        WebBookModel.getInstance()\n                .getBookInfo(bookShelfBean)\n                .flatMap(bookShelfBean1 -> WebBookModel.getInstance().getChapterList(bookShelfBean1))\n                .flatMap(chapterBeanList -> saveBookToShelfO(bookShelfBean, chapterBeanList))\n                .subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<BookShelfBean>() {\n                    @Override\n                    public void onNext(@NonNull BookShelfBean value) {\n                        if (value.getBookInfoBean().getChapterUrl() == null) {\n                            mView.toast(\"添加书籍失败\");\n                        } else {\n                            //成功   //发送RxBus\n                            RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelfBean);\n                            mView.toast(\"添加书籍成功\");\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(\"添加书籍失败\" + e.getMessage());\n                    }\n                });\n    }\n\n    /**\n     * 保存数据\n     */\n    private Observable<BookShelfBean> saveBookToShelfO(BookShelfBean bookShelfBean, List<BookChapterBean> chapterBeanList) {\n        return Observable.create(e -> {\n            BookshelfHelp.saveBookToShelf(bookShelfBean);\n            DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterBeanList);\n            e.onNext(bookShelfBean);\n            e.onComplete();\n        });\n    }\n\n    /////////////////////////////////////////////////////////////////////////////////////////////////////////////\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.IMMERSION_CHANGE)})\n    public void initImmersionBar(Boolean immersion) {\n        mView.initImmersionBar();\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.RECREATE)})\n    public void recreate(Boolean recreate) {\n        mView.recreate();\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/ReadBookPresenter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.presenter;\n\nimport static android.text.TextUtils.isEmpty;\n\nimport android.app.Activity;\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.provider.MediaStore;\n\nimport androidx.annotation.NonNull;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.BookmarkBean;\nimport com.kunfei.bookshelf.bean.DownloadBookBean;\nimport com.kunfei.bookshelf.bean.LocBookShelfBean;\nimport com.kunfei.bookshelf.bean.OpenChapterBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.bean.TwoDataBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.ChangeSourceHelp;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.ImportBookModel;\nimport com.kunfei.bookshelf.model.SavedSource;\nimport com.kunfei.bookshelf.presenter.contract.ReadBookContract;\nimport com.kunfei.bookshelf.service.DownloadService;\nimport com.kunfei.bookshelf.service.ReadAloudService;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class ReadBookPresenter extends BasePresenterImpl<ReadBookContract.View> implements ReadBookContract.Presenter {\n    private final static int OPEN_FROM_OTHER = 0;\n    public final static int OPEN_FROM_APP = 1;\n\n    private BookShelfBean bookShelf;\n    private ChangeSourceHelp changeSourceHelp;\n    private List<BookChapterBean> chapterBeanList = new ArrayList<>();\n    private Disposable changeSourceDisposable;\n\n    @Override\n    public void initData(Activity activity) {\n        Intent intent = activity.getIntent();\n        int open_from = intent.getData() != null ? OPEN_FROM_OTHER : OPEN_FROM_APP;\n        open_from = intent.getIntExtra(\"openFrom\", open_from);\n        mView.setAdd(intent.getBooleanExtra(\"inBookshelf\", true));\n        if (open_from == OPEN_FROM_APP) {\n            loadBook(intent);\n        } else {\n            mView.openBookFromOther();\n            mView.upMenu();\n        }\n    }\n\n    @Override\n    public void loadBook(Intent intent) {\n        Observable.create((ObservableOnSubscribe<BookShelfBean>) e -> {\n            if (bookShelf == null) {\n                String bookKey = intent.getStringExtra(\"bookKey\");\n                if (!isEmpty(bookKey)) {\n                    bookShelf = (BookShelfBean) BitIntentDataManager.getInstance().getData(bookKey);\n                }\n            }\n            if (bookShelf == null && !isEmpty(mView.getNoteUrl())) {\n                bookShelf = BookshelfHelp.getBook(mView.getNoteUrl());\n            }\n            if (bookShelf == null) {\n                List<BookShelfBean> beans = BookshelfHelp.getAllBook();\n                if (beans != null && beans.size() > 0) {\n                    bookShelf = beans.get(0);\n                }\n            }\n            if (bookShelf != null && chapterBeanList.isEmpty()) {\n                chapterBeanList = BookshelfHelp.getChapterList(bookShelf.getNoteUrl());\n            }\n            if (bookShelf == null) {\n                e.onError(new Exception(\"没有书籍\"));\n            } else {\n                e.onNext(bookShelf);\n                e.onComplete();\n            }\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<BookShelfBean>() {\n                    @Override\n                    public void onNext(@NonNull BookShelfBean bookShelfBean) {\n                        if (isEmpty(bookShelf.getBookInfoBean().getName())) {\n                            mView.finish();\n                        } else {\n                            mView.startLoadingBook();\n                            mView.upMenu();\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.finish();\n                    }\n                });\n    }\n\n    /**\n     * 禁用当前书源\n     */\n    public void disableDurBookSource() {\n        try {\n            BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(bookShelf.getTag());\n            if (bookSourceBean != null) {\n                bookSourceBean.addGroup(\"禁用\");\n                DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean);\n                mView.toast(\"已禁用\" + bookSourceBean.getBookSourceName());\n            }\n        } catch (Exception ignored) {\n        }\n    }\n\n    @Override\n    public BookChapterBean getDurChapter() {\n        if (chapterBeanList.size() == 0) {\n            return null;\n        }\n        if (chapterBeanList.size() > bookShelf.getDurChapter()) {\n            return chapterBeanList.get(bookShelf.getDurChapter());\n        }\n        return chapterBeanList.get(chapterBeanList.size() - 1);\n    }\n\n    @Override\n    public void saveBook() {\n        if (bookShelf != null) {\n            AsyncTask.execute(() -> BookshelfHelp.saveBookToShelf(bookShelf));\n        }\n    }\n\n    @Override\n    public void saveProgress() {\n        if (bookShelf != null) {\n            AsyncTask.execute(() -> {\n                bookShelf.setFinalDate(System.currentTimeMillis());\n                bookShelf.setHasUpdate(false);\n                DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelf);\n                RxBus.get().post(RxBusTag.UPDATE_BOOK_PROGRESS, bookShelf);\n            });\n        }\n    }\n\n    /**\n     * APP外部打开\n     */\n    @Override\n    public void openBookFromOther(Activity activity) {\n        Uri uri = activity.getIntent().getData();\n        getRealFilePath(activity, uri)\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribeOn(Schedulers.io())\n                .subscribe(new MyObserver<String>() {\n                    @Override\n                    public void onNext(@NonNull String value) {\n                        ImportBookModel.getInstance().importBook(new File(value))\n                                .observeOn(AndroidSchedulers.mainThread())\n                                .subscribeOn(Schedulers.io())\n                                .subscribe(new MyObserver<LocBookShelfBean>() {\n                                    @Override\n                                    public void onNext(@NonNull LocBookShelfBean value) {\n                                        if (value.getNew())\n                                            RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value);\n                                        bookShelf = value.getBookShelfBean();\n                                        mView.setAdd(BookshelfHelp.isInBookShelf(bookShelf.getNoteUrl()));\n                                        mView.startLoadingBook();\n                                    }\n\n                                    @Override\n                                    public void onError(Throwable e) {\n                                        e.printStackTrace();\n                                        mView.toast(\"文本打开失败！\");\n                                    }\n                                });\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                        mView.toast(\"文本打开失败！\");\n                    }\n                });\n    }\n\n    /**\n     * 下载\n     */\n    @Override\n    public void addDownload(int start, int end) {\n        addToShelf(() -> {\n            DownloadBookBean downloadBook = new DownloadBookBean();\n            downloadBook.setName(bookShelf.getBookInfoBean().getName());\n            downloadBook.setNoteUrl(bookShelf.getNoteUrl());\n            downloadBook.setCoverUrl(bookShelf.getBookInfoBean().getCoverUrl());\n            downloadBook.setStart(start);\n            downloadBook.setEnd(end);\n            downloadBook.setFinalDate(System.currentTimeMillis());\n            DownloadService.addDownload(mView.getContext(), downloadBook);\n        });\n    }\n\n    /**\n     * 换源\n     */\n    @Override\n    public void changeBookSource(SearchBookBean searchBook) {\n        if (changeSourceDisposable != null && !changeSourceDisposable.isDisposed()) {\n            changeSourceDisposable.dispose();\n        }\n        searchBook.setName(bookShelf.getBookInfoBean().getName());\n        searchBook.setAuthor(bookShelf.getBookInfoBean().getAuthor());\n        ChangeSourceHelp.changeBookSource(searchBook, bookShelf)\n                .subscribe(new MyObserver<TwoDataBean<BookShelfBean, List<BookChapterBean>>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        super.onSubscribe(d);\n                        changeSourceDisposable = d;\n                    }\n\n                    @Override\n                    public void onNext(@NonNull TwoDataBean<BookShelfBean, List<BookChapterBean>> value) {\n                        RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf);\n                        RxBus.get().post(RxBusTag.HAD_ADD_BOOK, value);\n                        bookShelf = value.getData1();\n                        chapterBeanList = value.getData2();\n                        mView.changeSourceFinish(bookShelf);\n                        String tag = bookShelf.getTag();\n                        try {\n                            long currentTime = System.currentTimeMillis();\n                            String bookName = bookShelf.getBookInfoBean().getName();\n                            BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(tag);\n                            if (SavedSource.Instance.getBookSource() != null\n                                    && currentTime - SavedSource.Instance.getSaveTime() < 60000\n                                    && SavedSource.Instance.getBookName().equals(bookName))\n                                SavedSource.Instance.getBookSource().increaseWeight(-450);\n                            BookSourceManager.saveBookSource(SavedSource.Instance.getBookSource());\n                            SavedSource.Instance.setBookName(bookName);\n                            SavedSource.Instance.setSaveTime(currentTime);\n                            SavedSource.Instance.setBookSource(bookSourceBean);\n                            assert bookSourceBean != null;\n                            bookSourceBean.increaseWeightBySelection();\n                            BookSourceManager.saveBookSource(bookSourceBean);\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(e.getMessage());\n                        mView.changeSourceFinish(null);\n                    }\n                });\n    }\n\n    @Override\n    public void autoChangeSource() {\n        if (changeSourceHelp == null) {\n            changeSourceHelp = new ChangeSourceHelp();\n        }\n        changeSourceHelp.autoChange(bookShelf, new ChangeSourceHelp.ChangeSourceListener() {\n\n            @Override\n            public void finish(BookShelfBean bookShelfBean, List<BookChapterBean> chapterBeanList) {\n                if (!chapterBeanList.isEmpty()) {\n                    RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf);\n                    RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelfBean);\n                    bookShelf = bookShelfBean;\n                    ReadBookPresenter.this.chapterBeanList = chapterBeanList;\n                    mView.changeSourceFinish(bookShelf);\n                } else {\n                    mView.changeSourceFinish(null);\n                }\n            }\n\n            @Override\n            public void error(Throwable throwable) {\n                mView.toast(throwable.getMessage());\n                mView.changeSourceFinish(null);\n            }\n        });\n    }\n\n    @Override\n    public void saveBookmark(BookmarkBean bookmarkBean) {\n        Observable.create((ObservableOnSubscribe<BookmarkBean>) e -> {\n            BookshelfHelp.saveBookmark(bookmarkBean);\n            e.onNext(bookmarkBean);\n            e.onComplete();\n        })\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe();\n    }\n\n    @Override\n    public void delBookmark(BookmarkBean bookmarkBean) {\n        Observable.create((ObservableOnSubscribe<BookmarkBean>) e -> {\n            BookshelfHelp.delBookmark(bookmarkBean);\n            e.onNext(bookmarkBean);\n            e.onComplete();\n        })\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe();\n    }\n\n    @Override\n    public BookShelfBean getBookShelf() {\n        return bookShelf;\n    }\n\n    @Override\n    public List<BookChapterBean> getChapterList() {\n        return chapterBeanList;\n    }\n\n    @Override\n    public void setChapterList(List<BookChapterBean> chapterList) {\n        this.chapterBeanList = chapterList;\n        AsyncTask.execute(() -> DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(chapterList));\n    }\n\n    @Override\n    public void addToShelf(final OnAddListener addListener) {\n        if (bookShelf != null) {\n            AsyncTask.execute(() -> {\n                BookshelfHelp.saveBookToShelf(bookShelf);\n                RxBus.get().post(RxBusTag.HAD_ADD_BOOK, bookShelf);\n                mView.setAdd(true);\n                if (addListener != null) {\n                    addListener.addSuccess();\n                }\n            });\n        }\n    }\n\n    @Override\n    public void removeFromShelf() {\n        if (bookShelf != null) {\n            Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n                BookshelfHelp.removeFromBookShelf(bookShelf);\n                e.onNext(true);\n                e.onComplete();\n            }).subscribeOn(Schedulers.io())\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .subscribe(new MyObserver<Boolean>() {\n                        @Override\n                        public void onNext(@NonNull Boolean aBoolean) {\n                            RxBus.get().post(RxBusTag.HAD_REMOVE_BOOK, bookShelf);\n                            mView.setAdd(true);\n                            mView.finish();\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n\n                        }\n                    });\n        }\n    }\n\n    private Observable<String> getRealFilePath(final Context context, final Uri uri) {\n        return Observable.create(e -> {\n            String data = \"\";\n            if (null != uri) {\n                final String scheme = uri.getScheme();\n                if (scheme == null)\n                    data = uri.getPath();\n                else if (ContentResolver.SCHEME_FILE.equals(scheme)) {\n                    data = uri.getPath();\n                } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {\n                    Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null);\n                    if (null != cursor) {\n                        if (cursor.moveToFirst()) {\n                            int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);\n                            if (index > -1) {\n                                data = cursor.getString(index);\n                            }\n                        }\n                        cursor.close();\n                    }\n\n                    if ((data == null || data.length() <= 0) && uri.getPath() != null && uri.getPath().contains(\"/storage/emulated/\")) {\n                        data = uri.getPath().substring(uri.getPath().indexOf(\"/storage/emulated/\"));\n                    }\n                }\n            }\n            e.onNext(data == null ? \"\" : data);\n            e.onComplete();\n        });\n    }\n\n    @Override\n    public BookSourceBean getBookSource() {\n        if (bookShelf != null) {\n            return BookSourceManager.getBookSourceByUrl(bookShelf.getTag());\n        }\n        return null;\n    }\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n        RxBus.get().register(this);\n    }\n\n    /////////////////////////////////////////////////\n\n    @Override\n    public void detachView() {\n        if (changeSourceHelp != null) {\n            changeSourceHelp.stopSearch();\n        }\n        RxBus.get().unregister(this);\n    }\n\n    /////////////////////RxBus////////////////////////\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.MEDIA_BUTTON)})\n    public void onMediaButton(String command) {\n        if (bookShelf != null) {\n            mView.onMediaButton(command);\n        }\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.UPDATE_READ)})\n    public void updateRead(Boolean recreate) {\n        mView.refresh(recreate);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.ALOUD_STATE)})\n    public void upAloudState(ReadAloudService.Status state) {\n        mView.upAloudState(state);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.ALOUD_TIMER)})\n    public void upAloudTimer(String timer) {\n        mView.upAloudTimer(timer);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.SKIP_TO_CHAPTER)})\n    public void skipToChapter(OpenChapterBean openChapterBean) {\n        mView.skipToChapter(openChapterBean.getChapterIndex(), openChapterBean.getPageIndex());\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.OPEN_BOOK_MARK)})\n    public void openBookmark(BookmarkBean bookmarkBean) {\n        mView.showBookmark(bookmarkBean);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.READ_ALOUD_START)})\n    public void readAloudStart(Integer start) {\n        mView.readAloudStart(start);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.READ_ALOUD_NUMBER)})\n    public void readAloudLength(Integer start) {\n        mView.readAloudLength(start);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.RECREATE)})\n    public void recreate(Boolean recreate) {\n        mView.recreate();\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.AUDIO_SIZE)})\n    public void upAudioSize(Integer audioSize) {\n        mView.upAudioSize(audioSize);\n        BookChapterBean bean = chapterBeanList.get(bookShelf.getDurChapter());\n        bean.setEnd(Long.valueOf(audioSize));\n        DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplace(bean);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.AUDIO_DUR)})\n    public void upAudioDur(Integer audioDur) {\n        mView.upAudioDur(audioDur);\n    }\n\n    public interface OnAddListener {\n        void addSuccess();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/ReplaceRulePresenter.java",
    "content": "package com.kunfei.bookshelf.presenter;\n\nimport android.graphics.Color;\n\nimport androidx.documentfile.provider.DocumentFile;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\nimport com.kunfei.bookshelf.help.DocumentHelper;\nimport com.kunfei.bookshelf.model.ReplaceRuleManager;\nimport com.kunfei.bookshelf.presenter.contract.ReplaceRuleContract;\n\nimport java.io.File;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\nimport static android.text.TextUtils.isEmpty;\n\n/**\n * Created by GKF on 2017/12/18.\n * 书源管理\n */\n\npublic class ReplaceRulePresenter extends BasePresenterImpl<ReplaceRuleContract.View> implements ReplaceRuleContract.Presenter {\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n    }\n\n    @Override\n    public void saveData(List<ReplaceRuleBean> replaceRuleBeans) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            int i = 0;\n            for (ReplaceRuleBean replaceRuleBean : replaceRuleBeans) {\n                i++;\n                replaceRuleBean.setSerialNumber(i + 1);\n            }\n            ReplaceRuleManager.addDataS(replaceRuleBeans);\n            e.onNext(true);\n            e.onComplete();\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe();\n    }\n\n    @Override\n    public void delData(ReplaceRuleBean replaceRuleBean) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            ReplaceRuleManager.delData(replaceRuleBean);\n            e.onNext(true);\n            e.onComplete();\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean replaceRuleBeans) {\n                        mView.refresh();\n                        mView.getSnackBar(replaceRuleBean.getReplaceSummary() + \"已删除\", Snackbar.LENGTH_LONG)\n                                .setAction(\"恢复\", view -> restoreData(replaceRuleBean))\n                                .setActionTextColor(Color.WHITE)\n                                .show();\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n\n                    }\n                });\n    }\n\n    @Override\n    public void delData(List<ReplaceRuleBean> replaceRuleBeans) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            ReplaceRuleManager.delDataS(replaceRuleBeans);\n            e.onNext(true);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean aBoolean) {\n                        mView.toast(\"删除成功\");\n                        mView.refresh();\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(\"删除失败\");\n                    }\n                });\n    }\n\n    private void restoreData(ReplaceRuleBean replaceRuleBean) {\n        ReplaceRuleManager.saveData(replaceRuleBean)\n                .subscribe(new MySingleObserver<Boolean>() {\n                    @Override\n                    public void onSuccess(Boolean aBoolean) {\n                        mView.refresh();\n                    }\n                });\n    }\n\n    @Override\n    public void importDataSLocal(String path) {\n        String json;\n        DocumentFile file = DocumentFile.fromFile(new File(path));\n        json = DocumentHelper.readString(file);\n        if (!isEmpty(json)) {\n            importDataS(json);\n        } else {\n            mView.toast(\"文件读取失败\");\n        }\n    }\n\n    @Override\n    public void importDataS(String text) {\n        Observable<Boolean> observable = ReplaceRuleManager.importReplaceRule(text);\n        if (observable != null) {\n            observable.subscribe(new MyObserver<Boolean>() {\n                @Override\n                public void onNext(Boolean aBoolean) {\n                    mView.refresh();\n                    mView.toast(\"导入成功\");\n                }\n\n                @Override\n                public void onError(Throwable e) {\n                    mView.toast(\"格式不对\");\n                }\n            });\n        } else {\n            mView.toast(\"导入失败\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/SearchBookPresenter.java",
    "content": "package com.kunfei.bookshelf.presenter;\n\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.bean.SearchHistoryBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.dao.SearchHistoryBeanDao;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.SearchBookModel;\nimport com.kunfei.bookshelf.presenter.contract.SearchBookContract;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class SearchBookPresenter extends BasePresenterImpl<SearchBookContract.View> implements SearchBookContract.Presenter {\n    private static final int BOOK = 2;\n\n    private long startThisSearchTime;\n    private String durSearchKey;\n    private List<BookShelfBean> bookShelfS = new ArrayList<>();   //用来比对搜索的书籍是否已经添加进书架\n    private SearchBookModel searchBookModel;\n\n    public SearchBookPresenter() {\n        Observable.create((ObservableOnSubscribe<List<BookShelfBean>>) e -> {\n            List<BookShelfBean> booAll = BookshelfHelp.getAllBook();\n            e.onNext(booAll == null ? new ArrayList<>() : booAll);\n            e.onComplete();\n        }).subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<List<BookShelfBean>>() {\n                    @Override\n                    public void onNext(List<BookShelfBean> value) {\n                        bookShelfS.addAll(value);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                    }\n                });\n\n        //搜索监听\n        SearchBookModel.OnSearchListener onSearchListener = new SearchBookModel.OnSearchListener() {\n            @Override\n            public void refreshSearchBook() {\n                mView.refreshSearchBook();\n            }\n\n            @Override\n            public void refreshFinish(Boolean value) {\n                mView.refreshFinish(value);\n            }\n\n            @Override\n            public void loadMoreFinish(Boolean value) {\n                mView.loadMoreFinish(value);\n            }\n\n            @Override\n            public void loadMoreSearchBook(List<SearchBookBean> value) {\n                mView.loadMoreSearchBook(value);\n            }\n\n            @Override\n            public void searchBookError(Throwable throwable) {\n                mView.searchBookError(throwable);\n            }\n\n            @Override\n            public int getItemCount() {\n                return mView.getSearchBookAdapter().getICount();\n            }\n        };\n        //搜索引擎初始化\n        if (MApplication.SEARCH_GROUP != null) {\n            List<BookSourceBean> sourceBeanList = BookSourceManager.getEnableSourceByGroup(MApplication.SEARCH_GROUP);\n            if (sourceBeanList.size() > 0) {\n                searchBookModel = new SearchBookModel(onSearchListener, sourceBeanList);\n            } else {\n                searchBookModel = new SearchBookModel(onSearchListener);\n            }\n        } else {\n            searchBookModel = new SearchBookModel(onSearchListener);\n        }\n    }\n\n    /**\n     * 插入搜索历史\n     */\n    public void insertSearchHistory() {\n        final int type = SearchBookPresenter.BOOK;\n        final String content = mView.getEdtContent().getText().toString().trim();\n        Observable.create((ObservableOnSubscribe<SearchHistoryBean>) e -> {\n            List<SearchHistoryBean> data = DbHelper.getDaoSession().getSearchHistoryBeanDao()\n                    .queryBuilder()\n                    .where(SearchHistoryBeanDao.Properties.Type.eq(type), SearchHistoryBeanDao.Properties.Content.eq(content))\n                    .limit(1)\n                    .build().list();\n            SearchHistoryBean searchHistoryBean;\n            if (null != data && data.size() > 0) {\n                searchHistoryBean = data.get(0);\n                searchHistoryBean.setDate(System.currentTimeMillis());\n                DbHelper.getDaoSession().getSearchHistoryBeanDao().update(searchHistoryBean);\n            } else {\n                searchHistoryBean = new SearchHistoryBean(type, content, System.currentTimeMillis());\n                DbHelper.getDaoSession().getSearchHistoryBeanDao().insert(searchHistoryBean);\n            }\n            e.onNext(searchHistoryBean);\n        }).subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<SearchHistoryBean>() {\n                    @Override\n                    public void onNext(SearchHistoryBean value) {\n                        mView.insertSearchHistorySuccess(value);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                    }\n                });\n    }\n\n    @Override\n    public void cleanSearchHistory() {\n        final String content = mView.getEdtContent().getText().toString().trim();\n        Observable.create((ObservableOnSubscribe<Integer>) e -> {\n            int a = DbHelper.getDb().delete(SearchHistoryBeanDao.TABLENAME,\n                    SearchHistoryBeanDao.Properties.Type.columnName + \"=? and \" + SearchHistoryBeanDao.Properties.Content.columnName + \" like ?\",\n                    new String[]{String.valueOf(SearchBookPresenter.BOOK), \"%\" + content + \"%\"});\n            e.onNext(a);\n        }).subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Integer>() {\n                    @Override\n                    public void onNext(Integer value) {\n                        if (value > 0) {\n                            mView.querySearchHistorySuccess(null);\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                    }\n                });\n    }\n\n    @Override\n    public void cleanSearchHistory(SearchHistoryBean searchHistoryBean) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            DbHelper.getDaoSession().getSearchHistoryBeanDao().delete(searchHistoryBean);\n            e.onNext(true);\n            e.onComplete();\n        }).subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean value) {\n                        if (value) {\n                            querySearchHistory(mView.getEdtContent().getText().toString().trim());\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        e.printStackTrace();\n                    }\n                });\n    }\n\n    @Override\n    public void querySearchHistory(String content) {\n        Observable.create((ObservableOnSubscribe<List<SearchHistoryBean>>) e -> {\n            List<SearchHistoryBean> data = DbHelper.getDaoSession().getSearchHistoryBeanDao()\n                    .queryBuilder()\n                    .where(SearchHistoryBeanDao.Properties.Type.eq(SearchBookPresenter.BOOK), SearchHistoryBeanDao.Properties.Content.like(\"%\" + content + \"%\"))\n                    .orderDesc(SearchHistoryBeanDao.Properties.Date)\n                    .limit(50)\n                    .build().list();\n            e.onNext(data);\n        }).subscribeOn(Schedulers.newThread())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<List<SearchHistoryBean>>() {\n                    @Override\n                    public void onNext(List<SearchHistoryBean> value) {\n                        if (null != value)\n                            mView.querySearchHistorySuccess(value);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n\n                    }\n                });\n    }\n\n    @Override\n    public int getPage() {\n        return searchBookModel.getPage();\n    }\n\n    @Override\n    public void initPage() {\n        searchBookModel.setPage(0);\n    }\n\n    /**\n     * 搜索\n     */\n    @Override\n    public void toSearchBooks(String key, Boolean fromError) {\n        if (key != null) {\n            durSearchKey = key;\n            startThisSearchTime = System.currentTimeMillis();\n            searchBookModel.setSearchTime(startThisSearchTime);\n            searchBookModel.searchReNew();\n        }\n        searchBookModel.search(durSearchKey, startThisSearchTime, bookShelfS, fromError);\n    }\n\n    @Override\n    public void initSearchEngineS(String group) {\n        if (TextUtils.isEmpty(group)) {\n            searchBookModel.initSearchEngineS(BookSourceManager.getSelectedBookSource());\n        } else {\n            searchBookModel.initSearchEngineS(BookSourceManager.getEnableSourceByGroup(group));\n        }\n    }\n\n    /**\n     * 停止搜索\n     */\n    @Override\n    public void stopSearch() {\n        searchBookModel.stopSearch();\n    }\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n        searchBookModel.onDestroy();\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.SEARCH_BOOK)})\n    public void searchBook(String searchKey) {\n        mView.searchBook(searchKey);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/SourceEditPresenter.java",
    "content": "package com.kunfei.bookshelf.presenter;\n\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.text.TextUtils;\n\nimport androidx.annotation.NonNull;\n\nimport com.google.gson.Gson;\nimport com.google.gson.reflect.TypeToken;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.bean.BookSource3Bean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.presenter.contract.SourceEditContract;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.util.List;\nimport java.util.Objects;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\n\n/**\n * Created by GKF on 2018/1/28.\n * 编辑书源\n */\npublic class SourceEditPresenter extends BasePresenterImpl<SourceEditContract.View> implements SourceEditContract.Presenter {\n\n    @Override\n    public Observable<Boolean> saveSource(BookSourceBean bookSource, BookSourceBean bookSourceOld) {\n        return Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            if (!TextUtils.isEmpty(bookSourceOld.getBookSourceUrl()) && !Objects.equals(bookSource.getBookSourceUrl(), bookSourceOld.getBookSourceUrl())) {\n                DbHelper.getDaoSession().getBookSourceBeanDao().delete(bookSourceOld);\n            }\n            BookSourceManager.addBookSource(bookSource);\n            e.onNext(true);\n        }).compose(RxUtils::toSimpleSingle);\n    }\n\n    @Override\n    public void copySource(String bookSource) {\n        ClipboardManager clipboard = (ClipboardManager) mView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n        ClipData clipData = ClipData.newPlainText(null, bookSource);\n        if (clipboard != null) {\n            clipboard.setPrimaryClip(clipData);\n        }\n    }\n\n    @Override\n    public void pasteSource() {\n        ClipboardManager clipboard = (ClipboardManager) mView.getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n        ClipData clipData = clipboard != null ? clipboard.getPrimaryClip() : null;\n        if (clipData != null && clipData.getItemCount() > 0) {\n            setText(String.valueOf(clipData.getItemAt(0).getText()));\n        }\n    }\n\n    @Override\n    public void setText(String bookSourceStr) {\n        try {\n            if (bookSourceStr.trim().length() > 5) {\n                mView.setText( mathcSourceBean(bookSourceStr.trim())  );\n            } else {\n                mView.toast(\"似乎不是书源内容\");\n                // 理论上这里已经没用了\n//                Gson gson = new Gson();\n//                BookSourceBean bookSourceBean = gson.fromJson(bookSourceStr, BookSourceBean.class);\n//                mView.setText(bookSourceBean);\n            }\n        } catch (Exception e) {\n            mView.toast(\"数据格式不对\");\n            e.printStackTrace();\n        }\n    }\n\n    private BookSourceBean mathcSourceBean(String str) {\n        Gson gson = new Gson();\n        BookSource3Bean bookSource3Bean=new BookSource3Bean();\n        BookSourceBean bookSource2Bean=new BookSourceBean();\n        int r2 = 0, r3 = 0;\n        try {\n            if (str.charAt(0) == '[' && str.charAt(str.length() - 1) == ']') {\n                List<BookSource3Bean> list = gson.fromJson(str, new TypeToken<List<BookSource3Bean>>() {\n                }.getType());\n                bookSource3Bean = list.get(0);\n            } else {\n                bookSource3Bean = gson.fromJson(str, BookSource3Bean.class);\n            }\n            r3 = gson.toJson(bookSource3Bean).length();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n\n        try {\n            if (str.charAt(0) == '[' && str.charAt(str.length() - 1) == ']') {\n                List<BookSourceBean> list = gson.fromJson(str, new TypeToken<List<BookSourceBean>>() {\n                }.getType());\n                bookSource2Bean = list.get(0);\n            } else {\n                bookSource2Bean = gson.fromJson(str, BookSourceBean.class);\n            }\n            r2 = gson.toJson(bookSource2Bean).length();\n            // r2 r3的计算在调用searchUrl2RuleSearchUrl() 等高级转换方法之前，是简化算法的粗糙的做法\n            if (r2 > r3)\n                return bookSource2Bean;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n\n        if (r3 > 0) {\n            mView.toast(\"导入了阅读3.0书源。如有Bug请及时上报\");\n            return bookSource3Bean.addGroupTag(\"阅读3.0书源\").toBookSourceBean();\n        }\n        return bookSource2Bean;\n    }\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        super.attachView(iView);\n    }\n\n    @Override\n    public void detachView() {\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/TxtChapterRulePresenter.java",
    "content": "package com.kunfei.bookshelf.presenter;\n\nimport android.graphics.Color;\n\nimport androidx.documentfile.provider.DocumentFile;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BasePresenterImpl;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.TxtChapterRuleBean;\nimport com.kunfei.bookshelf.help.DocumentHelper;\nimport com.kunfei.bookshelf.model.ReplaceRuleManager;\nimport com.kunfei.bookshelf.model.TxtChapterRuleManager;\nimport com.kunfei.bookshelf.presenter.contract.TxtChapterRuleContract;\n\nimport java.io.File;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\nimport static android.text.TextUtils.isEmpty;\n\npublic class TxtChapterRulePresenter extends BasePresenterImpl<TxtChapterRuleContract.View> implements TxtChapterRuleContract.Presenter {\n\n    @Override\n    public void detachView() {\n        RxBus.get().unregister(this);\n    }\n\n    @Override\n    public void saveData(List<TxtChapterRuleBean> txtChapterRuleBeans) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            int i = 0;\n            for (TxtChapterRuleBean ruleBean : txtChapterRuleBeans) {\n                i++;\n                ruleBean.setSerialNumber(i + 1);\n            }\n            DbHelper.getDaoSession().getTxtChapterRuleBeanDao().insertOrReplaceInTx(txtChapterRuleBeans);\n            e.onNext(true);\n            e.onComplete();\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe();\n    }\n\n    @Override\n    public void delData(TxtChapterRuleBean txtChapterRuleBean) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            TxtChapterRuleManager.del(txtChapterRuleBean);\n            e.onNext(true);\n            e.onComplete();\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean replaceRuleBeans) {\n                        mView.refresh();\n                        mView.getSnackBar(txtChapterRuleBean.getName() + \"已删除\", Snackbar.LENGTH_LONG)\n                                .setAction(\"恢复\", view -> restoreData(txtChapterRuleBean))\n                                .setActionTextColor(Color.WHITE)\n                                .show();\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n\n                    }\n                });\n    }\n\n    @Override\n    public void delData(List<TxtChapterRuleBean> txtChapterRuleBeans) {\n        Observable.create((ObservableOnSubscribe<Boolean>) e -> {\n            TxtChapterRuleManager.del(txtChapterRuleBeans);\n            e.onNext(true);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<Boolean>() {\n                    @Override\n                    public void onNext(Boolean aBoolean) {\n                        mView.toast(\"删除成功\");\n                        mView.refresh();\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        mView.toast(\"删除失败\");\n                    }\n                });\n    }\n\n    private void restoreData(TxtChapterRuleBean txtChapterRuleBean) {\n        TxtChapterRuleManager.save(txtChapterRuleBean);\n        mView.refresh();\n    }\n\n    @Override\n    public void importDataSLocal(String path) {\n        String json;\n        DocumentFile file = DocumentFile.fromFile(new File(path));\n        json = DocumentHelper.readString(file);\n        if (!isEmpty(json)) {\n            importDataS(json);\n        } else {\n            mView.toast(\"文件读取失败\");\n        }\n    }\n\n    @Override\n    public void importDataS(String text) {\n        Observable<Boolean> observable = ReplaceRuleManager.importReplaceRule(text);\n        if (observable != null) {\n            observable.subscribe(new MyObserver<Boolean>() {\n                @Override\n                public void onNext(Boolean aBoolean) {\n                    mView.refresh();\n                    mView.toast(\"导入成功\");\n                }\n\n                @Override\n                public void onError(Throwable e) {\n                    mView.toast(\"格式不对\");\n                }\n            });\n        } else {\n            mView.toast(\"导入失败\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookDetailContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport android.content.Intent;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\n\nimport java.util.List;\n\npublic interface BookDetailContract {\n    interface Presenter extends IPresenter {\n        void initData(Intent intent);\n\n        int getOpenFrom();\n\n        SearchBookBean getSearchBook();\n\n        BookShelfBean getBookShelf();\n\n        List<BookChapterBean> getChapterList();\n\n        Boolean getInBookShelf();\n\n        void initBookFormSearch(SearchBookBean searchBookBean);\n\n        void getBookShelfInfo();\n\n        void addToBookShelf();\n\n        void removeFromBookShelf();\n\n        void changeBookSource(SearchBookBean searchBookBean);\n    }\n\n    interface View extends IView {\n        /**\n         * 更新书籍详情UI\n         */\n        void updateView();\n\n        /**\n         * 数据获取失败\n         */\n        void getBookShelfError();\n\n        void readBook();\n\n        void finish();\n\n        void toast(String msg);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookListContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport android.content.SharedPreferences;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\n\nimport java.util.List;\n\npublic interface BookListContract {\n\n    interface View extends IView {\n\n        /**\n         * 刷新书架书籍小说信息 更新UI\n         *\n         * @param bookShelfBeanList 书架\n         */\n        void refreshBookShelf(List<BookShelfBean> bookShelfBeanList);\n\n        void refreshBook(String noteUrl);\n\n        SharedPreferences getPreferences();\n\n        /**\n         * 更新Group\n         */\n        void updateGroup(Integer group);\n\n    }\n\n    interface Presenter extends IPresenter {\n        void queryBookShelf(Boolean needRefresh, int group);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/BookSourceContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\n\nimport java.util.List;\n\npublic interface BookSourceContract {\n\n    interface Presenter extends IPresenter {\n\n        void saveData(BookSourceBean bookSourceBean);\n\n        void saveData(List<BookSourceBean> bookSourceBeans);\n\n        void delData(BookSourceBean bookSourceBean);\n\n        void delData(List<BookSourceBean> bookSourceBeans);\n\n        void importBookSource(String url);\n\n        void importBookSourceLocal(String path);\n\n        void checkBookSource(List<BookSourceBean> sourceBeans);\n\n        void checkFindSource(List<BookSourceBean> sourceBeans);\n    }\n\n    interface View extends IView {\n\n        void refreshBookSource();\n\n        Snackbar getSnackBar(String msg, int length);\n\n        void showSnackBar(String msg, int length);\n\n        void setResult(int resultCode);\n\n        int getSort();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/ChoiceBookContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\n\nimport java.util.List;\n\npublic interface ChoiceBookContract {\n    interface Presenter extends IPresenter {\n\n        int getPage();\n\n        void initPage();\n\n        void toSearchBooks(String key);\n\n        String getTitle();\n    }\n\n    interface View extends IView {\n\n        void refreshSearchBook(List<SearchBookBean> books);\n\n        void loadMoreSearchBook(List<SearchBookBean> books);\n\n        void refreshFinish(Boolean isAll);\n\n        void loadMoreFinish(Boolean isAll);\n\n        void searchBookError(String msg);\n\n        void addBookShelfFailed(String massage);\n\n        void startRefreshAnim();\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/FindBookContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData;\n\nimport java.util.List;\n\npublic interface FindBookContract {\n    interface Presenter extends IPresenter {\n\n        void initData();\n\n    }\n\n    interface View extends IView {\n\n        void upData(List<RecyclerViewData> group);\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/ImportBookContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\n\nimport java.io.File;\nimport java.util.List;\n\npublic interface ImportBookContract {\n\n    interface Presenter extends IPresenter {\n\n        void importBooks(List<File> books);\n\n    }\n\n    interface View extends IView {\n\n        /**\n         * 添加成功\n         */\n        void addSuccess();\n\n        /**\n         * 添加失败\n         */\n        void addError(String msg);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/MainContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\n\npublic interface MainContract {\n\n    interface View extends IView {\n\n        void initImmersionBar();\n\n        /**\n         * 取消弹出框\n         */\n        void dismissHUD();\n\n        /**\n         * 恢复数据\n         */\n        void onRestore(String msg);\n\n        void recreate();\n\n        void toast(String msg);\n\n        void toast(int strId);\n\n        int getGroup();\n    }\n\n    interface Presenter extends IPresenter {\n\n        void addBookUrl(String bookUrl);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/ReadBookContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport android.app.Activity;\nimport android.content.Intent;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.BookmarkBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.presenter.ReadBookPresenter;\nimport com.kunfei.bookshelf.service.ReadAloudService;\n\nimport java.util.List;\n\npublic interface ReadBookContract {\n    interface View extends IView {\n\n        String getNoteUrl();\n\n        Boolean getAdd();\n\n        void setAdd(Boolean isAdd);\n\n        void changeSourceFinish(BookShelfBean book);\n\n        void startLoadingBook();\n\n        void upMenu();\n\n        void openBookFromOther();\n\n        void showBookmark(BookmarkBean bookmarkBean);\n\n        void skipToChapter(int chapterIndex, int pageIndex);\n\n        void onMediaButton(String cmd);\n\n        void upAloudState(ReadAloudService.Status state);\n\n        void upAloudTimer(String timer);\n\n        void readAloudStart(int start);\n\n        void readAloudLength(int readAloudLength);\n\n        void refresh(boolean recreate);\n\n        void finish();\n\n        void recreate();\n\n        void upAudioSize(int audioSize);\n\n        void upAudioDur(int audioDur);\n    }\n\n    interface Presenter extends IPresenter {\n\n        void loadBook(Intent intent);\n\n        BookShelfBean getBookShelf();\n\n        List<BookChapterBean> getChapterList();\n\n        BookChapterBean getDurChapter();\n\n        void setChapterList(List<BookChapterBean> chapterList);\n\n        void saveBook();\n\n        void saveProgress();\n\n        void addToShelf(final ReadBookPresenter.OnAddListener Listener);\n\n        void removeFromShelf();\n\n        void initData(Activity activity);\n\n        void openBookFromOther(Activity activity);\n\n        void addDownload(int start, int end);\n\n        void changeBookSource(SearchBookBean searchBookBean);\n\n        void autoChangeSource();\n\n        void saveBookmark(BookmarkBean bookmarkBean);\n\n        void delBookmark(BookmarkBean bookmarkBean);\n\n        void disableDurBookSource();\n\n        BookSourceBean getBookSource();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/ReplaceRuleContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\n\nimport java.util.List;\n\npublic interface ReplaceRuleContract {\n    interface Presenter extends IPresenter {\n\n        void saveData(List<ReplaceRuleBean> replaceRuleBeans);\n\n        void delData(ReplaceRuleBean replaceRuleBean);\n\n        void delData(List<ReplaceRuleBean> replaceRuleBeans);\n\n        void importDataSLocal(String uri);\n\n        void importDataS(String text);\n    }\n\n    interface View extends IView {\n\n        void refresh();\n\n        Snackbar getSnackBar(String msg, int length);\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/SearchBookContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport android.widget.EditText;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.bean.SearchHistoryBean;\nimport com.kunfei.bookshelf.view.adapter.SearchBookAdapter;\n\nimport java.util.List;\n\npublic interface SearchBookContract {\n    interface Presenter extends IPresenter {\n\n        void insertSearchHistory();\n\n        void querySearchHistory(String content);\n\n        void cleanSearchHistory();\n\n        void cleanSearchHistory(SearchHistoryBean searchHistoryBean);\n\n        int getPage();\n\n        void initPage();\n\n        void toSearchBooks(String key, Boolean fromError);\n\n        void stopSearch();\n\n        void initSearchEngineS(String group);\n    }\n\n    interface View extends IView {\n\n        void searchBook(String searchKey);\n\n        /**\n         * 成功 新增查询记录\n         */\n        void insertSearchHistorySuccess(SearchHistoryBean searchHistoryBean);\n\n        /**\n         * 成功搜索 搜索记录\n         */\n        void querySearchHistorySuccess(List<SearchHistoryBean> datas);\n\n        /**\n         * 首次查询成功 更新UI\n         */\n        void refreshSearchBook();\n\n        /**\n         * 加载更多书籍成功 更新UI\n         */\n        void loadMoreSearchBook(List<SearchBookBean> books);\n\n        /**\n         * 刷新成功\n         */\n        void refreshFinish(Boolean isAll);\n\n        /**\n         * 加载成功\n         */\n        void loadMoreFinish(Boolean isAll);\n\n        /**\n         * 搜索失败\n         */\n        void searchBookError(Throwable throwable);\n\n        /**\n         * 获取搜索内容EditText\n         */\n        EditText getEdtContent();\n\n        /**\n         * @return SearchBookAdapter\n         */\n        SearchBookAdapter getSearchBookAdapter();\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/SourceEditContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\n\nimport io.reactivex.Observable;\n\npublic interface SourceEditContract {\n    interface Presenter extends IPresenter {\n\n        Observable<Boolean> saveSource(BookSourceBean bookSource, BookSourceBean bookSourceOld);\n\n        void copySource(String bookSource);\n\n        void pasteSource();\n\n        void setText(String bookSourceStr);\n    }\n\n    interface View extends IView {\n\n        void setText(BookSourceBean bookSourceBean);\n\n        String getBookSourceStr(boolean hasFind);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/presenter/contract/TxtChapterRuleContract.java",
    "content": "package com.kunfei.bookshelf.presenter.contract;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.kunfei.bookshelf.bean.TxtChapterRuleBean;\n\nimport java.util.List;\n\npublic interface TxtChapterRuleContract {\n    interface Presenter extends IPresenter {\n\n        void saveData(List<TxtChapterRuleBean> txtChapterRuleBeans);\n\n        void delData(TxtChapterRuleBean txtChapterRuleBean);\n\n        void delData(List<TxtChapterRuleBean> txtChapterRuleBeans);\n\n        void importDataSLocal(String uri);\n\n        void importDataS(String text);\n    }\n\n    interface View extends IView {\n\n        void refresh();\n\n        Snackbar getSnackBar(String msg, int length);\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/service/CheckSourceService.java",
    "content": "package com.kunfei.bookshelf.service;\n\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.os.IBinder;\n\nimport androidx.annotation.Nullable;\nimport androidx.core.app.NotificationCompat;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.model.task.CheckSourceTask;\nimport com.kunfei.bookshelf.view.activity.BookSourceActivity;\n\nimport java.util.List;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\nimport io.reactivex.Scheduler;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService;\nimport static com.kunfei.bookshelf.constant.AppConstant.ActionStartService;\n\npublic class CheckSourceService extends Service {\n    private static final int notificationId = 3333;\n    private static final String ActionOpenActivity = \"openActivity\";\n\n    private List<BookSourceBean> bookSourceBeanList;\n    private int threadsNum;\n    private int checkIndex;\n    private CompositeDisposable compositeDisposable;\n    private ExecutorService executorService;\n    private Scheduler scheduler;\n    private CheckSourceListener checkSourceListener;\n\n    /**\n     * 启动服务\n     */\n    public static void start(Context context, List<BookSourceBean> sourceBeans) {\n        if (sourceBeans.isEmpty()) return;\n        String key = String.valueOf(System.currentTimeMillis());\n        BitIntentDataManager.getInstance().putData(key, sourceBeans);\n        Intent intent = new Intent(context, CheckSourceService.class);\n        intent.putExtra(\"data_key\", key);\n        intent.setAction(ActionStartService);\n        context.startService(intent);\n    }\n\n    /**\n     * 停止服务\n     */\n    public static void stop(Context context) {\n        Intent intent = new Intent(context, CheckSourceService.class);\n        intent.setAction(ActionDoneService);\n        context.startService(intent);\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        SharedPreferences preference = MApplication.getConfigPreferences();\n        checkSourceListener = new CheckSourceListener() {\n            @Override\n            public void nextCheck() {\n                CheckSourceService.this.nextCheck();\n            }\n\n            @Override\n            public void compositeDisposableAdd(Disposable disposable) {\n                compositeDisposable.add(disposable);\n            }\n\n            @Override\n            public int getCheckIndex() {\n                return checkIndex;\n            }\n        };\n        threadsNum = preference.getInt(this.getString(R.string.pk_threads_num), 6);\n        executorService = Executors.newFixedThreadPool(threadsNum);\n        scheduler = Schedulers.from(executorService);\n        compositeDisposable = new CompositeDisposable();\n        updateNotification(0, \"正在加载\");\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        if (intent != null) {\n            String action = intent.getAction();\n            if (action != null) {\n                switch (action) {\n                    case ActionDoneService:\n                        doneService();\n                        break;\n                    case ActionStartService:\n                        String key = intent.getStringExtra(\"data_key\");\n                        bookSourceBeanList = (List<BookSourceBean>) BitIntentDataManager.getInstance().getData(key);\n                        startCheck();\n                }\n            }\n        }\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        executorService.shutdown();\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return null;\n    }\n\n    private void doneService() {\n        RxBus.get().post(RxBusTag.CHECK_SOURCE_FINISH, \"校验完成\");\n        compositeDisposable.dispose();\n        stopSelf();\n    }\n\n    /**\n     * 更新通知\n     */\n    private void updateNotification(int state, String msg) {\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdReadAloud)\n                .setSmallIcon(R.drawable.ic_network_check)\n                .setOngoing(true)\n                .setContentTitle(getString(R.string.check_book_source))\n                .setContentText(msg)\n                .setContentIntent(getActivityPendingIntent());\n        builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getThisServicePendingIntent());\n        if (bookSourceBeanList != null) {\n            builder.setProgress(bookSourceBeanList.size(), state, false);\n        }\n        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);\n        Notification notification = builder.build();\n        startForeground(notificationId, notification);\n    }\n\n    private PendingIntent getActivityPendingIntent() {\n        Intent intent = new Intent(this, BookSourceActivity.class);\n        intent.setAction(CheckSourceService.ActionOpenActivity);\n        return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    private PendingIntent getThisServicePendingIntent() {\n        Intent intent = new Intent(this, this.getClass());\n        intent.setAction(ActionDoneService);\n        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    public void startCheck() {\n        if (bookSourceBeanList != null && bookSourceBeanList.size() > 0) {\n            RxBus.get().post(RxBusTag.CHECK_SOURCE_STATE, \"开始效验\");\n            checkIndex = -1;\n            for (int i = 1; i <= threadsNum; i++) {\n                nextCheck();\n            }\n        }\n    }\n\n    private synchronized void nextCheck() {\n        checkIndex++;\n        if (checkIndex > threadsNum) {\n            String msg = String.format(getString(R.string.progress_show), checkIndex - threadsNum, bookSourceBeanList.size());\n            RxBus.get().post(RxBusTag.CHECK_SOURCE_STATE, msg);\n            updateNotification(checkIndex - threadsNum, msg);\n        }\n\n        if (checkIndex < bookSourceBeanList.size()) {\n            CheckSourceTask checkSource = new CheckSourceTask(bookSourceBeanList.get(checkIndex), scheduler, checkSourceListener);\n            checkSource.startCheck();\n        } else {\n            if (checkIndex >= bookSourceBeanList.size() + threadsNum - 1) {\n                doneService();\n            }\n        }\n    }\n\n    public interface CheckSourceListener {\n        void nextCheck();\n\n        void compositeDisposableAdd(Disposable disposable);\n\n        int getCheckIndex();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/service/DownloadService.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.service;\n\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.graphics.BitmapFactory;\nimport android.os.Handler;\nimport android.os.IBinder;\nimport android.os.Looper;\nimport android.text.TextUtils;\nimport android.util.SparseArray;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\nimport androidx.core.app.NotificationCompat;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.DownloadBookBean;\nimport com.kunfei.bookshelf.bean.DownloadChapterBean;\nimport com.kunfei.bookshelf.model.impl.IDownloadTask;\nimport com.kunfei.bookshelf.model.task.DownloadTaskImpl;\nimport com.kunfei.bookshelf.view.activity.DownloadActivity;\n\nimport java.util.ArrayList;\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\nimport io.reactivex.Scheduler;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class DownloadService extends Service {\n    public static final String cancelAction = \"cancelAction\";\n    public static final String addDownloadAction = \"addDownload\";\n    public static final String removeDownloadAction = \"removeDownloadAction\";\n    public static final String progressDownloadAction = \"progressDownloadAction\";\n    public static final String obtainDownloadListAction = \"obtainDownloadListAction\";\n    public static final String finishDownloadAction = \"finishDownloadAction\";\n    private int notificationId = 19901122;\n    private int downloadTaskId = 0;\n    private long currentTime;\n\n    public static boolean isRunning = false;\n\n    private ExecutorService executor;\n    private Scheduler scheduler;\n    private int threadsNum;\n    private Handler handler = new Handler(Looper.getMainLooper());\n\n    private SparseArray<IDownloadTask> downloadTasks = new SparseArray<>();\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        isRunning = true;\n        //创建 Notification.Builder 对象\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdDownload)\n                .setSmallIcon(R.drawable.ic_download)\n                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))\n                .setOngoing(false)\n                .setContentTitle(getString(R.string.download_offline_t))\n                .setContentText(getString(R.string.download_offline_s));\n        //发送通知\n        Notification notification = builder.build();\n        startForeground(notificationId, notification);\n\n        SharedPreferences preferences = getSharedPreferences(\"CONFIG\", 0);\n        threadsNum = preferences.getInt(this.getString(R.string.pk_threads_num), 4);\n        executor = Executors.newFixedThreadPool(threadsNum);\n        scheduler = Schedulers.from(executor);\n    }\n\n    @Override\n    public void onDestroy() {\n        cancelDownload();\n        isRunning = false;\n        executor.shutdown();\n        stopForeground(true);\n        super.onDestroy();\n    }\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        if (intent != null) {\n            String action = intent.getAction();\n            if (action == null) {\n                finishSelf();\n            } else {\n                switch (action) {\n                    case addDownloadAction:\n                        DownloadBookBean downloadBook = intent.getParcelableExtra(\"downloadBook\");\n                        if (downloadBook != null) {\n                            addDownload(downloadBook);\n                        }\n                        break;\n                    case removeDownloadAction:\n                        String noteUrl = intent.getStringExtra(\"noteUrl\");\n                        removeDownload(noteUrl);\n                        break;\n                    case cancelAction:\n                        finishSelf();\n                        break;\n                    case obtainDownloadListAction:\n                        refreshDownloadList();\n                        break;\n\n                }\n            }\n        }\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return null;\n    }\n\n    private synchronized void addDownload(DownloadBookBean downloadBook) {\n        if (checkDownloadTaskExist(downloadBook)) {\n            return;\n        }\n        downloadTaskId++;\n        new DownloadTaskImpl(downloadTaskId, downloadBook) {\n            @Override\n            public void onDownloadPrepared(DownloadBookBean downloadBook) {\n                if (canStartNextTask()) {\n                    startDownload(scheduler);\n                }\n                downloadTasks.put(getId(), this);\n                sendUpDownloadBook(addDownloadAction, downloadBook);\n            }\n\n            @Override\n            public void onDownloadProgress(DownloadChapterBean chapterBean) {\n                isProgress(chapterBean);\n            }\n\n            @Override\n            public void onDownloadChange(DownloadBookBean downloadBook) {\n                sendUpDownloadBook(progressDownloadAction, downloadBook);\n            }\n\n            @Override\n            public void onDownloadError(DownloadBookBean downloadBook) {\n                if (downloadTasks.indexOfValue(this) >= 0) {\n                    downloadTasks.remove(getId());\n                }\n\n                toast(String.format(Locale.getDefault(), \"%s：下载失败\", downloadBook.getName()));\n\n                startNextTaskAfterRemove(downloadBook);\n            }\n\n            @Override\n            public void onDownloadComplete(DownloadBookBean downloadBook) {\n                if (downloadTasks.indexOfValue(this) >= 0) {\n                    downloadTasks.remove(getId());\n                }\n                startNextTaskAfterRemove(downloadBook);\n            }\n        };\n    }\n\n    private void cancelDownload() {\n        for (int i = downloadTasks.size() - 1; i >= 0; i--) {\n            IDownloadTask downloadTask = downloadTasks.valueAt(i);\n            downloadTask.stopDownload();\n        }\n    }\n\n    private void removeDownload(String noteUrl) {\n        if (noteUrl == null) {\n            return;\n        }\n\n        for (int i = downloadTasks.size() - 1; i >= 0; i--) {\n            IDownloadTask downloadTask = downloadTasks.valueAt(i);\n            DownloadBookBean downloadBook = downloadTask.getDownloadBook();\n            if (downloadBook != null && TextUtils.equals(noteUrl, downloadBook.getNoteUrl())) {\n                downloadTask.stopDownload();\n                break;\n            }\n        }\n    }\n\n    private void refreshDownloadList() {\n        ArrayList<DownloadBookBean> downloadBookBeans = new ArrayList<>();\n        for (int i = downloadTasks.size() - 1; i >= 0; i--) {\n            IDownloadTask downloadTask = downloadTasks.valueAt(i);\n            DownloadBookBean downloadBook = downloadTask.getDownloadBook();\n            if (downloadBook != null) {\n                downloadBookBeans.add(downloadBook);\n            }\n        }\n        if (!downloadBookBeans.isEmpty()) {\n            sendUpDownloadBooks(downloadBookBeans);\n        }\n    }\n\n    private void startNextTaskAfterRemove(DownloadBookBean downloadBook) {\n        sendUpDownloadBook(removeDownloadAction, downloadBook);\n        handler.postDelayed(() -> {\n            if (downloadTasks.size() == 0) {\n                finishSelf();\n            } else {\n                startNextTask();\n            }\n        }, 1000);\n    }\n\n    private void startNextTask() {\n        if (!canStartNextTask()) {\n            return;\n        }\n        for (int i = 0; i < downloadTasks.size(); i++) {\n            IDownloadTask downloadTask = downloadTasks.valueAt(i);\n            if (!downloadTask.isDownloading()) {\n                downloadTask.startDownload(scheduler);\n                break;\n            }\n        }\n    }\n\n\n    private boolean canStartNextTask() {\n        int downloading = 0;\n        for (int i = downloadTasks.size() - 1; i >= 0; i--) {\n            IDownloadTask downloadTask = downloadTasks.valueAt(i);\n            if (downloadTask.isDownloading()) {\n                downloading += 1;\n            }\n        }\n        return downloading < threadsNum;\n    }\n\n\n    private synchronized boolean checkDownloadTaskExist(DownloadBookBean downloadBook) {\n        for (int i = downloadTasks.size() - 1; i >= 0; i--) {\n            IDownloadTask downloadTask = downloadTasks.valueAt(i);\n            if (Objects.equals(downloadTask.getDownloadBook(), downloadBook)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n\n    private void sendUpDownloadBook(String action, DownloadBookBean downloadBook) {\n        Intent intent = new Intent(action);\n        intent.putExtra(\"downloadBook\", downloadBook);\n        sendBroadcast(intent);\n    }\n\n    private void sendUpDownloadBooks(ArrayList<DownloadBookBean> downloadBooks) {\n        Intent intent = new Intent(obtainDownloadListAction);\n        intent.putParcelableArrayListExtra(\"downloadBooks\", downloadBooks);\n        sendBroadcast(intent);\n    }\n\n    private void toast(String msg) {\n        Toast.makeText(DownloadService.this, msg, Toast.LENGTH_SHORT).show();\n    }\n\n    private PendingIntent getChancelPendingIntent() {\n        Intent intent = new Intent(this, DownloadService.class);\n        intent.setAction(DownloadService.cancelAction);\n        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    private synchronized void isProgress(DownloadChapterBean downloadChapterBean) {\n        if (!isRunning) {\n            return;\n        }\n\n        if (System.currentTimeMillis() - currentTime < 1000) {//更新太快无法取消\n            return;\n        }\n\n        currentTime = System.currentTimeMillis();\n\n        Intent mainIntent = new Intent(this, DownloadActivity.class);\n        PendingIntent mainPendingIntent = PendingIntent.getActivity(this, 0, mainIntent, PendingIntent.FLAG_UPDATE_CURRENT);\n        //创建 Notification.Builder 对象\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdDownload)\n                .setSmallIcon(R.drawable.ic_download)\n                //通知栏大图标\n                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))\n                //点击通知后自动清除\n                .setAutoCancel(true)\n                .setContentTitle(\"正在下载：\" + downloadChapterBean.getBookName())\n                .setContentText(downloadChapterBean.getDurChapterName() == null ? \"  \" : downloadChapterBean.getDurChapterName())\n                .setContentIntent(mainPendingIntent);\n        builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getChancelPendingIntent());\n        //发送通知\n        startForeground(notificationId, builder.build());\n    }\n\n    private void finishSelf() {\n        sendBroadcast(new Intent(finishDownloadAction));\n        stopSelf();\n    }\n\n    public static void addDownload(Context context, DownloadBookBean downloadBook) {\n        if (context == null || downloadBook == null) {\n            return;\n        }\n        Intent intent = new Intent(context, DownloadService.class);\n        intent.setAction(addDownloadAction);\n        intent.putExtra(\"downloadBook\", downloadBook);\n        context.startService(intent);\n    }\n\n    public static void removeDownload(Context context, String noteUrl) {\n        if (noteUrl == null || !isRunning) {\n            return;\n        }\n        Intent intent = new Intent(context, DownloadService.class);\n        intent.setAction(removeDownloadAction);\n        intent.putExtra(\"noteUrl\", noteUrl);\n        context.startService(intent);\n    }\n\n    public static void cancelDownload(Context context) {\n        if (!isRunning) {\n            return;\n        }\n        Intent intent = new Intent(context, DownloadService.class);\n        intent.setAction(cancelAction);\n        context.startService(intent);\n    }\n\n    public static void obtainDownloadList(Context context) {\n        if (!isRunning) {\n            return;\n        }\n        Intent intent = new Intent(context, DownloadService.class);\n        intent.setAction(obtainDownloadListAction);\n        context.startService(intent);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/service/MediaButtonIntentReceiver.java",
    "content": "package com.kunfei.bookshelf.service;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.KeyEvent;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.AppActivityManager;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.presenter.ReadBookPresenter;\nimport com.kunfei.bookshelf.view.activity.ReadBookActivity;\n\n/**\n * Created by GKF on 2018/1/6.\n * 监听耳机键\n */\n\npublic class MediaButtonIntentReceiver extends BroadcastReceiver {\n    public static final String TAG = MediaButtonIntentReceiver.class.getSimpleName();\n\n    public static boolean handleIntent(final Context context, final Intent intent) {\n        final String intentAction = intent.getAction();\n        if (Intent.ACTION_MEDIA_BUTTON.equals(intentAction)) {\n            final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);\n            if (event == null) {\n                return false;\n            }\n\n            final int keycode = event.getKeyCode();\n            final int action = event.getAction();\n\n            String command = null;\n            switch (keycode) {\n                case KeyEvent.KEYCODE_MEDIA_STOP:\n                case KeyEvent.KEYCODE_MEDIA_PAUSE:\n                case KeyEvent.KEYCODE_MEDIA_PLAY:\n                case KeyEvent.KEYCODE_HEADSETHOOK:\n                case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:\n                    command = ReadAloudService.ActionMediaPlay;\n                    break;\n                case KeyEvent.KEYCODE_MEDIA_PREVIOUS:\n                    command = ReadAloudService.ActionMediaPrev;\n                    break;\n                case KeyEvent.KEYCODE_MEDIA_NEXT:\n                    command = ReadAloudService.ActionMediaNext;\n                    break;\n                default:\n                    break;\n            }\n            if (command != null) {\n                if (action == KeyEvent.ACTION_DOWN) {\n                    readAloud(context, command);\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    private static void readAloud(final Context context, String command) {\n        if (!AppActivityManager.getInstance().isExist(ReadBookActivity.class)) {\n            Intent intent = new Intent(context, ReadBookActivity.class);\n            intent.putExtra(\"openFrom\", ReadBookPresenter.OPEN_FROM_APP);\n            intent.putExtra(\"readAloud\", true);\n            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n            try {\n                context.startActivity(intent);\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        } else {\n            RxBus.get().post(RxBusTag.MEDIA_BUTTON, command);\n        }\n    }\n\n    @Override\n    public void onReceive(final Context context, final Intent intent) {\n        if (handleIntent(context, intent) && isOrderedBroadcast()) {\n            abortBroadcast();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/service/ReadAloudService.java",
    "content": "package com.kunfei.bookshelf.service;\n\nimport static android.text.TextUtils.isEmpty;\nimport static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService;\n\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.app.Service;\nimport android.content.BroadcastReceiver;\nimport android.content.ComponentName;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.content.SharedPreferences;\nimport android.graphics.BitmapFactory;\nimport android.media.AudioAttributes;\nimport android.media.AudioFocusRequest;\nimport android.media.AudioManager;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.IBinder;\nimport android.os.Looper;\nimport android.speech.tts.TextToSpeech;\nimport android.speech.tts.UtteranceProgressListener;\nimport android.support.v4.media.session.MediaSessionCompat;\nimport android.support.v4.media.session.PlaybackStateCompat;\nimport android.text.TextUtils;\nimport android.widget.Toast;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport androidx.core.app.NotificationCompat;\n\nimport com.google.android.exoplayer2.PlaybackException;\nimport com.google.android.exoplayer2.Player;\nimport com.google.android.exoplayer2.SimpleExoPlayer;\nimport com.google.android.exoplayer2.source.MediaSource;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.ExoPlayerHelper;\nimport com.kunfei.bookshelf.help.MediaManager;\nimport com.kunfei.bookshelf.view.activity.ReadBookActivity;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\n\n/**\n * Created by GKF on 2018/1/2.\n * 朗读服务\n */\npublic class ReadAloudService extends Service implements Player.Listener {\n    private static final String TAG = ReadAloudService.class.getSimpleName();\n    public static final String ActionMediaPlay = \"mediaBtnPlay\";\n    public static final String ActionMediaPrev = \"mediaBtnPrev\";\n    public static final String ActionMediaNext = \"mediaBtnNext\";\n    public static final String ActionNewReadAloud = \"newReadAloud\";\n    public static final String ActionPauseService = \"pauseService\";\n    public static final String ActionResumeService = \"resumeService\";\n    private static final String ActionReadActivity = \"readActivity\";\n    private static final String ActionSetTimer = \"updateTimer\";\n    private static final String ActionSetProgress = \"setProgress\";\n    private static final String ActionUITimerStop = \"UITimerStop\";\n    private static final String ActionUITimerRemaining = \"UITimerRemaining\";\n    private static final int notificationId = 3222;\n    public static final int maxTimeMinute = 360;\n    private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY\n            | PlaybackStateCompat.ACTION_PAUSE\n            | PlaybackStateCompat.ACTION_PLAY_PAUSE\n            | PlaybackStateCompat.ACTION_SKIP_TO_NEXT\n            | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS\n            | PlaybackStateCompat.ACTION_STOP\n            | PlaybackStateCompat.ACTION_SEEK_TO;\n    public static Boolean running = false;\n    private TextToSpeech textToSpeech;\n    private TextToSpeech textToSpeech_ui;\n    private HashMap<String, String> mParams;\n    private Boolean ttsInitSuccess = false;\n    private Boolean speak = true;\n    private Boolean pause = false;\n    private final List<String> contentList = new ArrayList<>();\n    private int nowSpeak;\n    private static int timeMinute = 0;\n    private boolean timerEnable = false;\n    private AudioManager audioManager;\n    private MediaSessionCompat mediaSessionCompat;\n    private AudioFocusChangeListener audioFocusChangeListener;\n    private AudioFocusRequest mFocusRequest;\n    private BroadcastReceiver broadcastReceiver;\n    private SharedPreferences preference;\n    private int speechRate;\n    private String title;\n    private String text;\n    private boolean fadeTts;\n    private final Handler handler = new Handler();\n    private final Handler mainHandler = new Handler(Looper.getMainLooper());\n    private Runnable dsRunnable;\n    private Runnable mpRunnable;\n    private MediaManager mediaManager;\n    private int readAloudNumber;\n    private boolean isAudio;\n    private SimpleExoPlayer player;\n    private String audioUrl;\n    private Long progress;\n\n    /**\n     * 朗读\n     */\n    public static void play(Context context, Boolean aloudButton, String content, String title, String text, boolean isAudio, long progress) {\n        Intent readAloudIntent = new Intent(context, ReadAloudService.class);\n        readAloudIntent.setAction(ActionNewReadAloud);\n        readAloudIntent.putExtra(\"aloudButton\", aloudButton);\n        readAloudIntent.putExtra(\"content\", content);\n        readAloudIntent.putExtra(\"title\", title);\n        readAloudIntent.putExtra(\"text\", text);\n        readAloudIntent.putExtra(\"isAudio\", isAudio);\n        readAloudIntent.putExtra(\"progress\", progress);\n        context.startService(readAloudIntent);\n    }\n\n    /**\n     * @param context 停止\n     */\n    public static void stop(Context context) {\n        if (running) {\n            Intent intent = new Intent(context, ReadAloudService.class);\n            intent.setAction(ActionDoneService);\n            context.startService(intent);\n        }\n    }\n\n    /**\n     * @param context 暂停\n     */\n    public static void pause(Context context) {\n        if (running) {\n            Intent intent = new Intent(context, ReadAloudService.class);\n            intent.setAction(ActionPauseService);\n            context.startService(intent);\n        }\n    }\n\n    /**\n     * @param context 继续\n     */\n    public static void resume(Context context) {\n        if (running) {\n            Intent intent = new Intent(context, ReadAloudService.class);\n            intent.setAction(ActionResumeService);\n            context.startService(intent);\n        }\n    }\n\n    public static void setTimer(Context context, int minute) {\n        if (running) {\n            Intent intent = new Intent(context, ReadAloudService.class);\n            intent.setAction(ActionSetTimer);\n            intent.putExtra(\"minute\", minute);\n            context.startService(intent);\n        }\n    }\n\n    public static void setProgress(Context context, long progress) {\n        if (running) {\n            Intent intent = new Intent(context, ReadAloudService.class);\n            intent.setAction(ActionSetProgress);\n            intent.putExtra(\"progress\", progress);\n            context.startService(intent);\n        }\n    }\n\n    public static void tts_ui_timer_stop(Context context) {\n        if (running) {\n            Intent intent = new Intent(context, ReadAloudService.class);\n            intent.setAction(ActionUITimerStop);\n            context.startService(intent);\n        }\n    }\n\n    public static void tts_ui_timer_remaining(Context context) {\n        if (running) {\n            Intent intent = new Intent(context, ReadAloudService.class);\n            intent.setAction(ActionUITimerRemaining);\n            context.startService(intent);\n        }\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        running = true;\n        preference = MApplication.getConfigPreferences();\n        audioFocusChangeListener = new AudioFocusChangeListener();\n        audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);\n        mediaManager = MediaManager.getInstance();\n        mediaManager.setStream(TextToSpeech.Engine.DEFAULT_STREAM);\n        fadeTts = preference.getBoolean(\"fadeTTS\", false);\n        dsRunnable = this::doDs;\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            initFocusRequest();\n        }\n        initMediaSession();\n        initBroadcastReceiver();\n        mediaSessionCompat.setActive(true);\n        updateMediaSessionPlaybackState();\n        updateNotification();\n        mpRunnable = new Runnable() {\n            @Override\n            public void run() {\n                if (player != null) {\n                    RxBus.get().post(RxBusTag.AUDIO_DUR, (int) player.getCurrentPosition());\n                }\n                handler.removeCallbacks(this);\n                handler.postDelayed(this, 1000);\n            }\n        };\n    }\n\n    @Override\n    public void onTaskRemoved(Intent rootIntent) {\n        super.onTaskRemoved(rootIntent);\n        clearTTS();\n        stopSelf();\n    }\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        if (intent != null) {\n            String action = intent.getAction();\n            if (action != null) {\n                String sText;\n                switch (action) {\n                    case ActionDoneService:\n                        stopSelf();\n                        break;\n                    case ActionPauseService:\n                        pauseReadAloud(true);\n                        break;\n                    case ActionResumeService:\n                        resumeReadAloud();\n                        break;\n                    case ActionSetTimer:\n                        updateTimer(intent.getIntExtra(\"minute\", 10));\n                        break;\n                    case ActionNewReadAloud:\n                        newReadAloud(intent.getStringExtra(\"content\"),\n                                intent.getBooleanExtra(\"aloudButton\", false),\n                                intent.getStringExtra(\"title\"),\n                                intent.getStringExtra(\"text\"),\n                                intent.getBooleanExtra(\"isAudio\", false),\n                                intent.getLongExtra(\"progress\", 0L));\n                        break;\n                    case ActionSetProgress:\n                        if (player != null) {\n                            progress = intent.getLongExtra(\"progress\", 0);\n                            player.seekTo(progress);\n                        }\n                        break;\n                    case ActionUITimerStop:\n                        sText = getString(R.string.read_aloud_timerstop);\n                        textToSpeech_ui.speak(sText, TextToSpeech.QUEUE_FLUSH, mParams);\n                        break;\n                    case ActionUITimerRemaining:\n                        if (timeMinute > 0 && timeMinute <= maxTimeMinute) {\n                            if (timeMinute <= 60) {\n                                sText = getString(R.string.read_aloud_timerremaining, timeMinute);\n                            } else {\n                                int hours = timeMinute / 60;\n                                int minutes = timeMinute % 60;\n                                sText = getString(R.string.read_aloud_timerremaininglong, hours, minutes);\n                            }\n                        } else {\n                            sText = getString(R.string.read_aloud_timerstop);\n                        }\n                        pauseReadAloud(false);\n                        textToSpeech_ui.speak(sText, TextToSpeech.QUEUE_FLUSH, mParams);\n                        resumeReadAloud();\n                        break;\n                }\n            }\n        }\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return null;\n    }\n\n    private void initTTS() {\n        if (textToSpeech == null)\n            textToSpeech = new TextToSpeech(this, new TTSListener());\n        if (textToSpeech_ui == null)\n            textToSpeech_ui = new TextToSpeech(this, new TTSUIListener());\n        if (mParams == null) {\n            mParams = new HashMap<>();\n            mParams.put(TextToSpeech.Engine.KEY_PARAM_STREAM, \"3\");\n        }\n    }\n\n    private void initMediaPlayer() {\n        if (player != null) return;\n        player = new SimpleExoPlayer.Builder(this).build();\n        player.addListener(this);\n    }\n\n    @Override\n    public void onPlaybackStateChanged(int playbackState) {\n        switch (playbackState) {\n            case Player.STATE_IDLE:\n                //播放器没有可播放的媒体。\n                break;\n            case Player.STATE_BUFFERING:\n                //播放器无法立即从当前位置开始播放。这种状态通常需要加载更多数据时发生。\n                break;\n            case Player.STATE_READY:\n                // 播放器可以立即从当前位置开始播放。如果{@link#getPlayWhenReady（）}为true，否则暂停。\n                if (player.getCurrentPosition() != progress) {\n                    player.seekTo(progress);\n                }\n                if (player.getPlayWhenReady()) {\n                    speak = true;\n                    RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PLAY);\n                } else {\n                    speak = false;\n                    RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PAUSE);\n                }\n                RxBus.get().post(RxBusTag.AUDIO_SIZE, (int) player.getDuration());\n                RxBus.get().post(RxBusTag.AUDIO_DUR, (int) player.getCurrentPosition());\n                handler.postDelayed(mpRunnable, 1000);\n                break;\n            case Player.STATE_ENDED:\n                //播放器完成了播放\n                handler.removeCallbacks(mpRunnable);\n                RxBus.get().post(RxBusTag.ALOUD_STATE, Status.NEXT);\n                break;\n        }\n    }\n\n    @Override\n    public void onPlayerError(@NonNull PlaybackException error) {\n        error.printStackTrace();\n        mainHandler.post(() ->\n                Toast.makeText(ReadAloudService.this, \"播放出错:\" + error.getLocalizedMessage(), Toast.LENGTH_LONG).show());\n        pauseReadAloud(true);\n        player.release();\n    }\n\n    private void newReadAloud(String content, Boolean aloudButton, String title, String text, boolean isAudio, Long progress) {\n        if (TextUtils.isEmpty(content)) {\n            stopSelf();\n            return;\n        }\n        this.text = text;\n        this.title = title;\n        this.progress = progress;\n        this.isAudio = isAudio;\n        nowSpeak = 0;\n        readAloudNumber = 0;\n        contentList.clear();\n        if (isAudio) {\n            initMediaPlayer();\n            audioUrl = content;\n        } else {\n            initTTS();\n            String[] splitSpeech = content.split(\"\\n\");\n            for (String aSplitSpeech : splitSpeech) {\n                if (!isEmpty(aSplitSpeech)) {\n                    contentList.add(aSplitSpeech);\n                }\n            }\n        }\n        if (aloudButton || speak) {\n            speak = false;\n            pause = false;\n            playTTS();\n        }\n    }\n\n    public void playTTS() {\n        updateNotification();\n        if (isAudio) {\n            try {\n                Uri uri = Uri.parse(audioUrl);\n                MediaSource mediaSource = ExoPlayerHelper.INSTANCE.createMediaSource(uri, null);\n                player.setMediaSource(mediaSource);\n                player.setPlayWhenReady(true);\n                player.prepare();\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        } else {\n            if (fadeTts) {\n                AsyncTask.execute(() -> mediaManager.fadeInVolume());\n                handler.postDelayed(this::playTTSN, 200);\n            } else {\n                playTTSN();\n            }\n        }\n    }\n\n    public void playTTSN() {\n        if (contentList.size() < 1) {\n            RxBus.get().post(RxBusTag.ALOUD_STATE, Status.NEXT);\n            return;\n        }\n        if (ttsInitSuccess && !speak && requestFocus()) {\n            speak = !speak;\n            RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PLAY);\n            updateNotification();\n            initSpeechRate();\n            HashMap<String, String> map = new HashMap<>();\n            map.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, \"content\");\n            for (int i = nowSpeak; i < contentList.size(); i++) {\n                if (i == 0) {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n                        textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_FLUSH, null, \"content\");\n                    } else {\n                        textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_FLUSH, map);\n                    }\n                } else {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n                        textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_ADD, null, \"content\");\n                    } else {\n                        textToSpeech.speak(contentList.get(i), TextToSpeech.QUEUE_ADD, map);\n                    }\n                }\n            }\n        }\n    }\n\n    public void toTTSSetting() {\n        //跳转到文字转语音设置界面\n        try {\n            Intent intent = new Intent();\n            intent.setAction(\"com.android.settings.TTS_SETTINGS\");\n            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n            startActivity(intent);\n        } catch (Exception ignored) {\n        }\n    }\n\n    private void initSpeechRate() {\n        if (speechRate != preference.getInt(\"speechRate\", 10)\n                && !preference.getBoolean(\"speechRateFollowSys\", true)) {\n            speechRate = preference.getInt(\"speechRate\", 10);\n            float speechRateF = (float) speechRate / 10;\n            textToSpeech.setSpeechRate(speechRateF);\n        }\n    }\n\n    /**\n     * @param pause true 暂停, false 失去焦点\n     */\n    private void pauseReadAloud(Boolean pause) {\n        this.pause = pause;\n        speak = false;\n        updateNotification();\n        updateMediaSessionPlaybackState();\n        if (isAudio) {\n            if (player != null && player.isPlaying())\n                player.pause();\n        } else {\n            if (fadeTts) {\n                AsyncTask.execute(() -> mediaManager.fadeOutVolume());\n                handler.postDelayed(() -> textToSpeech.stop(), 300);\n            } else {\n                textToSpeech.stop();\n            }\n        }\n        RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PAUSE);\n    }\n\n    /**\n     * 恢复朗读\n     */\n    private void resumeReadAloud() {\n        updateTimer(0);\n        pause = false;\n        updateNotification();\n        if (isAudio) {\n            if (player != null && !player.isPlaying())\n                player.play();\n        } else {\n            playTTS();\n        }\n        RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PLAY);\n    }\n\n    private void updateTimer(int minute) {\n        if (10 == minute) {\n            if (timeMinute < 30) {\n                timeMinute = timeMinute + minute;\n            } else if (timeMinute < 120) {\n                timeMinute = timeMinute + 15;\n            } else if (timeMinute < 180) {\n                timeMinute = timeMinute + 30;\n            } else {\n                timeMinute = timeMinute + 60;\n            }\n        } else {\n            timeMinute = timeMinute + minute;\n        }\n        if (timeMinute > maxTimeMinute) {\n            timerEnable = false;\n            handler.removeCallbacks(dsRunnable);\n            timeMinute = 0;\n            updateNotification();\n        } else if (timeMinute <= 0) {\n            if (timerEnable) {\n                handler.removeCallbacks(dsRunnable);\n                stopSelf();\n            }\n        } else {\n            timerEnable = true;\n            updateNotification();\n            handler.removeCallbacks(dsRunnable);\n            handler.postDelayed(dsRunnable, 60000);\n        }\n    }\n\n    private void doDs() {\n        if (!pause) {\n            setTimer(this, -1);\n        }\n    }\n\n    private PendingIntent getReadBookActivityPendingIntent() {\n        Intent intent = new Intent(this, ReadBookActivity.class);\n        intent.setAction(ReadAloudService.ActionReadActivity);\n        return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    private PendingIntent getThisServicePendingIntent(String actionStr) {\n        Intent intent = new Intent(this, this.getClass());\n        intent.setAction(actionStr);\n        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    /**\n     * 更新通知\n     */\n    private void updateNotification() {\n        if (text == null)\n            text = getString(R.string.read_aloud_s);\n        String nTitle;\n        if (pause) {\n            nTitle = getString(R.string.read_aloud_pause);\n        } else if (timeMinute > 0 && timeMinute <= maxTimeMinute) {\n            if (timeMinute <= 60) {\n                nTitle = getString(R.string.read_aloud_timer, timeMinute);\n            } else {\n                int hours = timeMinute / 60;\n                int minutes = timeMinute % 60;\n                nTitle = getString(R.string.read_aloud_timerlong, hours, minutes);\n            }\n        } else {\n            nTitle = getString(R.string.read_aloud_t);\n        }\n        nTitle += \": \" + title;\n        RxBus.get().post(RxBusTag.ALOUD_TIMER, nTitle);\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdReadAloud)\n                .setSmallIcon(R.drawable.ic_volume_up)\n                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_read_book))\n                .setOngoing(true)\n                .setContentTitle(nTitle)\n                .setContentText(text)\n                .setContentIntent(getReadBookActivityPendingIntent());\n        if (pause) {\n            builder.addAction(R.drawable.ic_play_24dp, getString(R.string.resume), getThisServicePendingIntent(ActionResumeService));\n        } else {\n            builder.addAction(R.drawable.ic_pause_24dp, getString(R.string.pause), getThisServicePendingIntent(ActionPauseService));\n        }\n        builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.stop), getThisServicePendingIntent(ActionDoneService));\n        builder.addAction(R.drawable.ic_time_add_24dp, getString(R.string.set_timer), getThisServicePendingIntent(ActionSetTimer));\n        builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()\n                .setMediaSession(mediaSessionCompat.getSessionToken())\n                .setShowActionsInCompactView(0, 1, 2));\n        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);\n        Notification notification = builder.build();\n        startForeground(notificationId, notification);\n    }\n\n    @Override\n    public void onDestroy() {\n        running = false;\n        super.onDestroy();\n        stopForeground(true);\n        handler.removeCallbacks(dsRunnable);\n        RxBus.get().post(RxBusTag.ALOUD_STATE, Status.STOP);\n        unRegisterMediaButton();\n        unregisterReceiver(broadcastReceiver);\n        clearTTS();\n    }\n\n    private void clearTTS() {\n        if (player != null) {\n            player.release();\n            player = null;\n        }\n        if (textToSpeech != null) {\n            if (fadeTts) {\n                AsyncTask.execute(() -> mediaManager.fadeOutVolume());\n            }\n            textToSpeech.stop();\n            textToSpeech.shutdown();\n            textToSpeech = null;\n        }\n        if (textToSpeech_ui != null) {\n            textToSpeech_ui.stop();\n            textToSpeech_ui.shutdown();\n            textToSpeech_ui = null;\n        }\n    }\n\n    private void unRegisterMediaButton() {\n        if (mediaSessionCompat != null) {\n            mediaSessionCompat.setCallback(null);\n            mediaSessionCompat.setActive(false);\n            mediaSessionCompat.release();\n        }\n        audioManager.abandonAudioFocus(audioFocusChangeListener);\n    }\n\n    /**\n     * @return 音频焦点\n     */\n    private boolean requestFocus() {\n        if (!isAudio) {\n            MediaManager.playSilentSound(this);\n        }\n        int request;\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            request = audioManager.requestAudioFocus(mFocusRequest);\n        } else {\n            request = audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);\n        }\n        return (request == AudioManager.AUDIOFOCUS_REQUEST_GRANTED);\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.O)\n    private void initFocusRequest() {\n        AudioAttributes mPlaybackAttributes = new AudioAttributes.Builder()\n                .setUsage(AudioAttributes.USAGE_MEDIA)\n                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)\n                .build();\n        mFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)\n                .setAudioAttributes(mPlaybackAttributes)\n                .setAcceptsDelayedFocusGain(true)\n                .setOnAudioFocusChangeListener(audioFocusChangeListener)\n                .build();\n    }\n\n    /**\n     * 初始化MediaSession\n     */\n    private void initMediaSession() {\n        ComponentName mComponent = new ComponentName(getPackageName(), MediaButtonIntentReceiver.class.getName());\n        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);\n        mediaButtonIntent.setComponent(mComponent);\n        PendingIntent mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(this, 0,\n                mediaButtonIntent, PendingIntent.FLAG_CANCEL_CURRENT);\n\n        mediaSessionCompat = new MediaSessionCompat(this, TAG, mComponent, mediaButtonReceiverPendingIntent);\n        mediaSessionCompat.setCallback(new MediaSessionCompat.Callback() {\n            @Override\n            public boolean onMediaButtonEvent(Intent mediaButtonEvent) {\n                return MediaButtonIntentReceiver.handleIntent(ReadAloudService.this, mediaButtonEvent);\n            }\n        });\n        mediaSessionCompat.setMediaButtonReceiver(mediaButtonReceiverPendingIntent);\n    }\n\n    private void initBroadcastReceiver() {\n        broadcastReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                String action = intent.getAction();\n                if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(action)) {\n                    pauseReadAloud(true);\n                }\n            }\n        };\n        IntentFilter intentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);\n        registerReceiver(broadcastReceiver, intentFilter);\n    }\n\n    private void updateMediaSessionPlaybackState() {\n        mediaSessionCompat.setPlaybackState(\n                new PlaybackStateCompat.Builder()\n                        .setActions(MEDIA_SESSION_ACTIONS)\n                        .setState(speak ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED,\n                                nowSpeak, 1)\n                        .build());\n    }\n\n    private final class TTSListener implements TextToSpeech.OnInitListener {\n        @Override\n        public void onInit(int i) {\n            if (i == TextToSpeech.SUCCESS) {\n                textToSpeech.setLanguage(Locale.CHINA);\n                textToSpeech.setOnUtteranceProgressListener(new ttsUtteranceListener());\n                ttsInitSuccess = true;\n                playTTS();\n            } else {\n                mainHandler.post(() -> Toast.makeText(ReadAloudService.this, getString(R.string.tts_init_failed), Toast.LENGTH_SHORT).show());\n                ReadAloudService.this.stopSelf();\n            }\n        }\n    }\n\n    private final class TTSUIListener implements TextToSpeech.OnInitListener {\n        @Override\n        public void onInit(int i) {\n            if (i == TextToSpeech.SUCCESS) {\n                textToSpeech_ui.setLanguage(Locale.CHINA);\n            }\n        }\n    }\n\n    /**\n     * 朗读监听\n     */\n    private class ttsUtteranceListener extends UtteranceProgressListener {\n\n        @Override\n        public void onStart(String s) {\n            updateMediaSessionPlaybackState();\n            RxBus.get().post(RxBusTag.READ_ALOUD_START, readAloudNumber + 1);\n            RxBus.get().post(RxBusTag.READ_ALOUD_NUMBER, readAloudNumber + 1);\n        }\n\n        @Override\n        public void onDone(String s) {\n            readAloudNumber = readAloudNumber + contentList.get(nowSpeak).length() + 1;\n            nowSpeak = nowSpeak + 1;\n            if (nowSpeak >= contentList.size()) {\n                RxBus.get().post(RxBusTag.ALOUD_STATE, Status.NEXT);\n            }\n        }\n\n        @Override\n        public void onError(String s) {\n            pauseReadAloud(true);\n            RxBus.get().post(RxBusTag.ALOUD_STATE, Status.PAUSE);\n        }\n\n        @Override\n        public void onRangeStart(String utteranceId, int start, int end, int frame) {\n            super.onRangeStart(utteranceId, start, end, frame);\n            RxBus.get().post(RxBusTag.READ_ALOUD_NUMBER, readAloudNumber + start);\n        }\n    }\n\n    class AudioFocusChangeListener implements AudioManager.OnAudioFocusChangeListener {\n        @Override\n        public void onAudioFocusChange(int focusChange) {\n            switch (focusChange) {\n                case AudioManager.AUDIOFOCUS_GAIN:\n                    // 重新获得焦点,  可做恢复播放，恢复后台音量的操作\n                    if (!pause) {\n                        resumeReadAloud();\n                    }\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS:\n                    // 永久丢失焦点除非重新主动获取，这种情况是被其他播放器抢去了焦点，  为避免与其他播放器混音，可将音乐暂停\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:\n                    // 暂时丢失焦点，这种情况是被其他应用申请了短暂的焦点，可压低后台音量\n                    if (!pause) {\n                        pauseReadAloud(false);\n                    }\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:\n                    // 短暂丢失焦点，这种情况是被其他应用申请了短暂的焦点希望其他声音能压低音量（或者关闭声音）凸显这个声音（比如短信提示音），\n                    break;\n            }\n        }\n    }\n\n    public enum Status {\n        PLAY, STOP, PAUSE, NEXT\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/service/ShareService.java",
    "content": "package com.kunfei.bookshelf.service;\n\nimport android.app.Activity;\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Handler;\nimport android.os.IBinder;\nimport android.os.Looper;\nimport android.text.TextUtils;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\nimport androidx.core.app.NotificationCompat;\n\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.web.ShareServer;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\nimport java.util.List;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService;\nimport static com.kunfei.bookshelf.constant.AppConstant.ActionStartService;\n\npublic class ShareService extends Service {\n    private static boolean isRunning = false;\n    private ShareServer shareServer;\n\n    private List<BookSourceBean> bookSourceBeans;\n\n    public static void startThis(Activity activity, List<BookSourceBean> bookSourceBeans) {\n        String key = String.valueOf(System.currentTimeMillis());\n        BitIntentDataManager.getInstance().putData(key, bookSourceBeans);\n        Intent intent = new Intent(activity, ShareService.class);\n        intent.putExtra(\"data_key\", key);\n        intent.setAction(ActionStartService);\n        activity.startService(intent);\n    }\n\n    public static void upServer(Activity activity) {\n        if (isRunning) {\n            Intent intent = new Intent(activity, ShareService.class);\n            intent.setAction(ActionStartService);\n            activity.startService(intent);\n        }\n    }\n\n    public static void stopThis(Context context) {\n        if (isRunning) {\n            Intent intent = new Intent(context, ShareService.class);\n            intent.setAction(ActionDoneService);\n            context.startService(intent);\n        }\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        updateNotification(\"正在启动分享\");\n        new Handler(Looper.getMainLooper())\n                .post(() -> Toast.makeText(this, \"正在启动分享\\n具体信息查看通知栏\", Toast.LENGTH_SHORT).show());\n    }\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        if (intent != null) {\n            String action = intent.getAction();\n            if (action != null) {\n                switch (action) {\n                    case ActionStartService:\n                        upServer(intent);\n                        break;\n                    case ActionDoneService:\n                        stopSelf();\n                        break;\n                }\n            }\n        }\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    private void upServer(Intent intent) {\n        String key = intent.getStringExtra(\"data_key\");\n        if (!TextUtils.isEmpty(key)) {\n            bookSourceBeans = (List<BookSourceBean>) BitIntentDataManager.getInstance().getData(key);\n        }\n        if (shareServer != null && shareServer.isAlive()) {\n            shareServer.stop();\n        }\n        shareServer = new ShareServer(65501, () -> bookSourceBeans);\n        InetAddress inetAddress = NetworkUtils.getLocalIPAddress();\n        if (inetAddress != null) {\n            try {\n                shareServer.start();\n                isRunning = true;\n                updateNotification(String.format(\"分享地址:%s\", inetAddress.getHostAddress()));\n            } catch (IOException e) {\n                stopSelf();\n            }\n        } else {\n            stopSelf();\n        }\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        isRunning = false;\n        if (shareServer != null && shareServer.isAlive()) {\n            shareServer.stop();\n        }\n    }\n\n    private PendingIntent getThisServicePendingIntent() {\n        Intent intent = new Intent(this, this.getClass());\n        intent.setAction(ActionDoneService);\n        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    /**\n     * 更新通知\n     */\n    private void updateNotification(String content) {\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdWeb)\n                .setSmallIcon(R.drawable.ic_share)\n                .setOngoing(true)\n                .setContentTitle(getString(R.string.wifi_share))\n                .setContentText(content);\n        builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getThisServicePendingIntent());\n        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);\n        Notification notification = builder.build();\n        int notificationId = 1122;\n        startForeground(notificationId, notification);\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/service/WebService.java",
    "content": "package com.kunfei.bookshelf.service;\n\nimport android.app.Activity;\nimport android.app.Notification;\nimport android.app.PendingIntent;\nimport android.app.Service;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Handler;\nimport android.os.IBinder;\nimport android.os.Looper;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\nimport androidx.core.app.NotificationCompat;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.web.HttpServer;\nimport com.kunfei.bookshelf.web.WebSocketServer;\n\nimport java.io.IOException;\nimport java.net.InetAddress;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.ActionDoneService;\nimport static com.kunfei.bookshelf.constant.AppConstant.ActionStartService;\n\npublic class WebService extends Service {\n    private static boolean sIsRunning = false;\n    private HttpServer httpServer;\n    private WebSocketServer webSocketServer;\n\n    /**\n     * Start the web service, return true if the service can be started normally, false if it is started.\n     *\n     * @param context Indicates component context.\n     * @return true if the service can be started normally, false if it is started.\n     */\n    public static boolean startThis(Context context) {\n        if (sIsRunning) {\n            return false;\n        } else {\n            Intent intent = new Intent(context, WebService.class);\n            intent.setAction(ActionStartService);\n            context.startService(intent);\n            return true;\n        }\n    }\n\n    public static void upHttpServer(Activity activity) {\n        if (sIsRunning) {\n            Intent intent = new Intent(activity, WebService.class);\n            intent.setAction(ActionStartService);\n            activity.startService(intent);\n        }\n    }\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        updateNotification(getString(R.string.web_service_starting_hint_short));\n        new Handler(Looper.getMainLooper())\n                .post(() -> Toast.makeText(this, R.string.web_service_starting_hint_long, Toast.LENGTH_SHORT).show());\n    }\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        if (intent != null) {\n            String action = intent.getAction();\n            if (action != null) {\n                switch (action) {\n                    case ActionStartService:\n                        upServer();\n                        break;\n                    case ActionDoneService:\n                        stopSelf();\n                        break;\n                }\n            }\n        }\n        return super.onStartCommand(intent, flags, startId);\n    }\n\n    private void upServer() {\n        if (httpServer != null && httpServer.isAlive()) {\n            httpServer.stop();\n        }\n        if (webSocketServer != null && webSocketServer.isAlive()) {\n            webSocketServer.stop();\n        }\n        int port = getPort();\n        httpServer = new HttpServer(port);\n        webSocketServer = new WebSocketServer(port + 1);\n        InetAddress inetAddress = NetworkUtils.getLocalIPAddress();\n        if (inetAddress != null) {\n            try {\n                httpServer.start();\n                webSocketServer.start(1000 * 30); // 通信超时设置\n                sIsRunning = true;\n                updateNotification(getString(R.string.http_ip, inetAddress.getHostAddress(), port));\n            } catch (IOException e) {\n                stopSelf();\n            }\n        } else {\n            stopSelf();\n        }\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        sIsRunning = false;\n        if (httpServer != null && httpServer.isAlive()) {\n            httpServer.stop();\n        }\n        if (webSocketServer != null && webSocketServer.isAlive()) {\n            webSocketServer.stop();\n        }\n    }\n\n    private PendingIntent getThisServicePendingIntent() {\n        Intent intent = new Intent(this, this.getClass());\n        intent.setAction(ActionDoneService);\n        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    private int getPort() {\n        int port = MApplication.getConfigPreferences().getInt(\"webPort\", 1122);\n        if (port > 65530 || port < 1024) {\n            port = 1122;\n        }\n        return port;\n    }\n\n    /**\n     * 更新通知\n     */\n    private void updateNotification(String content) {\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, MApplication.channelIdWeb)\n                .setSmallIcon(R.drawable.ic_web_service_noti)\n                .setOngoing(true)\n                .setContentTitle(getString(R.string.web_service))\n                .setContentText(content);\n        builder.addAction(R.drawable.ic_stop_black_24dp, getString(R.string.cancel), getThisServicePendingIntent());\n        builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);\n        Notification notification = builder.build();\n        int notificationId = 1122;\n        startForeground(notificationId, notification);\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ACache.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.utils;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.graphics.PixelFormat;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.io.BufferedReader;\nimport java.io.BufferedWriter;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.FileReader;\nimport java.io.FileWriter;\nimport java.io.IOException;\nimport java.io.ObjectInputStream;\nimport java.io.ObjectOutputStream;\nimport java.io.RandomAccessFile;\nimport java.io.Serializable;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\n\nimport timber.log.Timber;\n\n/**\n * 本地缓存\n */\n@SuppressWarnings({\"unused\", \"ResultOfMethodCallIgnored\", \"WeakerAccess\"})\npublic class ACache {\n    public static final int TIME_HOUR = 60 * 60;\n    public static final int TIME_DAY = TIME_HOUR * 24;\n    private static final int MAX_SIZE = 1000 * 1000 * 50; // 50 mb\n    private static final int MAX_COUNT = Integer.MAX_VALUE; // 不限制存放数据的数量\n    private static Map<String, ACache> mInstanceMap = new HashMap<>();\n    private ACacheManager mCache;\n\n    private ACache(File cacheDir, long max_size, int max_count) {\n        try {\n            if (!cacheDir.exists() && !cacheDir.mkdirs()) {\n                Timber.tag(\"ACache\").i(\"can't make dirs in %s\", cacheDir.getAbsolutePath());\n            }\n            mCache = new ACacheManager(cacheDir, max_size, max_count);\n        } catch (Exception ignored) {\n        }\n    }\n\n    public static ACache get(Context ctx) {\n        return get(ctx, \"ACache\");\n    }\n\n    public static ACache get(Context ctx, String cacheName) {\n        File f = new File(ctx.getFilesDir(), cacheName);\n        return get(f, MAX_SIZE, MAX_COUNT);\n    }\n\n    public static ACache get(File cacheDir) {\n        return get(cacheDir, MAX_SIZE, MAX_COUNT);\n    }\n\n    public static ACache get(Context ctx, long max_zise, int max_count) {\n        try {\n            File f = new File(ctx.getFilesDir(), \"ACache\");\n            return get(f, max_zise, max_count);\n        } catch (Exception ignored) {\n        }\n\n        return null;\n    }\n\n    public static ACache get(File cacheDir, long max_zise, int max_count) {\n        try {\n            ACache manager = mInstanceMap.get(cacheDir.getAbsoluteFile() + myPid());\n            if (manager == null) {\n                manager = new ACache(cacheDir, max_zise, max_count);\n                mInstanceMap.put(cacheDir.getAbsolutePath() + myPid(), manager);\n            }\n            return manager;\n        } catch (Exception ignored) {\n        }\n\n        return null;\n    }\n\n    private static String myPid() {\n        return \"_\" + android.os.Process.myPid();\n    }\n\n    // =======================================\n    // ============ String数据 读写 ==============\n    // =======================================\n\n    /**\n     * 保存 String数据 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的String数据\n     */\n    public void put(String key, String value) {\n        try {\n            File file = mCache.newFile(key);\n            BufferedWriter out = null;\n            try {\n                out = new BufferedWriter(new FileWriter(file), 1024);\n                out.write(value);\n            } catch (IOException ignored) {\n            } finally {\n                if (out != null) {\n                    try {\n                        out.flush();\n                        out.close();\n                    } catch (IOException ignored) {\n                    }\n                }\n                mCache.put(file);\n            }\n        } catch (Exception ignored) {\n        }\n\n    }\n\n    /**\n     * 保存 String数据 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的String数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    public void put(String key, String value, int saveTime) {\n        put(key, Utils.newStringWithDateInfo(saveTime, value));\n    }\n\n    /**\n     * 读取 String数据\n     *\n     * @return String 数据\n     */\n    public String getAsString(String key) {\n        File file = mCache.get(key);\n        if (!file.exists())\n            return null;\n        boolean removeFile = false;\n        try (BufferedReader in = new BufferedReader(new FileReader(file))) {\n            StringBuilder readString = new StringBuilder();\n            String currentLine;\n            while ((currentLine = in.readLine()) != null) {\n                readString.append(currentLine);\n            }\n            if (!Utils.isDue(readString.toString())) {\n                return Utils.clearDateInfo(readString.toString());\n            } else {\n                removeFile = true;\n                return null;\n            }\n        } catch (IOException e) {\n            return null;\n        } finally {\n            if (removeFile)\n                remove(key);\n        }\n    }\n\n    // =======================================\n    // ========== JSONObject 数据 读写 =========\n    // =======================================\n\n    /**\n     * 保存 JSONObject数据 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的JSON数据\n     */\n    public void put(String key, JSONObject value) {\n        put(key, value.toString());\n    }\n\n    /**\n     * 保存 JSONObject数据 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的JSONObject数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    public void put(String key, JSONObject value, int saveTime) {\n        put(key, value.toString(), saveTime);\n    }\n\n    /**\n     * 读取JSONObject数据\n     *\n     * @return JSONObject数据\n     */\n    public JSONObject getAsJSONObject(String key) {\n        String JSONString = getAsString(key);\n        try {\n            return new JSONObject(JSONString);\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    // =======================================\n    // ============ JSONArray 数据 读写 =============\n    // =======================================\n\n    /**\n     * 保存 JSONArray数据 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的JSONArray数据\n     */\n    public void put(String key, JSONArray value) {\n        put(key, value.toString());\n    }\n\n    /**\n     * 保存 JSONArray数据 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的JSONArray数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    public void put(String key, JSONArray value, int saveTime) {\n        put(key, value.toString(), saveTime);\n    }\n\n    /**\n     * 读取JSONArray数据\n     *\n     * @return JSONArray数据\n     */\n    public JSONArray getAsJSONArray(String key) {\n        String JSONString = getAsString(key);\n        try {\n            return new JSONArray(JSONString);\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    // =======================================\n    // ============== byte 数据 读写 =============\n    // =======================================\n\n    /**\n     * 保存 byte数据 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的数据\n     */\n    public void put(String key, byte[] value) {\n        File file = mCache.newFile(key);\n        FileOutputStream out = null;\n        try {\n            out = new FileOutputStream(file);\n            out.write(value);\n        } catch (Exception ignored) {\n        } finally {\n            if (out != null) {\n                try {\n                    out.flush();\n                    out.close();\n                } catch (IOException ignored) {\n                }\n            }\n            mCache.put(file);\n        }\n    }\n\n    /**\n     * 保存 byte数据 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    public void put(String key, byte[] value, int saveTime) {\n        put(key, Utils.newByteArrayWithDateInfo(saveTime, value));\n    }\n\n    /**\n     * 获取 byte 数据\n     *\n     * @return byte 数据\n     */\n    public byte[] getAsBinary(String key) {\n        RandomAccessFile RAFile = null;\n        boolean removeFile = false;\n        try {\n            File file = mCache.get(key);\n            if (!file.exists())\n                return null;\n            RAFile = new RandomAccessFile(file, \"r\");\n            byte[] byteArray = new byte[(int) RAFile.length()];\n            RAFile.read(byteArray);\n            if (!Utils.isDue(byteArray)) {\n                return Utils.clearDateInfo(byteArray);\n            } else {\n                removeFile = true;\n                return null;\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n            return null;\n        } finally {\n            if (RAFile != null) {\n                try {\n                    RAFile.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n            if (removeFile)\n                remove(key);\n        }\n    }\n\n    // =======================================\n    // ============= 序列化 数据 读写 ===============\n    // =======================================\n\n    /**\n     * 保存 Serializable数据 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的value\n     */\n    public void put(String key, Serializable value) {\n        put(key, value, -1);\n    }\n\n    /**\n     * 保存 Serializable数据到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的value\n     * @param saveTime 保存的时间，单位：秒\n     */\n    public void put(String key, Serializable value, int saveTime) {\n        ByteArrayOutputStream baos;\n        baos = new ByteArrayOutputStream();\n        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {\n            oos.writeObject(value);\n            byte[] data = baos.toByteArray();\n            if (saveTime != -1) {\n                put(key, data, saveTime);\n            } else {\n                put(key, data);\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * 读取 Serializable数据\n     *\n     * @return Serializable 数据\n     */\n    public Object getAsObject(String key) {\n        byte[] data = getAsBinary(key);\n        if (data != null) {\n            ByteArrayInputStream bais = null;\n            ObjectInputStream ois = null;\n            try {\n                bais = new ByteArrayInputStream(data);\n                ois = new ObjectInputStream(bais);\n                return ois.readObject();\n            } catch (Exception e) {\n                e.printStackTrace();\n                return null;\n            } finally {\n                try {\n                    if (bais != null)\n                        bais.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n                try {\n                    if (ois != null)\n                        ois.close();\n                } catch (IOException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n        return null;\n\n    }\n\n    // =======================================\n    // ============== bitmap 数据 读写 =============\n    // =======================================\n\n    /**\n     * 保存 bitmap 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的bitmap数据\n     */\n    public void put(String key, Bitmap value) {\n        put(key, Utils.Bitmap2Bytes(value));\n    }\n\n    /**\n     * 保存 bitmap 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的 bitmap 数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    public void put(String key, Bitmap value, int saveTime) {\n        put(key, Utils.Bitmap2Bytes(value), saveTime);\n    }\n\n    /**\n     * 读取 bitmap 数据\n     *\n     * @return bitmap 数据\n     */\n    public Bitmap getAsBitmap(String key) {\n        if (getAsBinary(key) == null) {\n            return null;\n        }\n        return Utils.Bytes2Bimap(getAsBinary(key));\n    }\n\n    // =======================================\n    // ============= drawable 数据 读写 =============\n    // =======================================\n\n    /**\n     * 保存 drawable 到 缓存中\n     *\n     * @param key   保存的key\n     * @param value 保存的drawable数据\n     */\n    public void put(String key, Drawable value) {\n        put(key, Utils.drawable2Bitmap(value));\n    }\n\n    /**\n     * 保存 drawable 到 缓存中\n     *\n     * @param key      保存的key\n     * @param value    保存的 drawable 数据\n     * @param saveTime 保存的时间，单位：秒\n     */\n    public void put(String key, Drawable value, int saveTime) {\n        put(key, Utils.drawable2Bitmap(value), saveTime);\n    }\n\n    /**\n     * 读取 Drawable 数据\n     *\n     * @return Drawable 数据\n     */\n    public Drawable getAsDrawable(String key) {\n        if (getAsBinary(key) == null) {\n            return null;\n        }\n        return Utils.bitmap2Drawable(Utils.Bytes2Bimap(getAsBinary(key)));\n    }\n\n    /**\n     * 获取缓存文件\n     *\n     * @return value 缓存的文件\n     */\n    public File file(String key) {\n        try {\n            File f = mCache.newFile(key);\n            if (f.exists()) {\n                return f;\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n\n        return null;\n    }\n\n    /**\n     * 移除某个key\n     *\n     * @return 是否移除成功\n     */\n    public boolean remove(String key) {\n        return mCache.remove(key);\n    }\n\n    /**\n     * 清除所有数据\n     */\n    public void clear() {\n        mCache.clear();\n    }\n\n    /**\n     * @author 杨福海（michael） www.yangfuhai.com\n     * @version 1.0\n     * @title 时间计算工具类\n     */\n    private static class Utils {\n\n        private static final char mSeparator = ' ';\n\n        /**\n         * 判断缓存的String数据是否到期\n         *\n         * @return true：到期了 false：还没有到期\n         */\n        private static boolean isDue(String str) {\n            return isDue(str.getBytes());\n        }\n\n        /**\n         * 判断缓存的byte数据是否到期\n         *\n         * @return true：到期了 false：还没有到期\n         */\n        private static boolean isDue(byte[] data) {\n            try {\n                String[] strs = getDateInfoFromDate(data);\n                if (strs != null && strs.length == 2) {\n                    String saveTimeStr = strs[0];\n                    while (saveTimeStr.startsWith(\"0\")) {\n                        saveTimeStr = saveTimeStr\n                                .substring(1);\n                    }\n                    long saveTime = Long.valueOf(saveTimeStr);\n                    long deleteAfter = Long.valueOf(strs[1]);\n                    if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) {\n                        return true;\n                    }\n                }\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n\n            return false;\n        }\n\n        private static String newStringWithDateInfo(int second, String strInfo) {\n            return createDateInfo(second) + strInfo;\n        }\n\n        private static byte[] newByteArrayWithDateInfo(int second, byte[] data2) {\n            byte[] data1 = createDateInfo(second).getBytes();\n            byte[] retdata = new byte[data1.length + data2.length];\n            System.arraycopy(data1, 0, retdata, 0, data1.length);\n            System.arraycopy(data2, 0, retdata, data1.length, data2.length);\n            return retdata;\n        }\n\n        private static String clearDateInfo(String strInfo) {\n            if (strInfo != null && hasDateInfo(strInfo.getBytes())) {\n                strInfo = strInfo.substring(strInfo.indexOf(mSeparator) + 1);\n            }\n            return strInfo;\n        }\n\n        private static byte[] clearDateInfo(byte[] data) {\n            if (hasDateInfo(data)) {\n                return copyOfRange(data, indexOf(data, mSeparator) + 1,\n                        data.length);\n            }\n            return data;\n        }\n\n        private static boolean hasDateInfo(byte[] data) {\n            return data != null && data.length > 15 && data[13] == '-'\n                    && indexOf(data, mSeparator) > 14;\n        }\n\n        private static String[] getDateInfoFromDate(byte[] data) {\n            if (hasDateInfo(data)) {\n                String saveDate = new String(copyOfRange(data, 0, 13));\n                String deleteAfter = new String(copyOfRange(data, 14,\n                        indexOf(data, mSeparator)));\n                return new String[]{saveDate, deleteAfter};\n            }\n            return null;\n        }\n\n        private static int indexOf(byte[] data, char c) {\n            for (int i = 0; i < data.length; i++) {\n                if (data[i] == c) {\n                    return i;\n                }\n            }\n            return -1;\n        }\n\n        private static byte[] copyOfRange(byte[] original, int from, int to) {\n            int newLength = to - from;\n            if (newLength < 0)\n                throw new IllegalArgumentException(from + \" > \" + to);\n            byte[] copy = new byte[newLength];\n            System.arraycopy(original, from, copy, 0,\n                    Math.min(original.length - from, newLength));\n            return copy;\n        }\n\n        private static String createDateInfo(int second) {\n            StringBuilder currentTime = new StringBuilder(System.currentTimeMillis() + \"\");\n            while (currentTime.length() < 13) {\n                currentTime.insert(0, \"0\");\n            }\n            return currentTime + \"-\" + second + mSeparator;\n        }\n\n        /*\n         * Bitmap → byte[]\n         */\n        private static byte[] Bitmap2Bytes(Bitmap bm) {\n            if (bm == null) {\n                return null;\n            }\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            bm.compress(Bitmap.CompressFormat.PNG, 100, baos);\n            return baos.toByteArray();\n        }\n\n        /*\n         * byte[] → Bitmap\n         */\n        private static Bitmap Bytes2Bimap(byte[] b) {\n            if (b.length == 0) {\n                return null;\n            }\n            return BitmapFactory.decodeByteArray(b, 0, b.length);\n        }\n\n        /*\n         * Drawable → Bitmap\n         */\n        private static Bitmap drawable2Bitmap(Drawable drawable) {\n            if (drawable == null) {\n                return null;\n            }\n            // 取 drawable 的长宽\n            int w = drawable.getIntrinsicWidth();\n            int h = drawable.getIntrinsicHeight();\n            // 取 drawable 的颜色格式\n            Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888\n                    : Bitmap.Config.RGB_565;\n            // 建立对应 bitmap\n            Bitmap bitmap = Bitmap.createBitmap(w, h, config);\n            // 建立对应 bitmap 的画布\n            Canvas canvas = new Canvas(bitmap);\n            drawable.setBounds(0, 0, w, h);\n            // 把 drawable 内容画到画布中\n            drawable.draw(canvas);\n            return bitmap;\n        }\n\n        /*\n         * Bitmap → Drawable\n         */\n        @SuppressWarnings(\"deprecation\")\n        private static Drawable bitmap2Drawable(Bitmap bm) {\n            if (bm == null) {\n                return null;\n            }\n            return new BitmapDrawable(bm);\n        }\n    }\n\n    /**\n     * @author 杨福海（michael） www.yangfuhai.com\n     * @version 1.0\n     * @title 缓存管理器\n     */\n    public class ACacheManager {\n        private final AtomicLong cacheSize;\n        private final AtomicInteger cacheCount;\n        private final long sizeLimit;\n        private final int countLimit;\n        private final Map<File, Long> lastUsageDates = Collections\n                .synchronizedMap(new HashMap<>());\n        protected File cacheDir;\n\n        private ACacheManager(File cacheDir, long sizeLimit, int countLimit) {\n            this.cacheDir = cacheDir;\n            this.sizeLimit = sizeLimit;\n            this.countLimit = countLimit;\n            cacheSize = new AtomicLong();\n            cacheCount = new AtomicInteger();\n            calculateCacheSizeAndCacheCount();\n        }\n\n        /**\n         * 计算 cacheSize和cacheCount\n         */\n        private void calculateCacheSizeAndCacheCount() {\n            new Thread(() -> {\n\n                try {\n                    int size = 0;\n                    int count = 0;\n                    File[] cachedFiles = cacheDir.listFiles();\n                    if (cachedFiles != null) {\n                        for (File cachedFile : cachedFiles) {\n                            size += calculateSize(cachedFile);\n                            count += 1;\n                            lastUsageDates.put(cachedFile,\n                                    cachedFile.lastModified());\n                        }\n                        cacheSize.set(size);\n                        cacheCount.set(count);\n                    }\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n\n            }).start();\n        }\n\n        private void put(File file) {\n\n            try {\n                int curCacheCount = cacheCount.get();\n                while (curCacheCount + 1 > countLimit) {\n                    long freedSize = removeNext();\n                    cacheSize.addAndGet(-freedSize);\n\n                    curCacheCount = cacheCount.addAndGet(-1);\n                }\n                cacheCount.addAndGet(1);\n\n                long valueSize = calculateSize(file);\n                long curCacheSize = cacheSize.get();\n                while (curCacheSize + valueSize > sizeLimit) {\n                    long freedSize = removeNext();\n                    curCacheSize = cacheSize.addAndGet(-freedSize);\n                }\n                cacheSize.addAndGet(valueSize);\n\n                long currentTime = System.currentTimeMillis();\n                file.setLastModified(currentTime);\n                lastUsageDates.put(file, currentTime);\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n\n        }\n\n        private File get(String key) {\n            File file = newFile(key);\n            long currentTime = System.currentTimeMillis();\n            file.setLastModified(currentTime);\n            lastUsageDates.put(file, currentTime);\n\n            return file;\n        }\n\n        private File newFile(String key) {\n            return new File(cacheDir, key.hashCode() + \"\");\n        }\n\n        private boolean remove(String key) {\n            File image = get(key);\n            return image.delete();\n        }\n\n        private void clear() {\n            try {\n                lastUsageDates.clear();\n                cacheSize.set(0);\n                File[] files = cacheDir.listFiles();\n                if (files != null) {\n                    for (File f : files) {\n                        f.delete();\n                    }\n                }\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n\n        }\n\n        /**\n         * 移除旧的文件\n         */\n        private long removeNext() {\n            try {\n                if (lastUsageDates.isEmpty()) {\n                    return 0;\n                }\n\n                Long oldestUsage = null;\n                File mostLongUsedFile = null;\n                Set<Entry<File, Long>> entries = lastUsageDates.entrySet();\n                synchronized (lastUsageDates) {\n                    for (Entry<File, Long> entry : entries) {\n                        if (mostLongUsedFile == null) {\n                            mostLongUsedFile = entry.getKey();\n                            oldestUsage = entry.getValue();\n                        } else {\n                            Long lastValueUsage = entry.getValue();\n                            if (lastValueUsage < oldestUsage) {\n                                oldestUsage = lastValueUsage;\n                                mostLongUsedFile = entry.getKey();\n                            }\n                        }\n                    }\n                }\n\n                long fileSize = 0;\n                if (mostLongUsedFile != null) {\n                    fileSize = calculateSize(mostLongUsedFile);\n                    if (mostLongUsedFile.delete()) {\n                        lastUsageDates.remove(mostLongUsedFile);\n                    }\n                }\n                return fileSize;\n            } catch (Exception e) {\n                e.printStackTrace();\n                return 0;\n            }\n\n\n        }\n\n        private long calculateSize(File file) {\n            return file.length();\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ActivityExtensions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils\n\nimport android.app.Activity\nimport android.graphics.Color\nimport android.os.Build\nimport android.os.Bundle\nimport android.util.DisplayMetrics\nimport android.view.*\nimport android.widget.FrameLayout\nimport androidx.annotation.ColorInt\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.fragment.app.DialogFragment\nimport com.kunfei.bookshelf.R\n\ninline fun <reified T : DialogFragment> AppCompatActivity.showDialogFragment(\n    arguments: Bundle.() -> Unit = {}\n) {\n    val dialog = T::class.java.newInstance()\n    val bundle = Bundle()\n    bundle.apply(arguments)\n    dialog.arguments = bundle\n    dialog.show(supportFragmentManager, T::class.simpleName)\n}\n\nfun AppCompatActivity.showDialogFragment(dialogFragment: DialogFragment) {\n    dialogFragment.show(supportFragmentManager, dialogFragment::class.simpleName)\n}\n\nval Activity.windowSize: DisplayMetrics\n    get() {\n        val displayMetrics = DisplayMetrics()\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n            val windowMetrics: WindowMetrics = windowManager.currentWindowMetrics\n            val insets = windowMetrics.windowInsets\n                .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())\n            displayMetrics.widthPixels = windowMetrics.bounds.width() - insets.left - insets.right\n            displayMetrics.heightPixels = windowMetrics.bounds.height() - insets.top - insets.bottom\n        } else {\n            @Suppress(\"DEPRECATION\")\n            windowManager.defaultDisplay.getMetrics(displayMetrics)\n        }\n        return displayMetrics\n    }\n\nfun Activity.fullScreen() {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n        window.setDecorFitsSystemWindows(true)\n    }\n    @Suppress(\"DEPRECATION\")\n    window.decorView.systemUiVisibility =\n        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n    @Suppress(\"DEPRECATION\")\n    window.clearFlags(\n        WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS\n                or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION\n    )\n    window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)\n}\n\n/**\n * 设置状态栏颜色\n */\nfun Activity.setStatusBarColorAuto(\n    @ColorInt color: Int,\n    isTransparent: Boolean,\n    fullScreen: Boolean\n) {\n    val isLightBar = ColorUtils.isColorLight(color)\n    if (fullScreen) {\n        if (isTransparent) {\n            window.statusBarColor = Color.TRANSPARENT\n        } else {\n            window.statusBarColor = getCompatColor(R.color.status_bar_bag)\n        }\n    } else {\n        window.statusBarColor = color\n    }\n    setLightStatusBar(isLightBar)\n}\n\nfun Activity.setLightStatusBar(isLightBar: Boolean) {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n        window.insetsController?.let {\n            if (isLightBar) {\n                it.setSystemBarsAppearance(\n                    WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,\n                    WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS\n                )\n            } else {\n                it.setSystemBarsAppearance(\n                    0,\n                    WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS\n                )\n            }\n        }\n    }\n    @Suppress(\"DEPRECATION\")\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n        val decorView = window.decorView\n        val systemUiVisibility = decorView.systemUiVisibility\n        if (isLightBar) {\n            decorView.systemUiVisibility =\n                systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR\n        } else {\n            decorView.systemUiVisibility =\n                systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()\n        }\n    }\n}\n\n/**\n * 设置导航栏颜色\n */\nfun Activity.setNavigationBarColorAuto(@ColorInt color: Int) {\n    val isLightBor = ColorUtils.isColorLight(color)\n    window.navigationBarColor = color\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n        window.insetsController?.let {\n            if (isLightBor) {\n                it.setSystemBarsAppearance(\n                    WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,\n                    WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS\n                )\n            } else {\n                it.setSystemBarsAppearance(\n                    0,\n                    WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS\n                )\n            }\n        }\n    }\n    @Suppress(\"DEPRECATION\")\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n        val decorView = window.decorView\n        var systemUiVisibility = decorView.systemUiVisibility\n        systemUiVisibility = if (isLightBor) {\n            systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR\n        } else {\n            systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()\n        }\n        decorView.systemUiVisibility = systemUiVisibility\n    }\n}\n\n/////以下方法需要在View完全被绘制出来之后调用，否则判断不了,在比如 onWindowFocusChanged（）方法中可以得到正确的结果/////\n\n/**\n * 返回NavigationBar\n */\nval Activity.navigationBar: View?\n    get() {\n        val viewGroup = (window.decorView as? ViewGroup) ?: return null\n        for (i in 0 until viewGroup.childCount) {\n            val child = viewGroup.getChildAt(i)\n            val childId = child.id\n            if (childId != View.NO_ID\n                && resources.getResourceEntryName(childId) == \"navigationBarBackground\"\n            ) {\n                return child\n            }\n        }\n        return null\n    }\n\n/**\n * 返回NavigationBar是否存在\n */\nval Activity.isNavigationBarExist: Boolean\n    get() = navigationBar != null\n\n/**\n * 返回NavigationBar高度\n */\nval Activity.navigationBarHeight: Int\n    get() {\n        if (isNavigationBarExist) {\n            val resourceId = resources.getIdentifier(\"navigation_bar_height\", \"dimen\", \"android\")\n            return resources.getDimensionPixelSize(resourceId)\n        }\n        return 0\n    }\n\n/**\n * 返回navigationBar位置\n */\nval Activity.navigationBarGravity: Int\n    get() {\n        val gravity = (navigationBar?.layoutParams as? FrameLayout.LayoutParams)?.gravity\n        return gravity ?: Gravity.BOTTOM\n    }\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/BatteryUtil.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.os.BatteryManager;\n\npublic class BatteryUtil {\n\n    public static int getLevel(Context context) {\n        IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);\n        Intent batteryStatus = context.registerReceiver(null, iFilter);\n\n        return batteryStatus != null ? batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) : -1;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/BitmapUtil.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.graphics.Bitmap.Config;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Matrix;\nimport android.net.Uri;\nimport android.renderscript.Allocation;\nimport android.renderscript.Element;\nimport android.renderscript.RenderScript;\nimport android.renderscript.ScriptIntrinsicBlur;\nimport android.util.Log;\n\nimport com.kunfei.bookshelf.MApplication;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class BitmapUtil {\n    private static final String TAG = \"BitmapUtil\";\n\n    public static Bitmap getFitSampleBitmap(String file_path, int width, int height) {\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        BitmapFactory.decodeFile(file_path, options);\n        options.inSampleSize = getFitInSampleSize(width, height, options);\n        options.inJustDecodeBounds = false;\n        return BitmapFactory.decodeFile(file_path, options);\n    }\n\n    public static int getFitInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {\n        int inSampleSize = 1;\n        if (options.outWidth > reqWidth || options.outHeight > reqHeight) {\n            int widthRatio = Math.round((float) options.outWidth / (float) reqWidth);\n            int heightRatio = Math.round((float) options.outHeight / (float) reqHeight);\n            inSampleSize = Math.min(widthRatio, heightRatio);\n        }\n        return inSampleSize;\n    }\n\n    /**\n     * 通过资源id转化成Bitmap\n     */\n    public static Bitmap ReadBitmapById(Context context, int resId) {\n        BitmapFactory.Options opt = new BitmapFactory.Options();\n        opt.inPreferredConfig = Config.ARGB_8888;\n        opt.inPurgeable = true;\n        opt.inInputShareable = true;\n        InputStream is = context.getResources().openRawResource(resId);\n        return BitmapFactory.decodeStream(is, null, opt);\n    }\n\n    /**\n     * 缩放Bitmap满屏\n     */\n    public static Bitmap getBitmap(Bitmap bitmap, int screenWidth,\n                                   int screenHeight) {\n        int w = bitmap.getWidth();\n        int h = bitmap.getHeight();\n        Matrix matrix = new Matrix();\n        float scale = (float) screenWidth / w;\n        float scale2 = (float) screenHeight / h;\n        // scale = scale < scale2 ? scale : scale2;\n        matrix.postScale(scale, scale);\n        Bitmap bmp = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true);\n        if (!bitmap.equals(bmp) && !bitmap.isRecycled()) {\n            bitmap.recycle();\n            bitmap = null;\n        }\n        return bmp;// Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true);\n    }\n\n    /**\n     * 按最大边按一定大小缩放图片\n     */\n    public static Bitmap scaleImage(byte[] buffer, float size) {\n        // 获取原图宽度\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        options.inPurgeable = true;\n        options.inInputShareable = true;\n        Bitmap bm;\n        // 计算缩放比例\n        float reSize = options.outWidth / size;\n        if (options.outWidth < options.outHeight) {\n            reSize = options.outHeight / size;\n        }\n        // 如果是小图则放大\n        if (reSize <= 1) {\n            int newWidth = 0;\n            int newHeight = 0;\n            if (options.outWidth > options.outHeight) {\n                newWidth = (int) size;\n                newHeight = options.outHeight * (int) size / options.outWidth;\n            } else {\n                newHeight = (int) size;\n                newWidth = options.outWidth * (int) size / options.outHeight;\n            }\n            bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length);\n            bm = scaleImage(bm, newWidth, newHeight);\n            if (bm == null) {\n                Log.e(TAG, \"convertToThumb, decode fail:\" + null);\n                return null;\n            }\n            return bm;\n        }\n        // 缩放\n        options.inJustDecodeBounds = false;\n        options.inSampleSize = (int) reSize;\n        bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options);\n        if (bm == null) {\n            Log.e(TAG, \"convertToThumb, decode fail:\" + null);\n            return null;\n        }\n        return bm;\n    }\n\n    /**\n     * 检查图片是否超过一定值，是则缩小\n     */\n    public static Bitmap convertToThumb(byte[] buffer, float size) {\n        // 获取原图宽度\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inJustDecodeBounds = true;\n        options.inPurgeable = true;\n        options.inInputShareable = true;\n        Bitmap bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length,\n                options);\n        // 计算缩放比例\n        float reSize = options.outWidth / size;\n        if (options.outWidth > options.outHeight) {\n            reSize = options.outHeight / size;\n        }\n        if (reSize <= 0) {\n            reSize = 1;\n        }\n        Log.d(TAG, \"convertToThumb, reSize:\" + reSize);\n        // 缩放\n        options.inJustDecodeBounds = false;\n        options.inSampleSize = (int) reSize;\n        if (bm != null && !bm.isRecycled()) {\n            bm.recycle();\n            bm = null;\n            Log.e(TAG, \"convertToThumb, recyle\");\n        }\n        bm = BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options);\n        if (bm == null) {\n            Log.e(TAG, \"convertToThumb, decode fail:\" + null);\n            return null;\n        }\n        return bm;\n    }\n\n    /**\n     * Bitmap --> byte[]\n     */\n    private static byte[] readBitmap(Bitmap bmp) {\n        ByteArrayOutputStream stream = new ByteArrayOutputStream();\n        bmp.compress(Bitmap.CompressFormat.JPEG, 60, stream);\n        try {\n            stream.flush();\n            stream.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        return stream.toByteArray();\n    }\n\n    /**\n     * Bitmap --> byte[]\n     */\n    public static byte[] readBitmapFromBuffer(byte[] buffer, float size) {\n        return readBitmap(convertToThumb(buffer, size));\n    }\n\n    /**\n     * 以屏幕宽度为基准，显示图片\n     */\n    public static Bitmap decodeStream(Context context, Intent data, float size) {\n        Bitmap image = null;\n        try {\n            Uri dataUri = data.getData();\n            // 获取原图宽度\n            BitmapFactory.Options options = new BitmapFactory.Options();\n            options.inJustDecodeBounds = true;\n            options.inPurgeable = true;\n            options.inInputShareable = true;\n            BitmapFactory.decodeStream(context.getContentResolver()\n                    .openInputStream(dataUri), null, options);\n            // 计算缩放比例\n            float reSize = (int) (options.outWidth / size);\n            if (reSize <= 0) {\n                reSize = 1;\n            }\n            Log.d(TAG, \"old-w:\" + options.outWidth + \", llyt-w:\" + size\n                    + \", resize:\" + reSize);\n            // 缩放\n            options.inJustDecodeBounds = false;\n            options.inSampleSize = (int) reSize;\n            image = BitmapFactory.decodeStream(context.getContentResolver()\n                    .openInputStream(dataUri), null, options);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return image;\n    }\n\n    /**\n     * 按新的宽高缩放图片\n     */\n    public static Bitmap scaleImage(Bitmap bm, int newWidth, int newHeight) {\n        if (bm == null) {\n            return null;\n        }\n        int width = bm.getWidth();\n        int height = bm.getHeight();\n        float scaleWidth = ((float) newWidth) / width;\n        float scaleHeight = ((float) newHeight) / height;\n        Matrix matrix = new Matrix();\n        matrix.postScale(scaleWidth, scaleHeight);\n        Bitmap newbm = Bitmap.createBitmap(bm, 0, 0, width, height, matrix,\n                true);\n        if (!bm.isRecycled()) {\n            bm.recycle();\n        }\n        return newbm;\n    }\n\n    /**\n     * 设置固定的宽度，高度随之变化，使图片不会变形\n     */\n    public static Bitmap fitBitmap(Bitmap target, int newWidth) {\n        int width = target.getWidth();\n        int height = target.getHeight();\n        Matrix matrix = new Matrix();\n        float scaleWidth = ((float) newWidth) / width;\n        // float scaleHeight = ((float)newHeight) / height;\n        matrix.postScale(scaleWidth, scaleWidth);\n        // Bitmap result = Bitmap.createBitmap(target,0,0,width,height,\n        // matrix,true);\n        Bitmap bmp = Bitmap.createBitmap(target, 0, 0, width, height, matrix,\n                true);\n        if (!target.equals(bmp) && !target.isRecycled()) {\n            target.recycle();\n        }\n        return bmp;// Bitmap.createBitmap(target, 0, 0, width, height, matrix,\n        // true);\n    }\n\n    /**\n     * 根据指定的宽高平铺图像\n     */\n    public static Bitmap createRepeater(int width, int heigth, Bitmap src) {\n        int countWidth = (width + src.getWidth() - 1) / src.getWidth();\n        int countHeight = (heigth + src.getHeight() - 1) / src.getHeight();\n\n        Bitmap bitmap = Bitmap.createBitmap(width, heigth, Bitmap.Config.ARGB_8888);\n        Canvas canvas = new Canvas(bitmap);\n        for (int i = 0; i < countHeight; ++i) {\n            for (int idx = 0; idx < countWidth; ++idx) {\n                canvas.drawBitmap(src, idx * src.getWidth(), i * src.getHeight(), null);\n            }\n        }\n        return bitmap;\n\n    }\n\n    /**\n     * 图片的质量压缩方法\n     */\n    public static Bitmap compressImage(Bitmap image) {\n        ByteArrayOutputStream stream = new ByteArrayOutputStream();\n        image.compress(Bitmap.CompressFormat.JPEG, 100, stream);// 质量压缩方法，这里100表示不压缩，把压缩后的数据存放到baos中\n        int options = 100;\n        while (stream.toByteArray().length / 1024 > 100) { // 循环判断如果压缩后图片是否大于100kb,大于继续压缩\n            stream.reset();// 重置stream即清空stream\n            image.compress(Bitmap.CompressFormat.JPEG, options, stream);// 这里压缩options%，把压缩后的数据存放到baos中\n            options -= 10;// 每次都减少10\n        }\n        ByteArrayInputStream isBm = new ByteArrayInputStream(stream.toByteArray());// 把压缩后的数据baos存放到ByteArrayInputStream中\n        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);// 把ByteArrayInputStream数据生成图片\n        try {\n            stream.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        try {\n            isBm.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        if (!image.isRecycled()) {\n            image.recycle();\n        }\n        return bitmap;\n    }\n\n    /**\n     * 图片按比例大小压缩方法(根据Bitmap图片压缩)\n     */\n    public static Bitmap getImage(Bitmap image) {\n        ByteArrayOutputStream stream = new ByteArrayOutputStream();\n        image.compress(Bitmap.CompressFormat.JPEG, 100, stream);\n        if (stream.toByteArray().length / 1024 > 1024) {// 判断如果图片大于1M,进行压缩避免在生成图片（BitmapFactory.decodeStream）时溢出\n            stream.reset();// 重置stream即清空stream\n            image.compress(Bitmap.CompressFormat.JPEG, 50, stream);// 这里压缩50%，把压缩后的数据存放到baos中\n        }\n        ByteArrayInputStream isBm;\n        BitmapFactory.Options newOpts = new BitmapFactory.Options();\n        // 开始读入图片，此时把options.inJustDecodeBounds 设回true了\n        newOpts.inJustDecodeBounds = true;\n        Bitmap bitmap;\n        newOpts.inJustDecodeBounds = false;\n        int w = newOpts.outWidth;\n        int h = newOpts.outHeight;\n        // 现在主流手机比较多是800*480分辨率，所以高和宽我们设置为\n        float hh = 800f;// 这里设置高度为800f\n        float ww = 480f;// 这里设置宽度为480f\n        // 缩放比。由于是固定比例缩放，只用高或者宽其中一个数据进行计算即可\n        int be = 1;// be=1表示不缩放\n        if (w > h && w > ww) {// 如果宽度大的话根据宽度固定大小缩放\n            be = (int) (newOpts.outWidth / ww);\n        } else if (w < h && h > hh) {// 如果高度高的话根据宽度固定大小缩放\n            be = (int) (newOpts.outHeight / hh);\n        }\n        if (be <= 0)\n            be = 1;\n        newOpts.inSampleSize = be;// 设置缩放比例\n        // 重新读入图片，注意此时已经把options.inJustDecodeBounds 设回false了\n        isBm = new ByteArrayInputStream(stream.toByteArray());\n        bitmap = BitmapFactory.decodeStream(isBm, null, newOpts);\n        try {\n            isBm.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n        if (!image.isRecycled()) {\n            image.recycle();\n        }\n        assert bitmap != null;\n        return compressImage(bitmap);// 压缩好比例大小后再进行质量压缩\n    }\n\n    /**\n     * 通过资源id转化成Bitmap 全屏显示\n     */\n    public static Bitmap ReadBitmapById(Context context, int drawableId,\n                                        int screenWidth, int screenHight) {\n        BitmapFactory.Options options = new BitmapFactory.Options();\n        options.inPreferredConfig = Config.ARGB_8888;\n        options.inInputShareable = true;\n        options.inPurgeable = true;\n        InputStream stream = context.getResources().openRawResource(drawableId);\n        Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);\n        assert bitmap != null;\n        return getBitmap(bitmap, screenWidth, screenHight);\n    }\n\n    /**\n     * 高斯模糊\n     */\n    public static Bitmap stackBlur(Bitmap srcBitmap) {\n        if (srcBitmap == null) return null;\n        RenderScript rs = RenderScript.create(MApplication.getInstance());\n        Bitmap blurredBitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);\n\n        //分配用于渲染脚本的内存\n        Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED);\n        Allocation output = Allocation.createTyped(rs, input.getType());\n\n        //加载我们想要使用的特定脚本的实例。\n        ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));\n        script.setInput(input);\n\n        //设置模糊半径\n        script.setRadius(8);\n\n        //启动 ScriptIntrinsicBlur\n        script.forEach(output);\n\n        //将输出复制到模糊的位图\n        output.copyTo(blurredBitmap);\n\n        return blurredBitmap;\n    }\n\n\n    public static Bitmap addBitmap(Bitmap first, Bitmap second,int spacer_mid,int spacer_width,int spacer_top,int spacer_bottom) {\n        int width = Math.max(first.getWidth(),second.getWidth())+spacer_width*2;\n        int height = first.getHeight() + second.getHeight() + spacer_mid+spacer_bottom+spacer_top;\n        Bitmap result = Bitmap.createBitmap(width, height, Config.ARGB_8888);\n        Canvas canvas = new Canvas(result);\n        canvas.drawColor(Color.WHITE);\n        canvas.drawBitmap(first, (width-first.getWidth())/2, spacer_top, null);\n        canvas.drawBitmap(second, (width-second.getWidth())/2,first.getHeight()+spacer_top+spacer_mid,  null);\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ColorUtils.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.graphics.Color\nimport androidx.annotation.ColorInt\nimport androidx.annotation.FloatRange\nimport java.util.*\nimport kotlin.math.*\n\n@Suppress(\"unused\")\nobject ColorUtils {\n\n    fun intToString(intColor: Int): String {\n        return String.format(\"#%06X\", 0xFFFFFF and intColor)\n    }\n\n    @JvmStatic\n    fun stripAlpha(@ColorInt color: Int): Int {\n        return -0x1000000 or color\n    }\n\n    @JvmStatic\n    @ColorInt\n    fun shiftColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 2.0) by: Float): Int {\n        if (by == 1f) return color\n        val alpha = Color.alpha(color)\n        val hsv = FloatArray(3)\n        Color.colorToHSV(color, hsv)\n        hsv[2] *= by // value component\n        return (alpha shl 24) + (0x00ffffff and Color.HSVToColor(hsv))\n    }\n\n    @JvmStatic\n    @ColorInt\n    fun darkenColor(@ColorInt color: Int): Int {\n        return shiftColor(color, 0.9f)\n    }\n\n    @ColorInt\n    fun lightenColor(@ColorInt color: Int): Int {\n        return shiftColor(color, 1.1f)\n    }\n\n    @JvmStatic\n    fun isColorLight(@ColorInt color: Int): Boolean {\n        val darkness =\n            1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255\n        return darkness < 0.4\n    }\n\n    @ColorInt\n    fun invertColor(@ColorInt color: Int): Int {\n        val r = 255 - Color.red(color)\n        val g = 255 - Color.green(color)\n        val b = 255 - Color.blue(color)\n        return Color.argb(Color.alpha(color), r, g, b)\n    }\n\n    @JvmStatic\n    @ColorInt\n    fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int {\n        val alpha = (Color.alpha(color) * factor).roundToInt()\n        val red = Color.red(color)\n        val green = Color.green(color)\n        val blue = Color.blue(color)\n        return Color.argb(alpha, red, green, blue)\n    }\n\n    @ColorInt\n    fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int {\n        val a = min(255, max(0, (alpha * 255).toInt())) shl 24\n        val rgb = 0x00ffffff and baseColor\n        return a + rgb\n    }\n\n    /**\n     * Taken from CollapsingToolbarLayout's CollapsingTextHelper class.\n     */\n    fun blendColors(color1: Int, color2: Int, @FloatRange(from = 0.0, to = 1.0) ratio: Float): Int {\n        val inverseRatio = 1f - ratio\n        val a = Color.alpha(color1) * inverseRatio + Color.alpha(color2) * ratio\n        val r = Color.red(color1) * inverseRatio + Color.red(color2) * ratio\n        val g = Color.green(color1) * inverseRatio + Color.green(color2) * ratio\n        val b = Color.blue(color1) * inverseRatio + Color.blue(color2) * ratio\n        return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())\n    }\n\n    /**\n     * 按条件的到随机颜色\n     *\n     * @param alpha 透明\n     * @param lower 下边界\n     * @param upper 上边界\n     * @return 颜色值\n     */\n    fun getRandomColor(alpha: Int, lower: Int, upper: Int): Int {\n        return RandomColor(alpha, lower, upper).color\n    }\n\n    /**\n     * @return 获取随机色\n     */\n    fun getRandomColor(): Int {\n        return RandomColor(255, 80, 200).color\n    }\n\n\n    /**\n     * 随机颜色\n     */\n    class RandomColor(alpha: Int, lower: Int, upper: Int) {\n        private var alpha: Int = 0\n        private var lower: Int = 0\n        private var upper: Int = 0\n\n        //随机数是前闭  后开\n        val color: Int\n            get() {\n                val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)\n                val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)\n                val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)\n\n                return Color.argb(getAlpha(), red, green, blue)\n            }\n\n        init {\n            require(upper > lower) { \"must be lower < upper\" }\n            setAlpha(alpha)\n            setLower(lower)\n            setUpper(upper)\n        }\n\n        private fun getAlpha(): Int {\n            return alpha\n        }\n\n        private fun setAlpha(alpha: Int) {\n            var alpha1 = alpha\n            if (alpha1 > 255) alpha1 = 255\n            if (alpha1 < 0) alpha1 = 0\n            this.alpha = alpha1\n        }\n\n        private fun getLower(): Int {\n            return lower\n        }\n\n        private fun setLower(lower: Int) {\n            var lower1 = lower\n            if (lower1 < 0) lower1 = 0\n            this.lower = lower1\n        }\n\n        private fun getUpper(): Int {\n            return upper\n        }\n\n        private fun setUpper(upper: Int) {\n            var upper1 = upper\n            if (upper1 > 255) upper1 = 255\n            this.upper = upper1\n        }\n    }\n\n    fun argb(R: Int, G: Int, B: Int): Int {\n        return argb(Byte.MAX_VALUE.toInt(), R, G, B)\n    }\n\n    fun argb(A: Int, R: Int, G: Int, B: Int): Int {\n        val colorByteArr =\n            byteArrayOf(A.toByte(), R.toByte(), G.toByte(), B.toByte())\n        return byteArrToInt(colorByteArr)\n    }\n\n    fun rgb(argb: Int): IntArray {\n        return intArrayOf(argb shr 16 and 0xFF, argb shr 8 and 0xFF, argb and 0xFF)\n    }\n\n    fun byteArrToInt(colorByteArr: ByteArray): Int {\n        return ((colorByteArr[0].toInt() shl 24) + (colorByteArr[1].toInt() and 0xFF shl 16)\n                + (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF))\n    }\n\n    fun rgb2lab(R: Int, G: Int, B: Int): IntArray {\n        val x: Float\n        val y: Float\n        val z: Float\n        val fx: Float\n        val fy: Float\n        val fz: Float\n        val xr: Float\n        val yr: Float\n        val zr: Float\n        val eps = 216f / 24389f\n        val k = 24389f / 27f\n        val xr1 = 0.964221f // reference white D50\n        val yr1 = 1.0f\n        val zr1 = 0.825211f\n\n        // RGB to XYZ\n        var r: Float = R / 255f //R 0..1\n        var g: Float = G / 255f //G 0..1\n        var b: Float = B / 255f //B 0..1\n\n        // assuming sRGB (D65)\n        r = if (r <= 0.04045) r / 12 else ((r + 0.055) / 1.055).pow(2.4).toFloat()\n        g = if (g <= 0.04045) g / 12 else ((g + 0.055) / 1.055).pow(2.4).toFloat()\n        b = if (b <= 0.04045) b / 12 else ((b + 0.055) / 1.055).pow(2.4).toFloat()\n        x = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b\n        y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b\n        z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b\n\n        // XYZ to Lab\n        xr = x / xr1\n        yr = y / yr1\n        zr = z / zr1\n        fx = if (xr > eps) xr.toDouble().pow(1 / 3.0)\n            .toFloat() else ((k * xr + 16.0) / 116.0).toFloat()\n        fy = if (yr > eps) yr.toDouble().pow(1 / 3.0)\n            .toFloat() else ((k * yr + 16.0) / 116.0).toFloat()\n        fz = if (zr > eps) zr.toDouble().pow(1 / 3.0)\n            .toFloat() else ((k * zr + 16.0) / 116).toFloat()\n        val ls: Float = 116 * fy - 16\n        val `as`: Float = 500 * (fx - fy)\n        val bs: Float = 200 * (fy - fz)\n        val lab = IntArray(3)\n        lab[0] = (2.55 * ls + .5).toInt()\n        lab[1] = (`as` + .5).toInt()\n        lab[2] = (bs + .5).toInt()\n        return lab\n    }\n\n    /**\n     * Computes the difference between two RGB colors by converting them to the L*a*b scale and\n     * comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}\n     */\n    fun getColorDifference(a: Int, b: Int): Double {\n        val r1: Int = Color.red(a)\n        val g1: Int = Color.green(a)\n        val b1: Int = Color.blue(a)\n        val r2: Int = Color.red(b)\n        val g2: Int = Color.green(b)\n        val b2: Int = Color.blue(b)\n        val lab1 = rgb2lab(r1, g1, b1)\n        val lab2 = rgb2lab(r2, g2, b2)\n        return sqrt(\n            (lab2[0] - lab1[0].toDouble())\n                .pow(2.0) + (lab2[1] - lab1[1].toDouble())\n                .pow(2.0) + (lab2[2] - lab1[2].toDouble())\n                .pow(2.0)\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ContextExtensions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.PendingIntent\nimport android.app.PendingIntent.*\nimport android.app.Service\nimport android.content.*\nimport android.content.pm.PackageManager\nimport android.content.res.ColorStateList\nimport android.content.res.Configuration\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport android.os.BatteryManager\nimport android.provider.Settings\nimport androidx.annotation.ColorRes\nimport androidx.annotation.DrawableRes\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.edit\nimport androidx.preference.PreferenceManager\nimport com.kunfei.bookshelf.R\nimport java.io.File\n\ninline fun <reified A : Activity> Context.startActivity(configIntent: Intent.() -> Unit = {}) {\n    val intent = Intent(this, A::class.java)\n    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n    intent.apply(configIntent)\n    startActivity(intent)\n}\n\ninline fun <reified T : Service> Context.startService(configIntent: Intent.() -> Unit = {}) {\n    startService(Intent(this, T::class.java).apply(configIntent))\n}\n\ninline fun <reified T : Service> Context.stopService() {\n    stopService(Intent(this, T::class.java))\n}\n\n@SuppressLint(\"UnspecifiedImmutableFlag\")\ninline fun <reified T : Service> Context.servicePendingIntent(\n    action: String,\n    configIntent: Intent.() -> Unit = {}\n): PendingIntent? {\n    val intent = Intent(this, T::class.java)\n    intent.action = action\n    configIntent.invoke(intent)\n    return getService(this, 0, intent, FLAG_UPDATE_CURRENT)\n}\n\n@SuppressLint(\"UnspecifiedImmutableFlag\")\ninline fun <reified T : Activity> Context.activityPendingIntent(\n    action: String,\n    configIntent: Intent.() -> Unit = {}\n): PendingIntent? {\n    val intent = Intent(this, T::class.java)\n    intent.action = action\n    configIntent.invoke(intent)\n    return getActivity(this, 0, intent, FLAG_UPDATE_CURRENT)\n}\n\n@SuppressLint(\"UnspecifiedImmutableFlag\")\ninline fun <reified T : BroadcastReceiver> Context.broadcastPendingIntent(\n    action: String,\n    configIntent: Intent.() -> Unit = {}\n): PendingIntent? {\n    val intent = Intent(this, T::class.java)\n    intent.action = action\n    configIntent.invoke(intent)\n    return getBroadcast(this, 0, intent, FLAG_CANCEL_CURRENT)\n}\n\nval Context.defaultSharedPreferences: SharedPreferences\n    get() = PreferenceManager.getDefaultSharedPreferences(this)\n\nfun Context.getPrefBoolean(key: String, defValue: Boolean = false) =\n    defaultSharedPreferences.getBoolean(key, defValue)\n\nfun Context.putPrefBoolean(key: String, value: Boolean = false) =\n    defaultSharedPreferences.edit { putBoolean(key, value) }\n\nfun Context.getPrefInt(key: String, defValue: Int = 0) =\n    defaultSharedPreferences.getInt(key, defValue)\n\nfun Context.putPrefInt(key: String, value: Int) =\n    defaultSharedPreferences.edit { putInt(key, value) }\n\nfun Context.getPrefLong(key: String, defValue: Long = 0L) =\n    defaultSharedPreferences.getLong(key, defValue)\n\nfun Context.putPrefLong(key: String, value: Long) =\n    defaultSharedPreferences.edit { putLong(key, value) }\n\nfun Context.getPrefString(key: String, defValue: String? = null) =\n    defaultSharedPreferences.getString(key, defValue)\n\nfun Context.putPrefString(key: String, value: String?) =\n    defaultSharedPreferences.edit { putString(key, value) }\n\nfun Context.getPrefStringSet(\n    key: String,\n    defValue: MutableSet<String>? = null\n): MutableSet<String>? = defaultSharedPreferences.getStringSet(key, defValue)\n\nfun Context.putPrefStringSet(key: String, value: MutableSet<String>) =\n    defaultSharedPreferences.edit { putStringSet(key, value) }\n\nfun Context.removePref(key: String) =\n    defaultSharedPreferences.edit { remove(key) }\n\n\nfun Context.getCompatColor(@ColorRes id: Int): Int = ContextCompat.getColor(this, id)\n\nfun Context.getCompatDrawable(@DrawableRes id: Int): Drawable? = ContextCompat.getDrawable(this, id)\n\nfun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? =\n    ContextCompat.getColorStateList(this, id)\n\n/**\n * 系统息屏时间\n */\nval Context.sysScreenOffTime: Int\n    get() {\n        var screenOffTime = 0\n        try {\n            screenOffTime =\n                Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT)\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        return screenOffTime\n    }\n\nval Context.statusBarHeight: Int\n    get() {\n        val resourceId = resources.getIdentifier(\"status_bar_height\", \"dimen\", \"android\")\n        return resources.getDimensionPixelSize(resourceId)\n    }\n\nval Context.navigationBarHeight: Int\n    get() {\n        val resourceId = resources.getIdentifier(\"navigation_bar_height\", \"dimen\", \"android\")\n        return resources.getDimensionPixelSize(resourceId)\n    }\n\nfun Context.share(text: String, title: String = getString(R.string.share)) {\n    kotlin.runCatching {\n        val intent = Intent(Intent.ACTION_SEND)\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        intent.putExtra(Intent.EXTRA_SUBJECT, title)\n        intent.putExtra(Intent.EXTRA_TEXT, text)\n        intent.type = \"text/plain\"\n        startActivity(Intent.createChooser(intent, title))\n    }\n}\n\nfun Context.sendToClip(text: String) {\n    val clipboard =\n        getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager\n    val clipData = ClipData.newPlainText(null, text)\n    clipboard?.let {\n        clipboard.setPrimaryClip(clipData)\n        longToastOnUi(R.string.copy_complete)\n    }\n}\n\nfun Context.getClipText(): String? {\n    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?\n    clipboard?.primaryClip?.let {\n        if (it.itemCount > 0) {\n            return it.getItemAt(0).text.toString().trim()\n        }\n    }\n    return null\n}\n\nfun Context.sendMail(mail: String) {\n    try {\n        val intent = Intent(Intent.ACTION_SENDTO)\n        intent.data = Uri.parse(\"mailto:$mail\")\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        startActivity(intent)\n    } catch (e: Exception) {\n        toastOnUi(e.localizedMessage ?: \"Error\")\n    }\n}\n\n/**\n * 系统是否暗色主题\n */\nfun Context.sysIsDarkMode(): Boolean {\n    val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK\n    return mode == Configuration.UI_MODE_NIGHT_YES\n}\n\n/**\n * 获取电量\n */\nval Context.sysBattery: Int\n    get() {\n        val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)\n        val batteryStatus = registerReceiver(null, iFilter)\n        return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1\n    }\n\nval Context.externalFiles: File\n    get() = this.getExternalFilesDir(null) ?: this.filesDir\n\nval Context.externalCache: File\n    get() = this.externalCacheDir ?: this.cacheDir\n\nfun Context.openUrl(url: String) {\n    openUrl(Uri.parse(url))\n}\n\nfun Context.openUrl(uri: Uri) {\n    val intent = Intent(Intent.ACTION_VIEW)\n    intent.data = uri\n    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n    if (intent.resolveActivity(packageManager) != null) {\n        try {\n            startActivity(intent)\n        } catch (e: Exception) {\n            toastOnUi(e.localizedMessage ?: \"open url error\")\n        }\n    } else {\n        try {\n            startActivity(Intent.createChooser(intent, \"请选择浏览器\"))\n        } catch (e: Exception) {\n            toastOnUi(e.localizedMessage ?: \"open url error\")\n        }\n    }\n}\n\nval Context.channel: String\n    get() {\n        try {\n            val pm = packageManager\n            val appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA)\n            return appInfo.metaData.getString(\"channel\") ?: \"\"\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n        return \"\"\n    }\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ConvertUtils.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.content.res.Resources\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.drawable.BitmapDrawable\nimport android.graphics.drawable.Drawable\nimport java.io.BufferedReader\nimport java.io.InputStream\nimport java.io.InputStreamReader\nimport java.text.DecimalFormat\n\n/**\n * 数据类型转换、单位转换\n *\n * @author 李玉江[QQ:1023694760]\n * @since 2014-4-18\n */\n@Suppress(\"MemberVisibilityCanBePrivate\")\nobject ConvertUtils {\n    const val GB: Long = 1073741824\n    const val MB: Long = 1048576\n    const val KB: Long = 1024\n\n    fun toInt(obj: Any): Int {\n        return kotlin.runCatching {\n            Integer.parseInt(obj.toString())\n        }.getOrDefault(-1)\n    }\n\n    fun toInt(bytes: ByteArray): Int {\n        var result = 0\n        var byte: Byte\n        for (i in bytes.indices) {\n            byte = bytes[i]\n            result += (byte.toInt() and 0xFF).shl(8 * i)\n        }\n        return result\n    }\n\n    fun toFloat(obj: Any): Float {\n        return kotlin.runCatching {\n            java.lang.Float.parseFloat(obj.toString())\n        }.getOrDefault(-1f)\n    }\n\n    fun toString(objects: Array<Any>, tag: String): String {\n        val sb = StringBuilder()\n        for (`object` in objects) {\n            sb.append(`object`)\n            sb.append(tag)\n        }\n        return sb.toString()\n    }\n\n    @JvmOverloads\n    fun toBitmap(bytes: ByteArray, width: Int = -1, height: Int = -1): Bitmap? {\n        var bitmap: Bitmap? = null\n        if (bytes.isNotEmpty()) {\n            kotlin.runCatching {\n                val options = BitmapFactory.Options()\n                // 设置让解码器以最佳方式解码\n                options.inPreferredConfig = null\n                if (width > 0 && height > 0) {\n                    options.outWidth = width\n                    options.outHeight = height\n                }\n                bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)\n                bitmap!!.density = 96// 96 dpi\n            }\n        }\n        return bitmap\n    }\n\n    private fun toDrawable(bitmap: Bitmap?): Drawable? {\n        return if (bitmap == null) null else BitmapDrawable(Resources.getSystem(), bitmap)\n    }\n\n    fun toDrawable(bytes: ByteArray): Drawable? {\n        return toDrawable(toBitmap(bytes))\n    }\n\n    fun toFileSizeString(fileSize: Long): String {\n        val df = DecimalFormat(\"0.00\")\n        val fileSizeString: String = when {\n            fileSize < KB -> fileSize.toString() + \"B\"\n            fileSize < MB -> df.format(fileSize.toDouble() / KB) + \"K\"\n            fileSize < GB -> df.format(fileSize.toDouble() / MB) + \"M\"\n            else -> df.format(fileSize.toDouble() / GB) + \"G\"\n        }\n        return fileSizeString\n    }\n\n    @JvmOverloads\n    fun toString(`is`: InputStream, charset: String = \"utf-8\"): String {\n        val sb = StringBuilder()\n        kotlin.runCatching {\n            val reader = BufferedReader(InputStreamReader(`is`, charset))\n            while (true) {\n                val line = reader.readLine()\n                if (line == null) {\n                    break\n                } else {\n                    sb.append(line).append(\"\\n\")\n                }\n            }\n            reader.close()\n            `is`.close()\n        }\n        return sb.toString()\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/DensityUtil.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.utils;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Point;\nimport android.util.DisplayMetrics;\nimport android.util.TypedValue;\nimport android.view.Display;\nimport android.view.WindowManager;\n\nimport java.lang.reflect.Method;\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class DensityUtil {\n    /**\n     * dp转px\n     */\n    public static int dp2px(Context context, float dpVal) {\n        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,\n                dpVal, context.getResources().getDisplayMetrics());\n    }\n\n    /**\n     * sp转px\n     */\n    public static int sp2px(Context context, float spVal) {\n        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,\n                spVal, context.getResources().getDisplayMetrics());\n    }\n\n    /**\n     * px转dp\n     */\n    public static float px2dp(Context context, float pxVal) {\n        final float scale = context.getResources().getDisplayMetrics().density;\n        return (pxVal / scale);\n    }\n\n    /**\n     * px转sp\n     */\n    public static float px2sp(Context context, float pxVal) {\n        return (pxVal / context.getResources().getDisplayMetrics().scaledDensity);\n    }\n\n    public static Point getDisplayPoint(Context context) {\n        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n        Display display = windowManager.getDefaultDisplay();\n        DisplayMetrics displayMetrics = new DisplayMetrics();\n        @SuppressWarnings(\"rawtypes\")\n        Class c;\n        try {\n            c = Class.forName(\"android.view.Display\");\n            @SuppressWarnings(\"unchecked\")\n            Method method = c.getMethod(\"getRealMetrics\", DisplayMetrics.class);\n            method.invoke(display, displayMetrics);\n            return new Point(displayMetrics.widthPixels, displayMetrics.heightPixels);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        DisplayMetrics dm = new DisplayMetrics();\n        ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(dm);\n        return new Point(dm.widthPixels, dm.heightPixels);\n    }\n\n    public static int getWindowWidth(Context context) {\n        WindowManager wm = (WindowManager) context\n                .getSystemService(Context.WINDOW_SERVICE);\n        return wm.getDefaultDisplay().getWidth();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/DialogExtensions.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.view.WindowManager\nimport androidx.appcompat.app.AlertDialog\nimport androidx.fragment.app.DialogFragment\nimport com.kunfei.bookshelf.utils.theme.ThemeStore\nimport com.kunfei.bookshelf.utils.theme.filletBackground\n\nfun AlertDialog.applyTint(): AlertDialog {\n    window?.setBackgroundDrawable(context.filletBackground)\n    val colorStateList = Selector.colorBuild()\n        .setDefaultColor(ThemeStore.accentColor(context))\n        .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(context)))\n        .create()\n    if (getButton(AlertDialog.BUTTON_NEGATIVE) != null) {\n        getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList)\n    }\n    if (getButton(AlertDialog.BUTTON_POSITIVE) != null) {\n        getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList)\n    }\n    if (getButton(AlertDialog.BUTTON_NEUTRAL) != null) {\n        getButton(AlertDialog.BUTTON_NEUTRAL).setTextColor(colorStateList)\n    }\n    return this\n}\n\nfun AlertDialog.requestInputMethod() {\n    window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)\n}\n\nfun DialogFragment.setLayout(widthMix: Float, heightMix: Float) {\n    val dm = requireActivity().windowSize\n    dialog?.window?.setLayout(\n        (dm.widthPixels * widthMix).toInt(),\n        (dm.heightPixels * heightMix).toInt()\n    )\n}\n\nfun DialogFragment.setLayout(width: Int, heightMix: Float) {\n    val dm = requireActivity().windowSize\n    dialog?.window?.setLayout(\n        width,\n        (dm.heightPixels * heightMix).toInt()\n    )\n}\n\nfun DialogFragment.setLayout(widthMix: Float, height: Int) {\n    val dm = requireActivity().windowSize\n    dialog?.window?.setLayout(\n        (dm.widthPixels * widthMix).toInt(),\n        height\n    )\n}\n\nfun DialogFragment.setLayout(width: Int, height: Int) {\n    dialog?.window?.setLayout(width, height)\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/DocumentExtensions.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.content.Context\nimport android.database.Cursor\nimport android.net.Uri\nimport android.provider.DocumentsContract\nimport androidx.documentfile.provider.DocumentFile\nimport splitties.init.appCtx\nimport java.io.File\nimport java.io.IOException\nimport java.nio.charset.Charset\nimport java.util.*\n\n\n@Suppress(\"MemberVisibilityCanBePrivate\")\nobject DocumentUtils {\n\n    fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean {\n        val parent = getDirDocument(root, *subDirs) ?: return false\n        return parent.findFile(fileName)?.exists() ?: false\n    }\n\n    fun delete(root: DocumentFile, fileName: String, vararg subDirs: String) {\n        val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)\n        parent?.findFile(fileName)?.delete()\n    }\n\n    fun createFileIfNotExist(\n        root: DocumentFile,\n        fileName: String,\n        mimeType: String = \"\",\n        vararg subDirs: String\n    ): DocumentFile? {\n        val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)\n        return parent?.createFile(mimeType, fileName)\n    }\n\n    fun createFolderIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? {\n        var parent: DocumentFile? = root\n        for (subDirName in subDirs) {\n            val subDir = parent?.findFile(subDirName)\n                ?: parent?.createDirectory(subDirName)\n            parent = subDir\n        }\n        return parent\n    }\n\n    fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? {\n        var parent = root\n        for (subDirName in subDirs) {\n            val subDir = parent.findFile(subDirName)\n            parent = subDir ?: return null\n        }\n        return parent\n    }\n\n    @JvmStatic\n    @Throws(Exception::class)\n    fun writeText(\n        context: Context,\n        data: String,\n        fileUri: Uri,\n        charset: Charset = Charsets.UTF_8\n    ): Boolean {\n        return writeBytes(context, data.toByteArray(charset), fileUri)\n    }\n\n    @JvmStatic\n    @Throws(Exception::class)\n    fun writeBytes(context: Context, data: ByteArray, fileUri: Uri): Boolean {\n        context.contentResolver.openOutputStream(fileUri)?.let {\n            it.write(data)\n            it.close()\n            return true\n        }\n        return false\n    }\n\n    @JvmStatic\n    @Throws(Exception::class)\n    fun readText(context: Context, uri: Uri): String {\n        return String(readBytes(context, uri))\n    }\n\n    @JvmStatic\n    @Throws(Exception::class)\n    fun readBytes(context: Context, uri: Uri): ByteArray {\n        context.contentResolver.openInputStream(uri)?.let {\n            val len: Int = it.available()\n            val buffer = ByteArray(len)\n            it.read(buffer)\n            it.close()\n            return buffer\n        } ?: throw IOException(\"打开文件失败\\n${uri}\")\n    }\n\n    @Throws(Exception::class)\n    fun listFiles(uri: Uri, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList<FileDoc> {\n        if (!uri.isContentScheme()) {\n            return listFiles(uri.path!!, filter)\n        }\n        val childrenUri = DocumentsContract\n            .buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))\n        val docList = arrayListOf<FileDoc>()\n        var cursor: Cursor? = null\n        try {\n            cursor = appCtx.contentResolver.query(\n                childrenUri, arrayOf(\n                    DocumentsContract.Document.COLUMN_DOCUMENT_ID,\n                    DocumentsContract.Document.COLUMN_DISPLAY_NAME,\n                    DocumentsContract.Document.COLUMN_LAST_MODIFIED,\n                    DocumentsContract.Document.COLUMN_SIZE,\n                    DocumentsContract.Document.COLUMN_MIME_TYPE\n                ), null, null, DocumentsContract.Document.COLUMN_DISPLAY_NAME\n            )\n            cursor?.let {\n                val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)\n                val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)\n                val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)\n                val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)\n                val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)\n                if (cursor.moveToFirst()) {\n                    do {\n                        val item = FileDoc(\n                            name = cursor.getString(nci),\n                            isDir = cursor.getString(mci) == DocumentsContract.Document.MIME_TYPE_DIR,\n                            size = cursor.getLong(sci),\n                            date = Date(cursor.getLong(dci)),\n                            uri = DocumentsContract\n                                .buildDocumentUriUsingTree(uri, cursor.getString(ici))\n                        )\n                        if (filter == null || filter.invoke(item)) {\n                            docList.add(item)\n                        }\n                    } while (cursor.moveToNext())\n                }\n            }\n        } finally {\n            cursor?.close()\n        }\n        return docList\n    }\n\n    @Throws(Exception::class)\n    fun listFiles(path: String, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList<FileDoc> {\n        val docList = arrayListOf<FileDoc>()\n        val file = File(path)\n        file.listFiles()?.forEach {\n            val item = FileDoc(\n                it.name,\n                it.isDirectory,\n                it.length(),\n                Date(it.lastModified()),\n                Uri.fromFile(it)\n            )\n            if (filter == null || filter.invoke(item)) {\n                docList.add(item)\n            }\n        }\n        return docList\n    }\n\n}\n\ndata class FileDoc(\n    val name: String,\n    val isDir: Boolean,\n    val size: Long,\n    val date: Date,\n    val uri: Uri\n) {\n\n    override fun toString(): String {\n        return if (uri.isContentScheme()) uri.toString() else uri.path!!\n    }\n\n    val isContentScheme get() = uri.isContentScheme()\n\n    fun readBytes(): ByteArray {\n        return uri.readBytes(appCtx)\n    }\n\n    companion object {\n\n        fun fromDocumentFile(doc: DocumentFile): FileDoc {\n            return FileDoc(\n                name = doc.name ?: \"\",\n                isDir = doc.isDirectory,\n                size = doc.length(),\n                date = Date(doc.lastModified()),\n                uri = doc.uri\n            )\n        }\n\n        fun fromFile(file: File): FileDoc {\n            return FileDoc(\n                name = file.name,\n                isDir = file.isDirectory,\n                size = file.length(),\n                date = Date(file.lastModified()),\n                uri = Uri.fromFile(file)\n            )\n        }\n\n    }\n}\n\n@Throws(Exception::class)\nfun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {\n    DocumentUtils.writeText(context, data, this.uri, charset)\n}\n\n@Throws(Exception::class)\nfun DocumentFile.writeBytes(context: Context, data: ByteArray) {\n    DocumentUtils.writeBytes(context, data, this.uri)\n}\n\n@Throws(Exception::class)\nfun DocumentFile.readText(context: Context): String {\n    return DocumentUtils.readText(context, this.uri)\n}\n\n@Throws(Exception::class)\nfun DocumentFile.readBytes(context: Context): ByteArray {\n    return DocumentUtils.readBytes(context, this.uri)\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/DocumentUtil.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.content.Context;\nimport android.net.Uri;\n\nimport androidx.annotation.NonNull;\nimport androidx.documentfile.provider.DocumentFile;\n\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.regex.Pattern;\n\n/**\n * Created by PureDark on 2016/9/24.\n */\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class DocumentUtil {\n\n    private static Pattern FilePattern = Pattern.compile(\"[\\\\\\\\/:*?\\\"<>|]\");\n\n    public static boolean isFileExist(Context context, String fileName, String rootPath, String... subDirs) {\n        Uri rootUri;\n        if (rootPath.startsWith(\"content\"))\n            rootUri = Uri.parse(rootPath);\n        else\n            rootUri = Uri.parse(Uri.decode(rootPath));\n        return isFileExist(context, fileName, rootUri, subDirs);\n    }\n\n    public static boolean isFileExist(Context context, String fileName, Uri rootUri, String... subDirs) {\n        DocumentFile root;\n        if (\"content\".equals(rootUri.getScheme()))\n            root = DocumentFile.fromTreeUri(context, rootUri);\n        else\n            root = DocumentFile.fromFile(new File(rootUri.getPath()));\n        return isFileExist(fileName, root, subDirs);\n    }\n\n    public static boolean isFileExist(String fileName, DocumentFile root, String... subDirs) {\n        DocumentFile parent = getDirDocument(root, subDirs);\n        if (parent == null)\n            return false;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        if (file != null && file.exists())\n            return true;\n        return false;\n    }\n\n    public static DocumentFile createDirIfNotExist(Context context, String rootPath, String... subDirs) {\n        Uri rootUri;\n        if (rootPath.startsWith(\"content\"))\n            rootUri = Uri.parse(rootPath);\n        else\n            rootUri = Uri.parse(Uri.decode(rootPath));\n        return createDirIfNotExist(context, rootUri, subDirs);\n    }\n\n    public static DocumentFile createDirIfNotExist(Context context, Uri rootUri, String... subDirs) {\n        DocumentFile root;\n        if (\"content\".equals(rootUri.getScheme()))\n            root = DocumentFile.fromTreeUri(context, rootUri);\n        else\n            root = DocumentFile.fromFile(new File(rootUri.getPath()));\n        return createDirIfNotExist(root, subDirs);\n    }\n\n    public static DocumentFile createDirIfNotExist(@NonNull DocumentFile root, String... subDirs) {\n        DocumentFile parent = root;\n        try {\n            for (String subDir1 : subDirs) {\n                String subDirName = filenameFilter(Uri.decode(subDir1));\n                DocumentFile subDir = parent.findFile(subDirName);\n                if (subDir == null) {\n                    subDir = parent.createDirectory(subDirName);\n                }\n                parent = subDir;\n            }\n        } catch (Exception e) {\n            e.printStackTrace();\n            return null;\n        }\n        return parent;\n    }\n\n    public static DocumentFile createFileIfNotExist(Context context, String fileName, String rootPath, String... subDirs) {\n        Uri rootUri;\n        if (rootPath.startsWith(\"content\"))\n            rootUri = Uri.parse(rootPath);\n        else\n            rootUri = Uri.parse(Uri.decode(rootPath));\n        return createFileIfNotExist(context, \"\", fileName, rootUri, subDirs);\n    }\n\n    public static DocumentFile createFileIfNotExist(Context context, String fileName, Uri rootUri, String... subDirs) {\n        return createFileIfNotExist(context, \"\", fileName, rootUri, subDirs);\n    }\n\n    public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, String rootPath, String... subDirs) {\n        Uri rootUri;\n        if (rootPath.startsWith(\"content\"))\n            rootUri = Uri.parse(rootPath);\n        else\n            rootUri = Uri.parse(Uri.decode(rootPath));\n        return createFileIfNotExist(context, mimeType, fileName, rootUri, subDirs);\n    }\n\n    public static DocumentFile createFileIfNotExist(Context context, String mimeType, String fileName, Uri rootUri, String... subDirs) {\n        DocumentFile parent = createDirIfNotExist(context, rootUri, subDirs);\n        if (parent == null)\n            return null;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null) {\n            file = parent.createFile(mimeType, fileName);\n        }\n        return file;\n    }\n\n    public static boolean deleteFile(Context context, String fileName, String rootPath, String... subDirs) {\n        Uri rootUri;\n        if (rootPath.startsWith(\"content\"))\n            rootUri = Uri.parse(rootPath);\n        else\n            rootUri = Uri.parse(Uri.decode(rootPath));\n        return deleteFile(context, fileName, rootUri, subDirs);\n    }\n\n    public static boolean deleteFile(Context context, String fileName, Uri rootUri, String... subDirs) {\n        DocumentFile root;\n        if (\"content\".equals(rootUri.getScheme()))\n            root = DocumentFile.fromTreeUri(context, rootUri);\n        else\n            root = DocumentFile.fromFile(new File(rootUri.getPath()));\n        return deleteFile(fileName, root, subDirs);\n    }\n\n    public static boolean deleteFile(String fileName, DocumentFile root, String... subDirs) {\n        DocumentFile parent = getDirDocument(root, subDirs);\n        if (parent == null)\n            return false;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        return file != null && file.exists() && file.delete();\n    }\n\n    public static boolean writeBytes(Context context, byte[] data, String fileName, String rootPath, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootPath, subDirs);\n        if (parent == null)\n            return false;\n        DocumentFile file = parent.findFile(fileName);\n        return writeBytes(context, data, file.getUri());\n    }\n\n    public static boolean writeBytes(Context context, byte[] data, String fileName, Uri rootUri, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootUri, subDirs);\n        if (parent == null)\n            return false;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        return writeBytes(context, data, file.getUri());\n    }\n\n    public static boolean writeBytes(Context context, byte[] data, String fileName, DocumentFile root, String... subDirs) {\n        DocumentFile parent = getDirDocument(root, subDirs);\n        if (parent == null)\n            return false;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        return writeBytes(context, data, file.getUri());\n    }\n\n    public static boolean writeBytes(Context context, byte[] data, DocumentFile file) {\n        return writeBytes(context, data, file.getUri());\n    }\n\n    public static boolean writeBytes(Context context, byte[] data, Uri fileUri) {\n        try {\n            OutputStream out = context.getContentResolver().openOutputStream(fileUri, \"wt\"); //Write file need open with truncate mode, the mode truncate file upon opening (to zero bytes)\n            out.write(data);\n            out.close();\n            return true;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return false;\n    }\n\n    public static boolean writeFromInputStream(Context context, InputStream inStream, DocumentFile file) {\n        return writeFromInputStream(context, inStream, file.getUri());\n    }\n\n    public static boolean writeFromInputStream(Context context, InputStream inStream, Uri fileUri) {\n        try {\n            OutputStream out = context.getContentResolver().openOutputStream(fileUri);\n            int byteread;\n            byte[] buffer = new byte[1024];\n            while ((byteread = inStream.read(buffer)) > 0) {\n                out.write(buffer, 0, byteread);\n            }\n            inStream.close();\n            out.close();\n            return true;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return false;\n    }\n\n    public static byte[] readBytes(Context context, String fileName, String rootPath, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootPath, subDirs);\n        if (parent == null)\n            return null;\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return readBytes(context, file.getUri());\n    }\n\n    public static byte[] readBytes(Context context, String fileName, Uri rootUri, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootUri, subDirs);\n        if (parent == null)\n            return null;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return readBytes(context, file.getUri());\n    }\n\n    public static byte[] readBytes(Context context, String fileName, DocumentFile root, String... subDirs) {\n        DocumentFile parent = getDirDocument(root, subDirs);\n        if (parent == null)\n            return null;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return readBytes(context, file.getUri());\n    }\n\n    public static byte[] readBytes(Context context, DocumentFile file) {\n        if (file == null)\n            return null;\n        return readBytes(context, file.getUri());\n    }\n\n    public static byte[] readBytes(Context context, Uri fileUri) {\n        try {\n            InputStream fis = context.getContentResolver().openInputStream(fileUri);\n            int len = fis.available();\n            byte[] buffer = new byte[len];\n            fis.read(buffer);\n            fis.close();\n            return buffer;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public static DocumentFile getDirDocument(Context context, String rootPath, String... subDirs) {\n        Uri rootUri;\n        if (rootPath.startsWith(\"content\"))\n            rootUri = Uri.parse(rootPath);\n        else\n            rootUri = Uri.parse(Uri.decode(rootPath));\n        return getDirDocument(context, rootUri, subDirs);\n    }\n\n    public static DocumentFile getDirDocument(Context context, Uri rootUri, String... subDirs) {\n        DocumentFile root;\n        if (\"content\".equals(rootUri.getScheme()))\n            root = DocumentFile.fromTreeUri(context, rootUri);\n        else\n            root = DocumentFile.fromFile(new File(rootUri.getPath()));\n        return getDirDocument(root, subDirs);\n    }\n\n    public static DocumentFile getDirDocument(DocumentFile root, String... subDirs) {\n        DocumentFile parent = root;\n        for (int i = 0; i < subDirs.length; i++) {\n            String subDirName = Uri.decode(subDirs[i]);\n            DocumentFile subDir = parent.findFile(subDirName);\n            if (subDir != null)\n                parent = subDir;\n            else\n                return null;\n        }\n        return parent;\n    }\n\n    public static OutputStream getFileOutputSteam(Context context, String fileName, String rootPath, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootPath, subDirs);\n        if (parent == null)\n            return null;\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return getFileOutputSteam(context, file.getUri());\n    }\n\n    public static OutputStream getFileOutputSteam(Context context, String fileName, Uri rootUri, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootUri, subDirs);\n        if (parent == null)\n            return null;\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return getFileOutputSteam(context, file.getUri());\n    }\n\n    public static OutputStream getFileOutputSteam(Context context, String fileName, DocumentFile root, String... subDirs) {\n        DocumentFile parent = getDirDocument(root, subDirs);\n        if (parent == null)\n            return null;\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return getFileOutputSteam(context, file.getUri());\n    }\n\n    public static OutputStream getFileOutputSteam(Context context, DocumentFile file) {\n        return getFileOutputSteam(context, file.getUri());\n    }\n\n    public static OutputStream getFileOutputSteam(Context context, Uri fileUri) {\n        try {\n            OutputStream out = context.getContentResolver().openOutputStream(fileUri);\n            return out;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public static InputStream getFileInputSteam(Context context, String fileName, String rootPath, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootPath, subDirs);\n        if (parent == null)\n            return null;\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return getFileInputSteam(context, file.getUri());\n    }\n\n    public static InputStream getFileInputSteam(Context context, String fileName, Uri rootUri, String... subDirs) {\n        DocumentFile parent = getDirDocument(context, rootUri, subDirs);\n        if (parent == null)\n            return null;\n        fileName = filenameFilter(Uri.decode(fileName));\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return getFileInputSteam(context, file.getUri());\n    }\n\n    public static InputStream getFileInputSteam(Context context, String fileName, DocumentFile root, String... subDirs) {\n        DocumentFile parent = getDirDocument(root, subDirs);\n        if (parent == null)\n            return null;\n        DocumentFile file = parent.findFile(fileName);\n        if (file == null)\n            return null;\n        return getFileInputSteam(context, file.getUri());\n    }\n\n    public static InputStream getFileInputSteam(Context context, DocumentFile file) {\n        return getFileInputSteam(context, file.getUri());\n    }\n\n    public static InputStream getFileInputSteam(Context context, Uri fileUri) {\n        try {\n            InputStream in = context.getContentResolver().openInputStream(fileUri);\n            return in;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    public static String filenameFilter(String str) {\n        return str == null ? null : FilePattern.matcher(str).replaceAll(\"_\");\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/DrawableUtil.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.content.res.ColorStateList\nimport android.graphics.PorterDuff\nimport android.graphics.drawable.ColorDrawable\nimport android.graphics.drawable.Drawable\nimport android.graphics.drawable.TransitionDrawable\nimport androidx.annotation.ColorInt\nimport androidx.core.graphics.drawable.DrawableCompat\n\n/**\n * @author Karim Abou Zeid (kabouzeid)\n */\nobject DrawableUtil {\n    fun createTransitionDrawable(\n        @ColorInt startColor: Int,\n        @ColorInt endColor: Int\n    ): TransitionDrawable {\n        return createTransitionDrawable(ColorDrawable(startColor), ColorDrawable(endColor))\n    }\n\n    @JvmStatic\n    fun createTransitionDrawable(start: Drawable?, end: Drawable?): TransitionDrawable {\n        val drawables = arrayOfNulls<Drawable>(2)\n        drawables[0] = start\n        drawables[1] = end\n        return TransitionDrawable(drawables)\n    }\n}\n\nfun Drawable.setTintList(\n    tint: ColorStateList,\n    tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP\n) {\n    val wrappedDrawable = DrawableCompat.wrap(this)\n    wrappedDrawable.mutate()\n    DrawableCompat.setTintMode(wrappedDrawable, tintMode)\n    DrawableCompat.setTintList(wrappedDrawable, tint)\n}\n\nfun Drawable.setTint(\n    @ColorInt tint: Int,\n    tintMode: PorterDuff.Mode = PorterDuff.Mode.SRC_ATOP\n) {\n    val wrappedDrawable = DrawableCompat.wrap(this)\n    wrappedDrawable.mutate()\n    DrawableCompat.setTintMode(wrappedDrawable, tintMode)\n    DrawableCompat.setTint(wrappedDrawable, tint)\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/EncoderUtils.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.util.Base64\nimport java.nio.charset.StandardCharsets\nimport java.security.spec.AlgorithmParameterSpec\nimport javax.crypto.Cipher\nimport javax.crypto.spec.IvParameterSpec\nimport javax.crypto.spec.SecretKeySpec\n\n@Suppress(\"unused\")\nobject EncoderUtils {\n\n    fun escape(src: String): String {\n        val tmp = StringBuilder()\n        for (char in src) {\n            val charCode = char.code\n            if (charCode in 48..57 || charCode in 65..90 || charCode in 97..122) {\n                tmp.append(char)\n                continue\n            }\n\n            val prefix = when {\n                charCode < 16 -> \"%0\"\n                charCode < 256 -> \"%\"\n                else -> \"%u\"\n            }\n            tmp.append(prefix).append(charCode.toString(16))\n        }\n        return tmp.toString()\n    }\n\n    fun base64Decode(str: String): String {\n        val bytes = Base64.decode(str, Base64.DEFAULT)\n        return try {\n            String(bytes, StandardCharsets.UTF_8)\n        } catch (e: Exception) {\n            String(bytes)\n        }\n    }\n\n    fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? {\n        return Base64.encodeToString(str.toByteArray(), flags)\n    }\n\n    //////////AES Start\n\n    /**\n     * Return the Base64-encode bytes of AES encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the Base64-encode bytes of AES encryption\n     */\n    fun encryptAES2Base64(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String?,\n        iv: ByteArray?\n    ): ByteArray? {\n        return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP)\n    }\n\n    /**\n     * Return the bytes of AES encryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES encryption\n     */\n    fun encryptAES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String?,\n        iv: ByteArray?\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"AES\", transformation!!, iv, true)\n    }\n\n\n    /**\n     * Return the bytes of AES decryption for Base64-encode bytes.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption for Base64-encode bytes\n     */\n    fun decryptBase64AES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DES/CBC/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)\n    }\n\n    /**\n     * Return the bytes of AES decryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param transformation The name of the transformation, e.g., *DES/CBC/PKCS5Padding*.\n     * @param iv             The buffer with the IV. The contents of the\n     * buffer are copied to protect against subsequent modification.\n     * @return the bytes of AES decryption\n     */\n    fun decryptAES(\n        data: ByteArray?,\n        key: ByteArray?,\n        transformation: String = \"DES/CBC/PKCS5Padding\",\n        iv: ByteArray? = null\n    ): ByteArray? {\n        return symmetricTemplate(data, key, \"AES\", transformation, iv, false)\n    }\n\n\n    /**\n     * Return the bytes of symmetric encryption or decryption.\n     *\n     * @param data           The data.\n     * @param key            The key.\n     * @param algorithm      The name of algorithm.\n     * @param transformation The name of the transformation, e.g., <i>DES/CBC/PKCS5Padding</i>.\n     * @param isEncrypt      True to encrypt, false otherwise.\n     * @return the bytes of symmetric encryption or decryption\n     */\n    @Suppress(\"SameParameterValue\")\n    private fun symmetricTemplate(\n        data: ByteArray?,\n        key: ByteArray?,\n        algorithm: String,\n        transformation: String,\n        iv: ByteArray?,\n        isEncrypt: Boolean\n    ): ByteArray? {\n        return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null\n        else try {\n            val keySpec = SecretKeySpec(key, algorithm)\n            val cipher = Cipher.getInstance(transformation)\n            if (iv == null || iv.isEmpty()) {\n                cipher.init(if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE, keySpec)\n            } else {\n                val params: AlgorithmParameterSpec = IvParameterSpec(iv)\n                cipher.init(\n                    if (isEncrypt) Cipher.ENCRYPT_MODE else Cipher.DECRYPT_MODE,\n                    keySpec,\n                    params\n                )\n            }\n            cipher.doFinal(data)\n        } catch (e: Throwable) {\n            e.printStackTrace()\n            null\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/EncodingDetect.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport androidx.annotation.NonNull;\n\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.select.Elements;\n\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.net.URL;\n\nimport static android.text.TextUtils.isEmpty;\n\n/**\n * <Detect encoding .> Copyright (C) <2009> <Fluck,ACC http://androidos.cc/dev>\n * <p>\n * This program is free software: you can redistribute it and/or modify it under\n * the terms of the GNU General Public License as published by the Free Software\n * Foundation, either version 3 of the License, or (at your option) any later\n * version.\n * <p>\n * This program is distributed in the hope that it will be useful, but WITHOUT\n * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more\n * details.\n * <p>\n * EncodingDetect.java<br>\n * 自动获取文件的编码\n *\n * @author Billows.Van\n * @version 1.0\n * @since Create on 2010-01-27 11:19:00\n */\npublic class EncodingDetect {\n\n    public static String getEncodeInHtml(@NonNull byte[] bytes) {\n        try {\n            String charsetStr = \"UTF-8\";\n            Document doc = Jsoup.parse(new String(bytes, charsetStr));\n            int a = doc.childNode(0).toString().indexOf(\"encoding\");\n            if (a > 0) {\n                String e = doc.childNode(0).toString().substring(a);\n                int b = e.indexOf('\"');\n                int c = e.indexOf('\"', b + 1);\n                return e.substring(b + 1, c);\n            }\n            Elements metaTags = doc.getElementsByTag(\"meta\");\n            for (Element metaTag : metaTags) {\n                String content = metaTag.attr(\"content\");\n                String http_equiv = metaTag.attr(\"http-equiv\");\n                charsetStr = metaTag.attr(\"charset\");\n                if (!charsetStr.isEmpty()) {\n                    if (!isEmpty(charsetStr)) {\n                        return charsetStr;\n                    }\n                }\n                if (http_equiv.toLowerCase().equals(\"content-type\")) {\n                    if (content.toLowerCase().contains(\"charset\")) {\n                        charsetStr = content.substring(content.toLowerCase().indexOf(\"charset\") + \"charset=\".length());\n                    } else {\n                        charsetStr = content.substring(content.toLowerCase().indexOf(\";\") + 1);\n                    }\n                    if (!isEmpty(charsetStr)) {\n                        return charsetStr;\n                    }\n                }\n            }\n        } catch (Exception ignored) {\n        }\n        return getJavaEncode(bytes);\n    }\n\n    public static String getJavaEncode(@NonNull byte[] bytes) {\n        int len = bytes.length > 2000 ? 2000 : bytes.length;\n        byte[] cBytes = new byte[len];\n        System.arraycopy(bytes, 0, cBytes, 0, len);\n        BytesEncodingDetect bytesEncodingDetect = new BytesEncodingDetect();\n        String code = BytesEncodingDetect.javaname[bytesEncodingDetect.detectEncoding(cBytes)];\n        // UTF-16LE 特殊处理\n        if (\"Unicode\".equals(code)) {\n            if (cBytes[0] == -1) {\n                code = \"UTF-16LE\";\n            }\n        }\n        return code;\n    }\n\n    /**\n     * 得到文件的编码\n     */\n    public static String getJavaEncode(@NonNull String filePath) {\n        BytesEncodingDetect s = new BytesEncodingDetect();\n        String fileCode = BytesEncodingDetect.javaname[s\n                .detectEncoding(new File(filePath))];\n\n        // UTF-16LE 特殊处理\n        if (\"Unicode\".equals(fileCode)) {\n            byte[] tempByte = BytesEncodingDetect.getFileBytes(new File(\n                    filePath));\n            if (tempByte[0] == -1) {\n                fileCode = \"UTF-16LE\";\n            }\n        }\n        return fileCode;\n    }\n\n    /**\n     * 得到文件的编码\n     */\n    public static String getJavaEncode(@NonNull File file) {\n        BytesEncodingDetect s = new BytesEncodingDetect();\n        String fileCode = BytesEncodingDetect.javaname[s.detectEncoding(file)];\n        // UTF-16LE 特殊处理\n        if (\"Unicode\".equals(fileCode)) {\n            byte[] tempByte = BytesEncodingDetect.getFileBytes(file);\n            if (tempByte[0] == -1) {\n                fileCode = \"UTF-16LE\";\n            }\n        }\n        return fileCode;\n    }\n\n}\n\nclass BytesEncodingDetect extends Encoding {\n    // Frequency tables to hold the GB, Big5, and EUC-TW character\n    // frequencies\n    private int[][] GBFreq;\n\n    private int[][] GBKFreq;\n\n    private int[][] Big5Freq;\n\n    private int[][] Big5PFreq;\n\n    private int[][] EUC_TWFreq;\n\n    private int[][] KRFreq;\n\n    private int[][] JPFreq;\n\n    // int UnicodeFreq[94][128];\n    // public static String[] nicename;\n    // public static String[] codings;\n    public boolean debug;\n\n    public BytesEncodingDetect() {\n        super();\n        debug = false;\n        GBFreq = new int[94][94];\n        GBKFreq = new int[126][191];\n        Big5Freq = new int[94][158];\n        Big5PFreq = new int[126][191];\n        EUC_TWFreq = new int[94][94];\n        KRFreq = new int[94][94];\n        JPFreq = new int[94][94];\n        // Initialize the Frequency Table for GB, GBK, Big5, EUC-TW, KR, JP\n        initialize_frequencies();\n    }\n\n    /**\n     * Function : detectEncoding Aruguments: URL Returns : One of the encodings\n     * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER)\n     * Description: This function looks at the URL contents and assigns it a\n     * probability score for each encoding type. The encoding type with the\n     * highest probability is returned.\n     */\n    public int detectEncoding(URL testurl) {\n        byte[] rawtext = new byte[10000];\n        int bytesread = 0, byteoffset = 0;\n        int guess = OTHER;\n        InputStream chinesestream;\n        try {\n            chinesestream = testurl.openStream();\n            while ((bytesread = chinesestream.read(rawtext, byteoffset,\n                    rawtext.length - byteoffset)) > 0) {\n                byteoffset += bytesread;\n            }\n            ;\n            chinesestream.close();\n            guess = detectEncoding(rawtext);\n        } catch (Exception e) {\n            System.err.println(\"Error loading or using URL \" + e.toString());\n            guess = -1;\n        }\n        return guess;\n    }\n\n    /**\n     * Function : detectEncoding Aruguments: File Returns : One of the encodings\n     * from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII, or OTHER)\n     * Description: This function looks at the file and assigns it a probability\n     * score for each encoding type. The encoding type with the highest\n     * probability is returned.\n     */\n    public int detectEncoding(File testfile) {\n        byte[] rawtext = getFileBytes(testfile);\n        return detectEncoding(rawtext);\n    }\n\n    public static byte[] getFileBytes(File testfile) {\n        FileInputStream chinesefile;\n        byte[] rawtext;\n        rawtext = new byte[2000];\n        try {\n            chinesefile = new FileInputStream(testfile);\n            chinesefile.read(rawtext);\n            chinesefile.close();\n        } catch (Exception e) {\n            System.err.println(\"Error: \" + e);\n        }\n        return rawtext;\n    }\n\n\n    /**\n     * Function : detectEncoding Aruguments: byte array Returns : One of the\n     * encodings from the Encoding enumeration (GB2312, HZ, BIG5, EUC_TW, ASCII,\n     * or OTHER) Description: This function looks at the byte array and assigns\n     * it a probability score for each encoding type. The encoding type with the\n     * highest probability is returned.\n     */\n    public int detectEncoding(byte[] rawtext) {\n        int[] scores;\n        int index, maxscore = 0;\n        int encoding_guess = OTHER;\n        scores = new int[TOTALTYPES];\n        // Assign Scores\n        scores[GB2312] = gb2312_probability(rawtext);\n        scores[GBK] = gbk_probability(rawtext);\n        scores[GB18030] = gb18030_probability(rawtext);\n        scores[HZ] = hz_probability(rawtext);\n        scores[BIG5] = big5_probability(rawtext);\n        scores[CNS11643] = euc_tw_probability(rawtext);\n        scores[ISO2022CN] = iso_2022_cn_probability(rawtext);\n        scores[UTF8] = utf8_probability(rawtext);\n        scores[UNICODE] = utf16_probability(rawtext);\n        scores[EUC_KR] = euc_kr_probability(rawtext);\n        scores[CP949] = cp949_probability(rawtext);\n        scores[JOHAB] = 0;\n        scores[ISO2022KR] = iso_2022_kr_probability(rawtext);\n        scores[ASCII] = ascii_probability(rawtext);\n        scores[SJIS] = sjis_probability(rawtext);\n        scores[EUC_JP] = euc_jp_probability(rawtext);\n        scores[ISO2022JP] = iso_2022_jp_probability(rawtext);\n        scores[UNICODET] = 0;\n        scores[UNICODES] = 0;\n        scores[ISO2022CN_GB] = 0;\n        scores[ISO2022CN_CNS] = 0;\n        scores[OTHER] = 0;\n        // Tabulate Scores\n        for (index = 0; index < TOTALTYPES; index++) {\n            if (debug)\n                System.err.println(\"Encoding \" + nicename[index] + \" score \"\n                        + scores[index]);\n            if (scores[index] > maxscore) {\n                encoding_guess = index;\n                maxscore = scores[index];\n            }\n        }\n        // Return OTHER if nothing scored above 50\n        if (maxscore <= 50) {\n            encoding_guess = OTHER;\n        }\n        return encoding_guess;\n    }\n\n    /*\n     * Function: gb2312_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses GB-2312\n     * encoding\n     */\n    private int gb2312_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, gbchars = 1;\n        long gbfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (GBFreq[row][column] != 0) {\n                        gbfreq += GBFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        // In GB high-freq character range\n                        gbfreq += 200;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) gbchars / (float) dbchars);\n        freqval = 50 * ((float) gbfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: gbk_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses GBK\n     * encoding\n     */\n    private int gbk_probability(byte[] rawText) {\n        int i, rawTextLen = 0;\n        int dbChars = 1, gbChars = 1;\n        long gbFreq = 0, totalFreq = 1;\n        float rangeVal = 0, freqVal = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawTextLen = rawText.length;\n        for (i = 0; i < rawTextLen - 1; i++) {\n            // System.err.println(rawText[i]);\n            if (rawText[i] < 0) {\n                dbChars++;\n                if ((byte) 0xA1 <= rawText[i] && rawText[i] <= (byte) 0xF7\n                        && // Original GB range\n                        (byte) 0xA1 <= rawText[i + 1]\n                        && rawText[i + 1] <= (byte) 0xFE) {\n                    gbChars++;\n                    totalFreq += 500;\n                    row = rawText[i] + 256 - 0xA1;\n                    column = rawText[i + 1] + 256 - 0xA1;\n                    // System.out.println(\"original row \" + row + \" column \" +\n                    // column);\n                    if (GBFreq[row][column] != 0) {\n                        gbFreq += GBFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        gbFreq += 200;\n                    }\n                } else if ((byte) 0x81 <= rawText[i]\n                        && rawText[i] <= (byte) 0xFE && // Extended GB range\n                        (((byte) 0x80 <= rawText[i + 1] && rawText[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawText[i + 1] && rawText[i + 1] <= (byte) 0x7E))) {\n                    gbChars++;\n                    totalFreq += 500;\n                    row = rawText[i] + 256 - 0x81;\n                    if (0x40 <= rawText[i + 1] && rawText[i + 1] <= 0x7E) {\n                        column = rawText[i + 1] - 0x40;\n                    } else {\n                        column = rawText[i + 1] + 256 - 0x40;\n                    }\n                    // System.out.println(\"extended row \" + row + \" column \" +\n                    // column + \" rawtext[i] \" + rawtext[i]);\n                    if (GBKFreq[row][column] != 0) {\n                        gbFreq += GBKFreq[row][column];\n                    }\n                }\n                i++;\n            }\n        }\n        rangeVal = 50 * ((float) gbChars / (float) dbChars);\n        freqVal = 50 * ((float) gbFreq / (float) totalFreq);\n        // For regular GB files, this would give the same score, so I handicap\n        // it slightly\n        return (int) (rangeVal + freqVal) - 1;\n    }\n\n    /*\n     * Function: gb18030_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses GBK\n     * encoding\n     */\n    private int gb18030_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, gbchars = 1;\n        long gbfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawText[i]);\n            if (rawtext[i] < 0) {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xF7\n                        && // Original GB range\n                        i + 1 < rawtextlen && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    // System.out.println(\"original row \" + row + \" column \" +\n                    // column);\n                    if (GBFreq[row][column] != 0) {\n                        gbfreq += GBFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        gbfreq += 200;\n                    }\n                } else if ((byte) 0x81 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && // Extended GB range\n                        i + 1 < rawtextlen\n                        && (((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE) || ((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E))) {\n                    gbchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0x81;\n                    if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) {\n                        column = rawtext[i + 1] - 0x40;\n                    } else {\n                        column = rawtext[i + 1] + 256 - 0x40;\n                    }\n                    // System.out.println(\"extended row \" + row + \" column \" +\n                    // column + \" rawtext[i] \" + rawtext[i]);\n                    if (GBKFreq[row][column] != 0) {\n                        gbfreq += GBKFreq[row][column];\n                    }\n                } else if ((byte) 0x81 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && // Extended GB range\n                        i + 3 < rawtextlen && (byte) 0x30 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0x39\n                        && (byte) 0x81 <= rawtext[i + 2]\n                        && rawtext[i + 2] <= (byte) 0xFE\n                        && (byte) 0x30 <= rawtext[i + 3]\n                        && rawtext[i + 3] <= (byte) 0x39) {\n                    gbchars++;\n                    /*\n                     * totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40\n                     * <= rawtext[i+1] && rawtext[i+1] <= 0x7E) { column =\n                     * rawtext[i+1] - 0x40; } else { column = rawtext[i+1] + 256\n                     * - 0x40; } //System.out.println(\"extended row \" + row + \"\n                     * column \" + column + \" rawtext[i] \" + rawtext[i]); if\n                     * (GBKFreq[row][column] != 0) { gbfreq +=\n                     * GBKFreq[row][column]; }\n                     */\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) gbchars / (float) dbchars);\n        freqval = 50 * ((float) gbfreq / (float) totalfreq);\n        // For regular GB files, this would give the same score, so I handicap\n        // it slightly\n        return (int) (rangeval + freqval) - 1;\n    }\n\n    /*\n     * Function: hz_probability Argument: byte array Returns : number from 0 to\n     * 100 representing probability text in array uses HZ encoding\n     */\n    private int hz_probability(byte[] rawtext) {\n        int i, rawtextlen;\n        int hzchars = 0, dbchars = 1;\n        long hzfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int hzstart = 0, hzend = 0;\n        int row, column;\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen; i++) {\n            if (rawtext[i] == '~') {\n                if (rawtext[i + 1] == '{') {\n                    hzstart++;\n                    i += 2;\n                    while (i < rawtextlen - 1) {\n                        if (rawtext[i] == 0x0A || rawtext[i] == 0x0D) {\n                            break;\n                        } else if (rawtext[i] == '~' && rawtext[i + 1] == '}') {\n                            hzend++;\n                            i++;\n                            break;\n                        } else if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77)\n                                && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) {\n                            hzchars += 2;\n                            row = rawtext[i] - 0x21;\n                            column = rawtext[i + 1] - 0x21;\n                            totalfreq += 500;\n                            if (GBFreq[row][column] != 0) {\n                                hzfreq += GBFreq[row][column];\n                            } else if (15 <= row && row < 55) {\n                                hzfreq += 200;\n                            }\n                        }\n                        dbchars += 2;\n                        i += 2;\n                    }\n                } else if (rawtext[i + 1] == '}') {\n                    hzend++;\n                    i++;\n                } else if (rawtext[i + 1] == '~') {\n                    i++;\n                }\n            }\n        }\n        if (hzstart > 4) {\n            rangeval = 50;\n        } else if (hzstart > 1) {\n            rangeval = 41;\n        } else if (hzstart > 0) { // Only 39 in case the sequence happened to\n            // occur\n            rangeval = 39; // in otherwise non-Hz text\n        } else {\n            rangeval = 0;\n        }\n        freqval = 50 * ((float) hzfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /**\n     * Function: big5_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses Big5 encoding\n     */\n    private int big5_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, bfchars = 1;\n        float rangeval = 0, freqval = 0;\n        long bffreq = 0, totalfreq = 1;\n        int row, column;\n        // Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            if (rawtext[i] < 0) {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xF9\n                        && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0xA1 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFE))) {\n                    bfchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    if (0x40 <= rawtext[i + 1] && rawtext[i + 1] <= 0x7E) {\n                        column = rawtext[i + 1] - 0x40;\n                    } else {\n                        column = rawtext[i + 1] + 256 - 0x61;\n                    }\n                    if (Big5Freq[row][column] != 0) {\n                        bffreq += Big5Freq[row][column];\n                    } else if (3 <= row && row <= 37) {\n                        bffreq += 200;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) bfchars / (float) dbchars);\n        freqval = 50 * ((float) bffreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: euc_tw_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses EUC-TW (CNS 11643)\n     * encoding\n     */\n    private int euc_tw_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, cnschars = 1;\n        long cnsfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Check to see if characters fit into acceptable ranges\n        // and have expected frequency of use\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            if (rawtext[i] < 0) { // high bit set\n                dbchars++;\n                if (i + 3 < rawtextlen && (byte) 0x8E == rawtext[i]\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xB0\n                        && (byte) 0xA1 <= rawtext[i + 2]\n                        && rawtext[i + 2] <= (byte) 0xFE\n                        && (byte) 0xA1 <= rawtext[i + 3]\n                        && rawtext[i + 3] <= (byte) 0xFE) { // Planes 1 - 16\n                    cnschars++;\n                    // System.out.println(\"plane 2 or above CNS char\");\n                    // These are all less frequent chars so just ignore freq\n                    i += 3;\n                } else if ((byte) 0xA1 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && // Plane 1\n                        (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    cnschars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (EUC_TWFreq[row][column] != 0) {\n                        cnsfreq += EUC_TWFreq[row][column];\n                    } else if (35 <= row && row <= 92) {\n                        cnsfreq += 150;\n                    }\n                    i++;\n                }\n            }\n        }\n        rangeval = 50 * ((float) cnschars / (float) dbchars);\n        freqval = 50 * ((float) cnsfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: iso_2022_cn_probability Argument: byte array Returns : number\n     * from 0 to 100 representing probability text in array uses ISO 2022-CN\n     * encoding WORKS FOR BASIC CASES, BUT STILL NEEDS MORE WORK\n     */\n    int iso_2022_cn_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, isochars = 1;\n        long isofreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Check to see if characters fit into acceptable ranges\n        // and have expected frequency of use\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            if (rawtext[i] == (byte) 0x1B && i + 3 < rawtextlen) { // Escape\n                // char ESC\n                if (rawtext[i + 1] == (byte) 0x24 && rawtext[i + 2] == 0x29\n                        && rawtext[i + 3] == (byte) 0x41) { // GB Escape $ ) A\n                    i += 4;\n                    while (rawtext[i] != (byte) 0x1B) {\n                        dbchars++;\n                        if ((0x21 <= rawtext[i] && rawtext[i] <= 0x77)\n                                && (0x21 <= rawtext[i + 1] && rawtext[i + 1] <= 0x77)) {\n                            isochars++;\n                            row = rawtext[i] - 0x21;\n                            column = rawtext[i + 1] - 0x21;\n                            totalfreq += 500;\n                            if (GBFreq[row][column] != 0) {\n                                isofreq += GBFreq[row][column];\n                            } else if (15 <= row && row < 55) {\n                                isofreq += 200;\n                            }\n                            i++;\n                        }\n                        i++;\n                    }\n                } else if (i + 3 < rawtextlen && rawtext[i + 1] == (byte) 0x24\n                        && rawtext[i + 2] == (byte) 0x29\n                        && rawtext[i + 3] == (byte) 0x47) {\n                    // CNS Escape $ ) G\n                    i += 4;\n                    while (rawtext[i] != (byte) 0x1B) {\n                        dbchars++;\n                        if ((byte) 0x21 <= rawtext[i]\n                                && rawtext[i] <= (byte) 0x7E\n                                && (byte) 0x21 <= rawtext[i + 1]\n                                && rawtext[i + 1] <= (byte) 0x7E) {\n                            isochars++;\n                            totalfreq += 500;\n                            row = rawtext[i] - 0x21;\n                            column = rawtext[i + 1] - 0x21;\n                            if (EUC_TWFreq[row][column] != 0) {\n                                isofreq += EUC_TWFreq[row][column];\n                            } else if (35 <= row && row <= 92) {\n                                isofreq += 150;\n                            }\n                            i++;\n                        }\n                        i++;\n                    }\n                }\n                if (rawtext[i] == (byte) 0x1B && i + 2 < rawtextlen\n                        && rawtext[i + 1] == (byte) 0x28\n                        && rawtext[i + 2] == (byte) 0x42) { // ASCII:\n                    // ESC\n                    // ( B\n                    i += 2;\n                }\n            }\n        }\n        rangeval = 50 * ((float) isochars / (float) dbchars);\n        freqval = 50 * ((float) isofreq / (float) totalfreq);\n        // System.out.println(\"isochars dbchars isofreq totalfreq \" + isochars +\n        // \" \" + dbchars + \" \" + isofreq + \" \" + totalfreq + \"\n        // \" + rangeval + \" \" + freqval);\n        return (int) (rangeval + freqval);\n        // return 0;\n    }\n\n    /*\n     * Function: utf8_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses UTF-8 encoding of\n     * Unicode\n     */\n    int utf8_probability(byte[] rawtext) {\n        int score = 0;\n        int i, rawtextlen = 0;\n        int goodbytes = 0, asciibytes = 0;\n        // Maybe also use UTF8 Byte Order Mark: EF BB BF\n        // Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen; i++) {\n            if ((rawtext[i] & (byte) 0x7F) == rawtext[i]) { // One byte\n                asciibytes++;\n                // Ignore ASCII, can throw off count\n            } else if (-64 <= rawtext[i] && rawtext[i] <= -33\n                    && // Two bytes\n                    i + 1 < rawtextlen && -128 <= rawtext[i + 1]\n                    && rawtext[i + 1] <= -65) {\n                goodbytes += 2;\n                i++;\n            } else if (-32 <= rawtext[i]\n                    && rawtext[i] <= -17\n                    && // Three bytes\n                    i + 2 < rawtextlen && -128 <= rawtext[i + 1]\n                    && rawtext[i + 1] <= -65 && -128 <= rawtext[i + 2]\n                    && rawtext[i + 2] <= -65) {\n                goodbytes += 3;\n                i += 2;\n            }\n        }\n        if (asciibytes == rawtextlen) {\n            return 0;\n        }\n        score = (int) (100 * ((float) goodbytes / (float) (rawtextlen - asciibytes)));\n        // System.out.println(\"rawtextlen \" + rawtextlen + \" goodbytes \" +\n        // goodbytes + \" asciibytes \" + asciibytes + \" score \" +\n        // score);\n        // If not above 98, reduce to zero to prevent coincidental matches\n        // Allows for some (few) bad formed sequences\n        if (score > 98) {\n            return score;\n        } else if (score > 95 && goodbytes > 30) {\n            return score;\n        } else {\n            return 0;\n        }\n    }\n\n    /*\n     * Function: utf16_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses UTF-16 encoding of\n     * Unicode, guess based on BOM // NOT VERY GENERAL, NEEDS MUCH MORE WORK\n     */\n    int utf16_probability(byte[] rawtext) {\n        // int score = 0;\n        // int i, rawtextlen = 0;\n        // int goodbytes = 0, asciibytes = 0;\n        if (rawtext.length > 1\n                && ((byte) 0xFE == rawtext[0] && (byte) 0xFF == rawtext[1]) || // Big-endian\n                ((byte) 0xFF == rawtext[0] && (byte) 0xFE == rawtext[1])) { // Little-endian\n            return 100;\n        }\n        return 0;\n        /*\n         * // Check to see if characters fit into acceptable ranges rawtextlen =\n         * rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] &\n         * (byte)0x7F) == rawtext[i]) { // One byte goodbytes += 1;\n         * asciibytes++; } else if ((rawtext[i] & (byte)0xDF) == rawtext[i]) {\n         * // Two bytes if (i+1 < rawtextlen && (rawtext[i+1] & (byte)0xBF) ==\n         * rawtext[i+1]) { goodbytes += 2; i++; } } else if ((rawtext[i] &\n         * (byte)0xEF) == rawtext[i]) { // Three bytes if (i+2 < rawtextlen &&\n         * (rawtext[i+1] & (byte)0xBF) == rawtext[i+1] && (rawtext[i+2] &\n         * (byte)0xBF) == rawtext[i+2]) { goodbytes += 3; i+=2; } } }\n         *\n         * score = (int)(100 * ((float)goodbytes/(float)rawtext.length)); // An\n         * all ASCII file is also a good UTF8 file, but I'd rather it // get\n         * identified as ASCII. Can delete following 3 lines otherwise if\n         * (goodbytes == asciibytes) { score = 0; } // If not above 90, reduce\n         * to zero to prevent coincidental matches if (score > 90) { return\n         * score; } else { return 0; }\n         */\n    }\n\n    /*\n     * Function: ascii_probability Argument: byte array Returns : number from 0\n     * to 100 representing probability text in array uses all ASCII Description:\n     * Sees if array has any characters not in ASCII range, if so, score is\n     * reduced\n     */\n    int ascii_probability(byte[] rawtext) {\n        int score = 75;\n        int i, rawtextlen;\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen; i++) {\n            if (rawtext[i] < 0) {\n                score = score - 5;\n            } else if (rawtext[i] == (byte) 0x1B) { // ESC (used by ISO 2022)\n                score = score - 5;\n            }\n            if (score <= 0) {\n                return 0;\n            }\n        }\n        return score;\n    }\n\n    /*\n     * Function: euc_kr__probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses EUC-KR\n     * encoding\n     */\n    int euc_kr_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, krchars = 1;\n        long krfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    krchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (KRFreq[row][column] != 0) {\n                        krfreq += KRFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        krfreq += 0;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) krchars / (float) dbchars);\n        freqval = 50 * ((float) krfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    /*\n     * Function: cp949__probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses Cp949\n     * encoding\n     */\n    int cp949_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, krchars = 1;\n        long krfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if ((byte) 0x81 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xFE\n                        && ((byte) 0x41 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0x5A\n                        || (byte) 0x61 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0x7A || (byte) 0x81 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE)) {\n                    krchars++;\n                    totalfreq += 500;\n                    if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE\n                            && (byte) 0xA1 <= rawtext[i + 1]\n                            && rawtext[i + 1] <= (byte) 0xFE) {\n                        row = rawtext[i] + 256 - 0xA1;\n                        column = rawtext[i + 1] + 256 - 0xA1;\n                        if (KRFreq[row][column] != 0) {\n                            krfreq += KRFreq[row][column];\n                        }\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) krchars / (float) dbchars);\n        freqval = 50 * ((float) krfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    private int iso_2022_kr_probability(byte[] rawtext) {\n        int i;\n        for (i = 0; i < rawtext.length; i++) {\n            if (i + 3 < rawtext.length && rawtext[i] == 0x1b\n                    && (char) rawtext[i + 1] == '$'\n                    && (char) rawtext[i + 2] == ')'\n                    && (char) rawtext[i + 3] == 'C') {\n                return 100;\n            }\n        }\n        return 0;\n    }\n\n    /*\n     * Function: euc_jp_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses EUC-JP\n     * encoding\n     */\n    private int euc_jp_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, jpchars = 1;\n        long jpfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawText[i]);\n            if (rawtext[i] >= 0) {\n                // asciiChars++;\n            } else {\n                dbchars++;\n                if ((byte) 0xA1 <= rawtext[i] && rawtext[i] <= (byte) 0xFE\n                        && (byte) 0xA1 <= rawtext[i + 1]\n                        && rawtext[i + 1] <= (byte) 0xFE) {\n                    jpchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256 - 0xA1;\n                    column = rawtext[i + 1] + 256 - 0xA1;\n                    if (JPFreq[row][column] != 0) {\n                        jpfreq += JPFreq[row][column];\n                    } else if (15 <= row && row < 55) {\n                        jpfreq += 0;\n                    }\n                }\n                i++;\n            }\n        }\n        rangeval = 50 * ((float) jpchars / (float) dbchars);\n        freqval = 50 * ((float) jpfreq / (float) totalfreq);\n        return (int) (rangeval + freqval);\n    }\n\n    int iso_2022_jp_probability(byte[] rawtext) {\n        int i;\n        for (i = 0; i < rawtext.length; i++) {\n            if (i + 2 < rawtext.length && rawtext[i] == 0x1b\n                    && (char) rawtext[i + 1] == '$'\n                    && (char) rawtext[i + 2] == 'B') {\n                return 100;\n            }\n        }\n        return 0;\n    }\n\n    /*\n     * Function: sjis_probability Argument: pointer to byte array Returns :\n     * number from 0 to 100 representing probability text in array uses\n     * Shift-JIS encoding\n     */\n    int sjis_probability(byte[] rawtext) {\n        int i, rawtextlen = 0;\n        int dbchars = 1, jpchars = 1;\n        long jpfreq = 0, totalfreq = 1;\n        float rangeval = 0, freqval = 0;\n        int row, column, adjust;\n        // Stage 1: Check to see if characters fit into acceptable ranges\n        rawtextlen = rawtext.length;\n        for (i = 0; i < rawtextlen - 1; i++) {\n            // System.err.println(rawtext[i]);\n            if (rawtext[i] >= 0) {\n                // asciichars++;\n            } else {\n                dbchars++;\n                if (i + 1 < rawtext.length\n                        && (((byte) 0x81 <= rawtext[i] && rawtext[i] <= (byte) 0x9F) || ((byte) 0xE0 <= rawtext[i] && rawtext[i] <= (byte) 0xEF))\n                        && (((byte) 0x40 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0x7E) || ((byte) 0x80 <= rawtext[i + 1] && rawtext[i + 1] <= (byte) 0xFC))) {\n                    jpchars++;\n                    totalfreq += 500;\n                    row = rawtext[i] + 256;\n                    column = rawtext[i + 1] + 256;\n                    if (column < 0x9f) {\n                        adjust = 1;\n                        if (column > 0x7f) {\n                            column -= 0x20;\n                        } else {\n                            column -= 0x19;\n                        }\n                    } else {\n                        adjust = 0;\n                        column -= 0x7e;\n                    }\n                    if (row < 0xa0) {\n                        row = ((row - 0x70) << 1) - adjust;\n                    } else {\n                        row = ((row - 0xb0) << 1) - adjust;\n                    }\n                    row -= 0x20;\n                    column = 0x20;\n                    // System.out.println(\"original row \" + row + \" column \" +\n                    // column);\n                    if (row < JPFreq.length && column < JPFreq[row].length\n                            && JPFreq[row][column] != 0) {\n                        jpfreq += JPFreq[row][column];\n                    }\n                    i++;\n                } else if ((byte) 0xA1 <= rawtext[i]\n                        && rawtext[i] <= (byte) 0xDF) {\n                    // half-width katakana, convert to full-width\n                }\n            }\n        }\n        rangeval = 50 * ((float) jpchars / (float) dbchars);\n        freqval = 50 * ((float) jpfreq / (float) totalfreq);\n        // For regular GB files, this would give the same score, so I handicap\n        // it slightly\n        return (int) (rangeval + freqval) - 1;\n    }\n\n    void initialize_frequencies() {\n        int i, j;\n        for (i = 93; i >= 0; i--) {\n            for (j = 93; j >= 0; j--) {\n                GBFreq[i][j] = 0;\n            }\n        }\n        for (i = 125; i >= 0; i--) {\n            for (j = 190; j >= 0; j--) {\n                GBKFreq[i][j] = 0;\n            }\n        }\n        // for (i = 0; i < 94; i++) {\n        // for (j = 0; j < 158; j++) {\n        for (i = 93; i >= 0; i--) {\n            for (j = 157; j >= 0; j--) {\n                Big5Freq[i][j] = 0;\n            }\n        }\n        // for (i = 0; i < 126; i++) {\n        // for (j = 0; j < 191; j++) {\n        for (i = 125; i >= 0; i--) {\n            for (j = 190; j >= 0; j--) {\n                Big5PFreq[i][j] = 0;\n            }\n        }\n        // for (i = 0; i < 94; i++) {\n        // for (j = 0; j < 94; j++) {\n        for (i = 93; i >= 0; i--) {\n            for (j = 93; j >= 0; j--) {\n                EUC_TWFreq[i][j] = 0;\n            }\n        }\n        for (i = 93; i >= 0; i--) {\n            for (j = 93; j >= 0; j--) {\n                JPFreq[i][j] = 0;\n            }\n        }\n        GBFreq[20][35] = 599;\n        GBFreq[49][26] = 598;\n        GBFreq[41][38] = 597;\n        GBFreq[17][26] = 596;\n        GBFreq[32][42] = 595;\n        GBFreq[39][42] = 594;\n        GBFreq[45][49] = 593;\n        GBFreq[51][57] = 592;\n        GBFreq[50][47] = 591;\n        GBFreq[42][90] = 590;\n        GBFreq[52][65] = 589;\n        GBFreq[53][47] = 588;\n        GBFreq[19][82] = 587;\n        GBFreq[31][19] = 586;\n        GBFreq[40][46] = 585;\n        GBFreq[24][89] = 584;\n        GBFreq[23][85] = 583;\n        GBFreq[20][28] = 582;\n        GBFreq[42][20] = 581;\n        GBFreq[34][38] = 580;\n        GBFreq[45][9] = 579;\n        GBFreq[54][50] = 578;\n        GBFreq[25][44] = 577;\n        GBFreq[35][66] = 576;\n        GBFreq[20][55] = 575;\n        GBFreq[18][85] = 574;\n        GBFreq[20][31] = 573;\n        GBFreq[49][17] = 572;\n        GBFreq[41][16] = 571;\n        GBFreq[35][73] = 570;\n        GBFreq[20][34] = 569;\n        GBFreq[29][44] = 568;\n        GBFreq[35][38] = 567;\n        GBFreq[49][9] = 566;\n        GBFreq[46][33] = 565;\n        GBFreq[49][51] = 564;\n        GBFreq[40][89] = 563;\n        GBFreq[26][64] = 562;\n        GBFreq[54][51] = 561;\n        GBFreq[54][36] = 560;\n        GBFreq[39][4] = 559;\n        GBFreq[53][13] = 558;\n        GBFreq[24][92] = 557;\n        GBFreq[27][49] = 556;\n        GBFreq[48][6] = 555;\n        GBFreq[21][51] = 554;\n        GBFreq[30][40] = 553;\n        GBFreq[42][92] = 552;\n        GBFreq[31][78] = 551;\n        GBFreq[25][82] = 550;\n        GBFreq[47][0] = 549;\n        GBFreq[34][19] = 548;\n        GBFreq[47][35] = 547;\n        GBFreq[21][63] = 546;\n        GBFreq[43][75] = 545;\n        GBFreq[21][87] = 544;\n        GBFreq[35][59] = 543;\n        GBFreq[25][34] = 542;\n        GBFreq[21][27] = 541;\n        GBFreq[39][26] = 540;\n        GBFreq[34][26] = 539;\n        GBFreq[39][52] = 538;\n        GBFreq[50][57] = 537;\n        GBFreq[37][79] = 536;\n        GBFreq[26][24] = 535;\n        GBFreq[22][1] = 534;\n        GBFreq[18][40] = 533;\n        GBFreq[41][33] = 532;\n        GBFreq[53][26] = 531;\n        GBFreq[54][86] = 530;\n        GBFreq[20][16] = 529;\n        GBFreq[46][74] = 528;\n        GBFreq[30][19] = 527;\n        GBFreq[45][35] = 526;\n        GBFreq[45][61] = 525;\n        GBFreq[30][9] = 524;\n        GBFreq[41][53] = 523;\n        GBFreq[41][13] = 522;\n        GBFreq[50][34] = 521;\n        GBFreq[53][86] = 520;\n        GBFreq[47][47] = 519;\n        GBFreq[22][28] = 518;\n        GBFreq[50][53] = 517;\n        GBFreq[39][70] = 516;\n        GBFreq[38][15] = 515;\n        GBFreq[42][88] = 514;\n        GBFreq[16][29] = 513;\n        GBFreq[27][90] = 512;\n        GBFreq[29][12] = 511;\n        GBFreq[44][22] = 510;\n        GBFreq[34][69] = 509;\n        GBFreq[24][10] = 508;\n        GBFreq[44][11] = 507;\n        GBFreq[39][92] = 506;\n        GBFreq[49][48] = 505;\n        GBFreq[31][46] = 504;\n        GBFreq[19][50] = 503;\n        GBFreq[21][14] = 502;\n        GBFreq[32][28] = 501;\n        GBFreq[18][3] = 500;\n        GBFreq[53][9] = 499;\n        GBFreq[34][80] = 498;\n        GBFreq[48][88] = 497;\n        GBFreq[46][53] = 496;\n        GBFreq[22][53] = 495;\n        GBFreq[28][10] = 494;\n        GBFreq[44][65] = 493;\n        GBFreq[20][10] = 492;\n        GBFreq[40][76] = 491;\n        GBFreq[47][8] = 490;\n        GBFreq[50][74] = 489;\n        GBFreq[23][62] = 488;\n        GBFreq[49][65] = 487;\n        GBFreq[28][87] = 486;\n        GBFreq[15][48] = 485;\n        GBFreq[22][7] = 484;\n        GBFreq[19][42] = 483;\n        GBFreq[41][20] = 482;\n        GBFreq[26][55] = 481;\n        GBFreq[21][93] = 480;\n        GBFreq[31][76] = 479;\n        GBFreq[34][31] = 478;\n        GBFreq[20][66] = 477;\n        GBFreq[51][33] = 476;\n        GBFreq[34][86] = 475;\n        GBFreq[37][67] = 474;\n        GBFreq[53][53] = 473;\n        GBFreq[40][88] = 472;\n        GBFreq[39][10] = 471;\n        GBFreq[24][3] = 470;\n        GBFreq[27][25] = 469;\n        GBFreq[26][15] = 468;\n        GBFreq[21][88] = 467;\n        GBFreq[52][62] = 466;\n        GBFreq[46][81] = 465;\n        GBFreq[38][72] = 464;\n        GBFreq[17][30] = 463;\n        GBFreq[52][92] = 462;\n        GBFreq[34][90] = 461;\n        GBFreq[21][7] = 460;\n        GBFreq[36][13] = 459;\n        GBFreq[45][41] = 458;\n        GBFreq[32][5] = 457;\n        GBFreq[26][89] = 456;\n        GBFreq[23][87] = 455;\n        GBFreq[20][39] = 454;\n        GBFreq[27][23] = 453;\n        GBFreq[25][59] = 452;\n        GBFreq[49][20] = 451;\n        GBFreq[54][77] = 450;\n        GBFreq[27][67] = 449;\n        GBFreq[47][33] = 448;\n        GBFreq[41][17] = 447;\n        GBFreq[19][81] = 446;\n        GBFreq[16][66] = 445;\n        GBFreq[45][26] = 444;\n        GBFreq[49][81] = 443;\n        GBFreq[53][55] = 442;\n        GBFreq[16][26] = 441;\n        GBFreq[54][62] = 440;\n        GBFreq[20][70] = 439;\n        GBFreq[42][35] = 438;\n        GBFreq[20][57] = 437;\n        GBFreq[34][36] = 436;\n        GBFreq[46][63] = 435;\n        GBFreq[19][45] = 434;\n        GBFreq[21][10] = 433;\n        GBFreq[52][93] = 432;\n        GBFreq[25][2] = 431;\n        GBFreq[30][57] = 430;\n        GBFreq[41][24] = 429;\n        GBFreq[28][43] = 428;\n        GBFreq[45][86] = 427;\n        GBFreq[51][56] = 426;\n        GBFreq[37][28] = 425;\n        GBFreq[52][69] = 424;\n        GBFreq[43][92] = 423;\n        GBFreq[41][31] = 422;\n        GBFreq[37][87] = 421;\n        GBFreq[47][36] = 420;\n        GBFreq[16][16] = 419;\n        GBFreq[40][56] = 418;\n        GBFreq[24][55] = 417;\n        GBFreq[17][1] = 416;\n        GBFreq[35][57] = 415;\n        GBFreq[27][50] = 414;\n        GBFreq[26][14] = 413;\n        GBFreq[50][40] = 412;\n        GBFreq[39][19] = 411;\n        GBFreq[19][89] = 410;\n        GBFreq[29][91] = 409;\n        GBFreq[17][89] = 408;\n        GBFreq[39][74] = 407;\n        GBFreq[46][39] = 406;\n        GBFreq[40][28] = 405;\n        GBFreq[45][68] = 404;\n        GBFreq[43][10] = 403;\n        GBFreq[42][13] = 402;\n        GBFreq[44][81] = 401;\n        GBFreq[41][47] = 400;\n        GBFreq[48][58] = 399;\n        GBFreq[43][68] = 398;\n        GBFreq[16][79] = 397;\n        GBFreq[19][5] = 396;\n        GBFreq[54][59] = 395;\n        GBFreq[17][36] = 394;\n        GBFreq[18][0] = 393;\n        GBFreq[41][5] = 392;\n        GBFreq[41][72] = 391;\n        GBFreq[16][39] = 390;\n        GBFreq[54][0] = 389;\n        GBFreq[51][16] = 388;\n        GBFreq[29][36] = 387;\n        GBFreq[47][5] = 386;\n        GBFreq[47][51] = 385;\n        GBFreq[44][7] = 384;\n        GBFreq[35][30] = 383;\n        GBFreq[26][9] = 382;\n        GBFreq[16][7] = 381;\n        GBFreq[32][1] = 380;\n        GBFreq[33][76] = 379;\n        GBFreq[34][91] = 378;\n        GBFreq[52][36] = 377;\n        GBFreq[26][77] = 376;\n        GBFreq[35][48] = 375;\n        GBFreq[40][80] = 374;\n        GBFreq[41][92] = 373;\n        GBFreq[27][93] = 372;\n        GBFreq[15][17] = 371;\n        GBFreq[16][76] = 370;\n        GBFreq[51][12] = 369;\n        GBFreq[18][20] = 368;\n        GBFreq[15][54] = 367;\n        GBFreq[50][5] = 366;\n        GBFreq[33][22] = 365;\n        GBFreq[37][57] = 364;\n        GBFreq[28][47] = 363;\n        GBFreq[42][31] = 362;\n        GBFreq[18][2] = 361;\n        GBFreq[43][64] = 360;\n        GBFreq[23][47] = 359;\n        GBFreq[28][79] = 358;\n        GBFreq[25][45] = 357;\n        GBFreq[23][91] = 356;\n        GBFreq[22][19] = 355;\n        GBFreq[25][46] = 354;\n        GBFreq[22][36] = 353;\n        GBFreq[54][85] = 352;\n        GBFreq[46][20] = 351;\n        GBFreq[27][37] = 350;\n        GBFreq[26][81] = 349;\n        GBFreq[42][29] = 348;\n        GBFreq[31][90] = 347;\n        GBFreq[41][59] = 346;\n        GBFreq[24][65] = 345;\n        GBFreq[44][84] = 344;\n        GBFreq[24][90] = 343;\n        GBFreq[38][54] = 342;\n        GBFreq[28][70] = 341;\n        GBFreq[27][15] = 340;\n        GBFreq[28][80] = 339;\n        GBFreq[29][8] = 338;\n        GBFreq[45][80] = 337;\n        GBFreq[53][37] = 336;\n        GBFreq[28][65] = 335;\n        GBFreq[23][86] = 334;\n        GBFreq[39][45] = 333;\n        GBFreq[53][32] = 332;\n        GBFreq[38][68] = 331;\n        GBFreq[45][78] = 330;\n        GBFreq[43][7] = 329;\n        GBFreq[46][82] = 328;\n        GBFreq[27][38] = 327;\n        GBFreq[16][62] = 326;\n        GBFreq[24][17] = 325;\n        GBFreq[22][70] = 324;\n        GBFreq[52][28] = 323;\n        GBFreq[23][40] = 322;\n        GBFreq[28][50] = 321;\n        GBFreq[42][91] = 320;\n        GBFreq[47][76] = 319;\n        GBFreq[15][42] = 318;\n        GBFreq[43][55] = 317;\n        GBFreq[29][84] = 316;\n        GBFreq[44][90] = 315;\n        GBFreq[53][16] = 314;\n        GBFreq[22][93] = 313;\n        GBFreq[34][10] = 312;\n        GBFreq[32][53] = 311;\n        GBFreq[43][65] = 310;\n        GBFreq[28][7] = 309;\n        GBFreq[35][46] = 308;\n        GBFreq[21][39] = 307;\n        GBFreq[44][18] = 306;\n        GBFreq[40][10] = 305;\n        GBFreq[54][53] = 304;\n        GBFreq[38][74] = 303;\n        GBFreq[28][26] = 302;\n        GBFreq[15][13] = 301;\n        GBFreq[39][34] = 300;\n        GBFreq[39][46] = 299;\n        GBFreq[42][66] = 298;\n        GBFreq[33][58] = 297;\n        GBFreq[15][56] = 296;\n        GBFreq[18][51] = 295;\n        GBFreq[49][68] = 294;\n        GBFreq[30][37] = 293;\n        GBFreq[51][84] = 292;\n        GBFreq[51][9] = 291;\n        GBFreq[40][70] = 290;\n        GBFreq[41][84] = 289;\n        GBFreq[28][64] = 288;\n        GBFreq[32][88] = 287;\n        GBFreq[24][5] = 286;\n        GBFreq[53][23] = 285;\n        GBFreq[42][27] = 284;\n        GBFreq[22][38] = 283;\n        GBFreq[32][86] = 282;\n        GBFreq[34][30] = 281;\n        GBFreq[38][63] = 280;\n        GBFreq[24][59] = 279;\n        GBFreq[22][81] = 278;\n        GBFreq[32][11] = 277;\n        GBFreq[51][21] = 276;\n        GBFreq[54][41] = 275;\n        GBFreq[21][50] = 274;\n        GBFreq[23][89] = 273;\n        GBFreq[19][87] = 272;\n        GBFreq[26][7] = 271;\n        GBFreq[30][75] = 270;\n        GBFreq[43][84] = 269;\n        GBFreq[51][25] = 268;\n        GBFreq[16][67] = 267;\n        GBFreq[32][9] = 266;\n        GBFreq[48][51] = 265;\n        GBFreq[39][7] = 264;\n        GBFreq[44][88] = 263;\n        GBFreq[52][24] = 262;\n        GBFreq[23][34] = 261;\n        GBFreq[32][75] = 260;\n        GBFreq[19][10] = 259;\n        GBFreq[28][91] = 258;\n        GBFreq[32][83] = 257;\n        GBFreq[25][75] = 256;\n        GBFreq[53][45] = 255;\n        GBFreq[29][85] = 254;\n        GBFreq[53][59] = 253;\n        GBFreq[16][2] = 252;\n        GBFreq[19][78] = 251;\n        GBFreq[15][75] = 250;\n        GBFreq[51][42] = 249;\n        GBFreq[45][67] = 248;\n        GBFreq[15][74] = 247;\n        GBFreq[25][81] = 246;\n        GBFreq[37][62] = 245;\n        GBFreq[16][55] = 244;\n        GBFreq[18][38] = 243;\n        GBFreq[23][23] = 242;\n        GBFreq[38][30] = 241;\n        GBFreq[17][28] = 240;\n        GBFreq[44][73] = 239;\n        GBFreq[23][78] = 238;\n        GBFreq[40][77] = 237;\n        GBFreq[38][87] = 236;\n        GBFreq[27][19] = 235;\n        GBFreq[38][82] = 234;\n        GBFreq[37][22] = 233;\n        GBFreq[41][30] = 232;\n        GBFreq[54][9] = 231;\n        GBFreq[32][30] = 230;\n        GBFreq[30][52] = 229;\n        GBFreq[40][84] = 228;\n        GBFreq[53][57] = 227;\n        GBFreq[27][27] = 226;\n        GBFreq[38][64] = 225;\n        GBFreq[18][43] = 224;\n        GBFreq[23][69] = 223;\n        GBFreq[28][12] = 222;\n        GBFreq[50][78] = 221;\n        GBFreq[50][1] = 220;\n        GBFreq[26][88] = 219;\n        GBFreq[36][40] = 218;\n        GBFreq[33][89] = 217;\n        GBFreq[41][28] = 216;\n        GBFreq[31][77] = 215;\n        GBFreq[46][1] = 214;\n        GBFreq[47][19] = 213;\n        GBFreq[35][55] = 212;\n        GBFreq[41][21] = 211;\n        GBFreq[27][10] = 210;\n        GBFreq[32][77] = 209;\n        GBFreq[26][37] = 208;\n        GBFreq[20][33] = 207;\n        GBFreq[41][52] = 206;\n        GBFreq[32][18] = 205;\n        GBFreq[38][13] = 204;\n        GBFreq[20][18] = 203;\n        GBFreq[20][24] = 202;\n        GBFreq[45][19] = 201;\n        GBFreq[18][53] = 200;\n        /*\n         * GBFreq[39][0] = 199; GBFreq[40][71] = 198; GBFreq[41][27] = 197;\n         * GBFreq[15][69] = 196; GBFreq[42][10] = 195; GBFreq[31][89] = 194;\n         * GBFreq[51][28] = 193; GBFreq[41][22] = 192; GBFreq[40][43] = 191;\n         * GBFreq[38][6] = 190; GBFreq[37][11] = 189; GBFreq[39][60] = 188;\n         * GBFreq[48][47] = 187; GBFreq[46][80] = 186; GBFreq[52][49] = 185;\n         * GBFreq[50][48] = 184; GBFreq[25][1] = 183; GBFreq[52][29] = 182;\n         * GBFreq[24][66] = 181; GBFreq[23][35] = 180; GBFreq[49][72] = 179;\n         * GBFreq[47][45] = 178; GBFreq[45][14] = 177; GBFreq[51][70] = 176;\n         * GBFreq[22][30] = 175; GBFreq[49][83] = 174; GBFreq[26][79] = 173;\n         * GBFreq[27][41] = 172; GBFreq[51][81] = 171; GBFreq[41][54] = 170;\n         * GBFreq[20][4] = 169; GBFreq[29][60] = 168; GBFreq[20][27] = 167;\n         * GBFreq[50][15] = 166; GBFreq[41][6] = 165; GBFreq[35][34] = 164;\n         * GBFreq[44][87] = 163; GBFreq[46][66] = 162; GBFreq[42][37] = 161;\n         * GBFreq[42][24] = 160; GBFreq[54][7] = 159; GBFreq[41][14] = 158;\n         * GBFreq[39][83] = 157; GBFreq[16][87] = 156; GBFreq[20][59] = 155;\n         * GBFreq[42][12] = 154; GBFreq[47][2] = 153; GBFreq[21][32] = 152;\n         * GBFreq[53][29] = 151; GBFreq[22][40] = 150; GBFreq[24][58] = 149;\n         * GBFreq[52][88] = 148; GBFreq[29][30] = 147; GBFreq[15][91] = 146;\n         * GBFreq[54][72] = 145; GBFreq[51][75] = 144; GBFreq[33][67] = 143;\n         * GBFreq[41][50] = 142; GBFreq[27][34] = 141; GBFreq[46][17] = 140;\n         * GBFreq[31][74] = 139; GBFreq[42][67] = 138; GBFreq[54][87] = 137;\n         * GBFreq[27][14] = 136; GBFreq[16][63] = 135; GBFreq[16][5] = 134;\n         * GBFreq[43][23] = 133; GBFreq[23][13] = 132; GBFreq[31][12] = 131;\n         * GBFreq[25][57] = 130; GBFreq[38][49] = 129; GBFreq[42][69] = 128;\n         * GBFreq[23][80] = 127; GBFreq[29][0] = 126; GBFreq[28][2] = 125;\n         * GBFreq[28][17] = 124; GBFreq[17][27] = 123; GBFreq[40][16] = 122;\n         * GBFreq[45][1] = 121; GBFreq[36][33] = 120; GBFreq[35][23] = 119;\n         * GBFreq[20][86] = 118; GBFreq[29][53] = 117; GBFreq[23][88] = 116;\n         * GBFreq[51][87] = 115; GBFreq[54][27] = 114; GBFreq[44][36] = 113;\n         * GBFreq[21][45] = 112; GBFreq[53][52] = 111; GBFreq[31][53] = 110;\n         * GBFreq[38][47] = 109; GBFreq[27][21] = 108; GBFreq[30][42] = 107;\n         * GBFreq[29][10] = 106; GBFreq[35][35] = 105; GBFreq[24][56] = 104;\n         * GBFreq[41][29] = 103; GBFreq[18][68] = 102; GBFreq[29][24] = 101;\n         * GBFreq[25][84] = 100; GBFreq[35][47] = 99; GBFreq[29][56] = 98;\n         * GBFreq[30][44] = 97; GBFreq[53][3] = 96; GBFreq[30][63] = 95;\n         * GBFreq[52][52] = 94; GBFreq[54][1] = 93; GBFreq[22][48] = 92;\n         * GBFreq[54][66] = 91; GBFreq[21][90] = 90; GBFreq[52][47] = 89;\n         * GBFreq[39][25] = 88; GBFreq[39][39] = 87; GBFreq[44][37] = 86;\n         * GBFreq[44][76] = 85; GBFreq[46][75] = 84; GBFreq[18][37] = 83;\n         * GBFreq[47][42] = 82; GBFreq[19][92] = 81; GBFreq[51][27] = 80;\n         * GBFreq[48][83] = 79; GBFreq[23][70] = 78; GBFreq[29][9] = 77;\n         * GBFreq[33][79] = 76; GBFreq[52][90] = 75; GBFreq[53][6] = 74;\n         * GBFreq[24][36] = 73; GBFreq[25][25] = 72; GBFreq[44][26] = 71;\n         * GBFreq[25][36] = 70; GBFreq[29][87] = 69; GBFreq[48][0] = 68;\n         * GBFreq[15][40] = 67; GBFreq[17][45] = 66; GBFreq[30][14] = 65;\n         * GBFreq[48][38] = 64; GBFreq[23][19] = 63; GBFreq[40][42] = 62;\n         * GBFreq[31][63] = 61; GBFreq[16][23] = 60; GBFreq[26][21] = 59;\n         * GBFreq[32][76] = 58; GBFreq[23][58] = 57; GBFreq[41][37] = 56;\n         * GBFreq[30][43] = 55; GBFreq[47][38] = 54; GBFreq[21][46] = 53;\n         * GBFreq[18][33] = 52; GBFreq[52][37] = 51; GBFreq[36][8] = 50;\n         * GBFreq[49][24] = 49; GBFreq[15][66] = 48; GBFreq[35][77] = 47;\n         * GBFreq[27][58] = 46; GBFreq[35][51] = 45; GBFreq[24][69] = 44;\n         * GBFreq[20][54] = 43; GBFreq[24][41] = 42; GBFreq[41][0] = 41;\n         * GBFreq[33][71] = 40; GBFreq[23][52] = 39; GBFreq[29][67] = 38;\n         * GBFreq[46][51] = 37; GBFreq[46][90] = 36; GBFreq[49][33] = 35;\n         * GBFreq[33][28] = 34; GBFreq[37][86] = 33; GBFreq[39][22] = 32;\n         * GBFreq[37][37] = 31; GBFreq[29][62] = 30; GBFreq[29][50] = 29;\n         * GBFreq[36][89] = 28; GBFreq[42][44] = 27; GBFreq[51][82] = 26;\n         * GBFreq[28][83] = 25; GBFreq[15][78] = 24; GBFreq[46][62] = 23;\n         * GBFreq[19][69] = 22; GBFreq[51][23] = 21; GBFreq[37][69] = 20;\n         * GBFreq[25][5] = 19; GBFreq[51][85] = 18; GBFreq[48][77] = 17;\n         * GBFreq[32][46] = 16; GBFreq[53][60] = 15; GBFreq[28][57] = 14;\n         * GBFreq[54][82] = 13; GBFreq[54][15] = 12; GBFreq[49][54] = 11;\n         * GBFreq[53][87] = 10; GBFreq[27][16] = 9; GBFreq[29][34] = 8;\n         * GBFreq[20][44] = 7; GBFreq[42][73] = 6; GBFreq[47][71] = 5;\n         * GBFreq[29][37] = 4; GBFreq[25][50] = 3; GBFreq[18][84] = 2;\n         * GBFreq[50][45] = 1; GBFreq[48][46] = 0;\n         */\n        // GBFreq[43][89] = -1; GBFreq[54][68] = -2;\n        Big5Freq[9][89] = 600;\n        Big5Freq[11][15] = 599;\n        Big5Freq[3][66] = 598;\n        Big5Freq[6][121] = 597;\n        Big5Freq[3][0] = 596;\n        Big5Freq[5][82] = 595;\n        Big5Freq[3][42] = 594;\n        Big5Freq[5][34] = 593;\n        Big5Freq[3][8] = 592;\n        Big5Freq[3][6] = 591;\n        Big5Freq[3][67] = 590;\n        Big5Freq[7][139] = 589;\n        Big5Freq[23][137] = 588;\n        Big5Freq[12][46] = 587;\n        Big5Freq[4][8] = 586;\n        Big5Freq[4][41] = 585;\n        Big5Freq[18][47] = 584;\n        Big5Freq[12][114] = 583;\n        Big5Freq[6][1] = 582;\n        Big5Freq[22][60] = 581;\n        Big5Freq[5][46] = 580;\n        Big5Freq[11][79] = 579;\n        Big5Freq[3][23] = 578;\n        Big5Freq[7][114] = 577;\n        Big5Freq[29][102] = 576;\n        Big5Freq[19][14] = 575;\n        Big5Freq[4][133] = 574;\n        Big5Freq[3][29] = 573;\n        Big5Freq[4][109] = 572;\n        Big5Freq[14][127] = 571;\n        Big5Freq[5][48] = 570;\n        Big5Freq[13][104] = 569;\n        Big5Freq[3][132] = 568;\n        Big5Freq[26][64] = 567;\n        Big5Freq[7][19] = 566;\n        Big5Freq[4][12] = 565;\n        Big5Freq[11][124] = 564;\n        Big5Freq[7][89] = 563;\n        Big5Freq[15][124] = 562;\n        Big5Freq[4][108] = 561;\n        Big5Freq[19][66] = 560;\n        Big5Freq[3][21] = 559;\n        Big5Freq[24][12] = 558;\n        Big5Freq[28][111] = 557;\n        Big5Freq[12][107] = 556;\n        Big5Freq[3][112] = 555;\n        Big5Freq[8][113] = 554;\n        Big5Freq[5][40] = 553;\n        Big5Freq[26][145] = 552;\n        Big5Freq[3][48] = 551;\n        Big5Freq[3][70] = 550;\n        Big5Freq[22][17] = 549;\n        Big5Freq[16][47] = 548;\n        Big5Freq[3][53] = 547;\n        Big5Freq[4][24] = 546;\n        Big5Freq[32][120] = 545;\n        Big5Freq[24][49] = 544;\n        Big5Freq[24][142] = 543;\n        Big5Freq[18][66] = 542;\n        Big5Freq[29][150] = 541;\n        Big5Freq[5][122] = 540;\n        Big5Freq[5][114] = 539;\n        Big5Freq[3][44] = 538;\n        Big5Freq[10][128] = 537;\n        Big5Freq[15][20] = 536;\n        Big5Freq[13][33] = 535;\n        Big5Freq[14][87] = 534;\n        Big5Freq[3][126] = 533;\n        Big5Freq[4][53] = 532;\n        Big5Freq[4][40] = 531;\n        Big5Freq[9][93] = 530;\n        Big5Freq[15][137] = 529;\n        Big5Freq[10][123] = 528;\n        Big5Freq[4][56] = 527;\n        Big5Freq[5][71] = 526;\n        Big5Freq[10][8] = 525;\n        Big5Freq[5][16] = 524;\n        Big5Freq[5][146] = 523;\n        Big5Freq[18][88] = 522;\n        Big5Freq[24][4] = 521;\n        Big5Freq[20][47] = 520;\n        Big5Freq[5][33] = 519;\n        Big5Freq[9][43] = 518;\n        Big5Freq[20][12] = 517;\n        Big5Freq[20][13] = 516;\n        Big5Freq[5][156] = 515;\n        Big5Freq[22][140] = 514;\n        Big5Freq[8][146] = 513;\n        Big5Freq[21][123] = 512;\n        Big5Freq[4][90] = 511;\n        Big5Freq[5][62] = 510;\n        Big5Freq[17][59] = 509;\n        Big5Freq[10][37] = 508;\n        Big5Freq[18][107] = 507;\n        Big5Freq[14][53] = 506;\n        Big5Freq[22][51] = 505;\n        Big5Freq[8][13] = 504;\n        Big5Freq[5][29] = 503;\n        Big5Freq[9][7] = 502;\n        Big5Freq[22][14] = 501;\n        Big5Freq[8][55] = 500;\n        Big5Freq[33][9] = 499;\n        Big5Freq[16][64] = 498;\n        Big5Freq[7][131] = 497;\n        Big5Freq[34][4] = 496;\n        Big5Freq[7][101] = 495;\n        Big5Freq[11][139] = 494;\n        Big5Freq[3][135] = 493;\n        Big5Freq[7][102] = 492;\n        Big5Freq[17][13] = 491;\n        Big5Freq[3][20] = 490;\n        Big5Freq[27][106] = 489;\n        Big5Freq[5][88] = 488;\n        Big5Freq[6][33] = 487;\n        Big5Freq[5][139] = 486;\n        Big5Freq[6][0] = 485;\n        Big5Freq[17][58] = 484;\n        Big5Freq[5][133] = 483;\n        Big5Freq[9][107] = 482;\n        Big5Freq[23][39] = 481;\n        Big5Freq[5][23] = 480;\n        Big5Freq[3][79] = 479;\n        Big5Freq[32][97] = 478;\n        Big5Freq[3][136] = 477;\n        Big5Freq[4][94] = 476;\n        Big5Freq[21][61] = 475;\n        Big5Freq[23][123] = 474;\n        Big5Freq[26][16] = 473;\n        Big5Freq[24][137] = 472;\n        Big5Freq[22][18] = 471;\n        Big5Freq[5][1] = 470;\n        Big5Freq[20][119] = 469;\n        Big5Freq[3][7] = 468;\n        Big5Freq[10][79] = 467;\n        Big5Freq[15][105] = 466;\n        Big5Freq[3][144] = 465;\n        Big5Freq[12][80] = 464;\n        Big5Freq[15][73] = 463;\n        Big5Freq[3][19] = 462;\n        Big5Freq[8][109] = 461;\n        Big5Freq[3][15] = 460;\n        Big5Freq[31][82] = 459;\n        Big5Freq[3][43] = 458;\n        Big5Freq[25][119] = 457;\n        Big5Freq[16][111] = 456;\n        Big5Freq[7][77] = 455;\n        Big5Freq[3][95] = 454;\n        Big5Freq[24][82] = 453;\n        Big5Freq[7][52] = 452;\n        Big5Freq[9][151] = 451;\n        Big5Freq[3][129] = 450;\n        Big5Freq[5][87] = 449;\n        Big5Freq[3][55] = 448;\n        Big5Freq[8][153] = 447;\n        Big5Freq[4][83] = 446;\n        Big5Freq[3][114] = 445;\n        Big5Freq[23][147] = 444;\n        Big5Freq[15][31] = 443;\n        Big5Freq[3][54] = 442;\n        Big5Freq[11][122] = 441;\n        Big5Freq[4][4] = 440;\n        Big5Freq[34][149] = 439;\n        Big5Freq[3][17] = 438;\n        Big5Freq[21][64] = 437;\n        Big5Freq[26][144] = 436;\n        Big5Freq[4][62] = 435;\n        Big5Freq[8][15] = 434;\n        Big5Freq[35][80] = 433;\n        Big5Freq[7][110] = 432;\n        Big5Freq[23][114] = 431;\n        Big5Freq[3][108] = 430;\n        Big5Freq[3][62] = 429;\n        Big5Freq[21][41] = 428;\n        Big5Freq[15][99] = 427;\n        Big5Freq[5][47] = 426;\n        Big5Freq[4][96] = 425;\n        Big5Freq[20][122] = 424;\n        Big5Freq[5][21] = 423;\n        Big5Freq[4][157] = 422;\n        Big5Freq[16][14] = 421;\n        Big5Freq[3][117] = 420;\n        Big5Freq[7][129] = 419;\n        Big5Freq[4][27] = 418;\n        Big5Freq[5][30] = 417;\n        Big5Freq[22][16] = 416;\n        Big5Freq[5][64] = 415;\n        Big5Freq[17][99] = 414;\n        Big5Freq[17][57] = 413;\n        Big5Freq[8][105] = 412;\n        Big5Freq[5][112] = 411;\n        Big5Freq[20][59] = 410;\n        Big5Freq[6][129] = 409;\n        Big5Freq[18][17] = 408;\n        Big5Freq[3][92] = 407;\n        Big5Freq[28][118] = 406;\n        Big5Freq[3][109] = 405;\n        Big5Freq[31][51] = 404;\n        Big5Freq[13][116] = 403;\n        Big5Freq[6][15] = 402;\n        Big5Freq[36][136] = 401;\n        Big5Freq[12][74] = 400;\n        Big5Freq[20][88] = 399;\n        Big5Freq[36][68] = 398;\n        Big5Freq[3][147] = 397;\n        Big5Freq[15][84] = 396;\n        Big5Freq[16][32] = 395;\n        Big5Freq[16][58] = 394;\n        Big5Freq[7][66] = 393;\n        Big5Freq[23][107] = 392;\n        Big5Freq[9][6] = 391;\n        Big5Freq[12][86] = 390;\n        Big5Freq[23][112] = 389;\n        Big5Freq[37][23] = 388;\n        Big5Freq[3][138] = 387;\n        Big5Freq[20][68] = 386;\n        Big5Freq[15][116] = 385;\n        Big5Freq[18][64] = 384;\n        Big5Freq[12][139] = 383;\n        Big5Freq[11][155] = 382;\n        Big5Freq[4][156] = 381;\n        Big5Freq[12][84] = 380;\n        Big5Freq[18][49] = 379;\n        Big5Freq[25][125] = 378;\n        Big5Freq[25][147] = 377;\n        Big5Freq[15][110] = 376;\n        Big5Freq[19][96] = 375;\n        Big5Freq[30][152] = 374;\n        Big5Freq[6][31] = 373;\n        Big5Freq[27][117] = 372;\n        Big5Freq[3][10] = 371;\n        Big5Freq[6][131] = 370;\n        Big5Freq[13][112] = 369;\n        Big5Freq[36][156] = 368;\n        Big5Freq[4][60] = 367;\n        Big5Freq[15][121] = 366;\n        Big5Freq[4][112] = 365;\n        Big5Freq[30][142] = 364;\n        Big5Freq[23][154] = 363;\n        Big5Freq[27][101] = 362;\n        Big5Freq[9][140] = 361;\n        Big5Freq[3][89] = 360;\n        Big5Freq[18][148] = 359;\n        Big5Freq[4][69] = 358;\n        Big5Freq[16][49] = 357;\n        Big5Freq[6][117] = 356;\n        Big5Freq[36][55] = 355;\n        Big5Freq[5][123] = 354;\n        Big5Freq[4][126] = 353;\n        Big5Freq[4][119] = 352;\n        Big5Freq[9][95] = 351;\n        Big5Freq[5][24] = 350;\n        Big5Freq[16][133] = 349;\n        Big5Freq[10][134] = 348;\n        Big5Freq[26][59] = 347;\n        Big5Freq[6][41] = 346;\n        Big5Freq[6][146] = 345;\n        Big5Freq[19][24] = 344;\n        Big5Freq[5][113] = 343;\n        Big5Freq[10][118] = 342;\n        Big5Freq[34][151] = 341;\n        Big5Freq[9][72] = 340;\n        Big5Freq[31][25] = 339;\n        Big5Freq[18][126] = 338;\n        Big5Freq[18][28] = 337;\n        Big5Freq[4][153] = 336;\n        Big5Freq[3][84] = 335;\n        Big5Freq[21][18] = 334;\n        Big5Freq[25][129] = 333;\n        Big5Freq[6][107] = 332;\n        Big5Freq[12][25] = 331;\n        Big5Freq[17][109] = 330;\n        Big5Freq[7][76] = 329;\n        Big5Freq[15][15] = 328;\n        Big5Freq[4][14] = 327;\n        Big5Freq[23][88] = 326;\n        Big5Freq[18][2] = 325;\n        Big5Freq[6][88] = 324;\n        Big5Freq[16][84] = 323;\n        Big5Freq[12][48] = 322;\n        Big5Freq[7][68] = 321;\n        Big5Freq[5][50] = 320;\n        Big5Freq[13][54] = 319;\n        Big5Freq[7][98] = 318;\n        Big5Freq[11][6] = 317;\n        Big5Freq[9][80] = 316;\n        Big5Freq[16][41] = 315;\n        Big5Freq[7][43] = 314;\n        Big5Freq[28][117] = 313;\n        Big5Freq[3][51] = 312;\n        Big5Freq[7][3] = 311;\n        Big5Freq[20][81] = 310;\n        Big5Freq[4][2] = 309;\n        Big5Freq[11][16] = 308;\n        Big5Freq[10][4] = 307;\n        Big5Freq[10][119] = 306;\n        Big5Freq[6][142] = 305;\n        Big5Freq[18][51] = 304;\n        Big5Freq[8][144] = 303;\n        Big5Freq[10][65] = 302;\n        Big5Freq[11][64] = 301;\n        Big5Freq[11][130] = 300;\n        Big5Freq[9][92] = 299;\n        Big5Freq[18][29] = 298;\n        Big5Freq[18][78] = 297;\n        Big5Freq[18][151] = 296;\n        Big5Freq[33][127] = 295;\n        Big5Freq[35][113] = 294;\n        Big5Freq[10][155] = 293;\n        Big5Freq[3][76] = 292;\n        Big5Freq[36][123] = 291;\n        Big5Freq[13][143] = 290;\n        Big5Freq[5][135] = 289;\n        Big5Freq[23][116] = 288;\n        Big5Freq[6][101] = 287;\n        Big5Freq[14][74] = 286;\n        Big5Freq[7][153] = 285;\n        Big5Freq[3][101] = 284;\n        Big5Freq[9][74] = 283;\n        Big5Freq[3][156] = 282;\n        Big5Freq[4][147] = 281;\n        Big5Freq[9][12] = 280;\n        Big5Freq[18][133] = 279;\n        Big5Freq[4][0] = 278;\n        Big5Freq[7][155] = 277;\n        Big5Freq[9][144] = 276;\n        Big5Freq[23][49] = 275;\n        Big5Freq[5][89] = 274;\n        Big5Freq[10][11] = 273;\n        Big5Freq[3][110] = 272;\n        Big5Freq[3][40] = 271;\n        Big5Freq[29][115] = 270;\n        Big5Freq[9][100] = 269;\n        Big5Freq[21][67] = 268;\n        Big5Freq[23][145] = 267;\n        Big5Freq[10][47] = 266;\n        Big5Freq[4][31] = 265;\n        Big5Freq[4][81] = 264;\n        Big5Freq[22][62] = 263;\n        Big5Freq[4][28] = 262;\n        Big5Freq[27][39] = 261;\n        Big5Freq[27][54] = 260;\n        Big5Freq[32][46] = 259;\n        Big5Freq[4][76] = 258;\n        Big5Freq[26][15] = 257;\n        Big5Freq[12][154] = 256;\n        Big5Freq[9][150] = 255;\n        Big5Freq[15][17] = 254;\n        Big5Freq[5][129] = 253;\n        Big5Freq[10][40] = 252;\n        Big5Freq[13][37] = 251;\n        Big5Freq[31][104] = 250;\n        Big5Freq[3][152] = 249;\n        Big5Freq[5][22] = 248;\n        Big5Freq[8][48] = 247;\n        Big5Freq[4][74] = 246;\n        Big5Freq[6][17] = 245;\n        Big5Freq[30][82] = 244;\n        Big5Freq[4][116] = 243;\n        Big5Freq[16][42] = 242;\n        Big5Freq[5][55] = 241;\n        Big5Freq[4][64] = 240;\n        Big5Freq[14][19] = 239;\n        Big5Freq[35][82] = 238;\n        Big5Freq[30][139] = 237;\n        Big5Freq[26][152] = 236;\n        Big5Freq[32][32] = 235;\n        Big5Freq[21][102] = 234;\n        Big5Freq[10][131] = 233;\n        Big5Freq[9][128] = 232;\n        Big5Freq[3][87] = 231;\n        Big5Freq[4][51] = 230;\n        Big5Freq[10][15] = 229;\n        Big5Freq[4][150] = 228;\n        Big5Freq[7][4] = 227;\n        Big5Freq[7][51] = 226;\n        Big5Freq[7][157] = 225;\n        Big5Freq[4][146] = 224;\n        Big5Freq[4][91] = 223;\n        Big5Freq[7][13] = 222;\n        Big5Freq[17][116] = 221;\n        Big5Freq[23][21] = 220;\n        Big5Freq[5][106] = 219;\n        Big5Freq[14][100] = 218;\n        Big5Freq[10][152] = 217;\n        Big5Freq[14][89] = 216;\n        Big5Freq[6][138] = 215;\n        Big5Freq[12][157] = 214;\n        Big5Freq[10][102] = 213;\n        Big5Freq[19][94] = 212;\n        Big5Freq[7][74] = 211;\n        Big5Freq[18][128] = 210;\n        Big5Freq[27][111] = 209;\n        Big5Freq[11][57] = 208;\n        Big5Freq[3][131] = 207;\n        Big5Freq[30][23] = 206;\n        Big5Freq[30][126] = 205;\n        Big5Freq[4][36] = 204;\n        Big5Freq[26][124] = 203;\n        Big5Freq[4][19] = 202;\n        Big5Freq[9][152] = 201;\n        /*\n         * Big5Freq[5][0] = 200; Big5Freq[26][57] = 199; Big5Freq[13][155] =\n         * 198; Big5Freq[3][38] = 197; Big5Freq[9][155] = 196; Big5Freq[28][53]\n         * = 195; Big5Freq[15][71] = 194; Big5Freq[21][95] = 193;\n         * Big5Freq[15][112] = 192; Big5Freq[14][138] = 191; Big5Freq[8][18] =\n         * 190; Big5Freq[20][151] = 189; Big5Freq[37][27] = 188;\n         * Big5Freq[32][48] = 187; Big5Freq[23][66] = 186; Big5Freq[9][2] = 185;\n         * Big5Freq[13][133] = 184; Big5Freq[7][127] = 183; Big5Freq[3][11] =\n         * 182; Big5Freq[12][118] = 181; Big5Freq[13][101] = 180;\n         * Big5Freq[30][153] = 179; Big5Freq[4][65] = 178; Big5Freq[5][25] =\n         * 177; Big5Freq[5][140] = 176; Big5Freq[6][25] = 175; Big5Freq[4][52] =\n         * 174; Big5Freq[30][156] = 173; Big5Freq[16][13] = 172; Big5Freq[21][8]\n         * = 171; Big5Freq[19][74] = 170; Big5Freq[15][145] = 169;\n         * Big5Freq[9][15] = 168; Big5Freq[13][82] = 167; Big5Freq[26][86] =\n         * 166; Big5Freq[18][52] = 165; Big5Freq[6][109] = 164; Big5Freq[10][99]\n         * = 163; Big5Freq[18][101] = 162; Big5Freq[25][49] = 161;\n         * Big5Freq[31][79] = 160; Big5Freq[28][20] = 159; Big5Freq[12][115] =\n         * 158; Big5Freq[15][66] = 157; Big5Freq[11][104] = 156;\n         * Big5Freq[23][106] = 155; Big5Freq[34][157] = 154; Big5Freq[32][94] =\n         * 153; Big5Freq[29][88] = 152; Big5Freq[10][46] = 151;\n         * Big5Freq[13][118] = 150; Big5Freq[20][37] = 149; Big5Freq[12][30] =\n         * 148; Big5Freq[21][4] = 147; Big5Freq[16][33] = 146; Big5Freq[13][52]\n         * = 145; Big5Freq[4][7] = 144; Big5Freq[21][49] = 143; Big5Freq[3][27]\n         * = 142; Big5Freq[16][91] = 141; Big5Freq[5][155] = 140;\n         * Big5Freq[29][130] = 139; Big5Freq[3][125] = 138; Big5Freq[14][26] =\n         * 137; Big5Freq[15][39] = 136; Big5Freq[24][110] = 135;\n         * Big5Freq[7][141] = 134; Big5Freq[21][15] = 133; Big5Freq[32][104] =\n         * 132; Big5Freq[8][31] = 131; Big5Freq[34][112] = 130; Big5Freq[10][75]\n         * = 129; Big5Freq[21][23] = 128; Big5Freq[34][131] = 127;\n         * Big5Freq[12][3] = 126; Big5Freq[10][62] = 125; Big5Freq[9][120] =\n         * 124; Big5Freq[32][149] = 123; Big5Freq[8][44] = 122; Big5Freq[24][2]\n         * = 121; Big5Freq[6][148] = 120; Big5Freq[15][103] = 119;\n         * Big5Freq[36][54] = 118; Big5Freq[36][134] = 117; Big5Freq[11][7] =\n         * 116; Big5Freq[3][90] = 115; Big5Freq[36][73] = 114; Big5Freq[8][102]\n         * = 113; Big5Freq[12][87] = 112; Big5Freq[25][64] = 111; Big5Freq[9][1]\n         * = 110; Big5Freq[24][121] = 109; Big5Freq[5][75] = 108;\n         * Big5Freq[17][83] = 107; Big5Freq[18][57] = 106; Big5Freq[8][95] =\n         * 105; Big5Freq[14][36] = 104; Big5Freq[28][113] = 103;\n         * Big5Freq[12][56] = 102; Big5Freq[14][61] = 101; Big5Freq[25][138] =\n         * 100; Big5Freq[4][34] = 99; Big5Freq[11][152] = 98; Big5Freq[35][0] =\n         * 97; Big5Freq[4][15] = 96; Big5Freq[8][82] = 95; Big5Freq[20][73] =\n         * 94; Big5Freq[25][52] = 93; Big5Freq[24][6] = 92; Big5Freq[21][78] =\n         * 91; Big5Freq[17][32] = 90; Big5Freq[17][91] = 89; Big5Freq[5][76] =\n         * 88; Big5Freq[15][60] = 87; Big5Freq[15][150] = 86; Big5Freq[5][80] =\n         * 85; Big5Freq[15][81] = 84; Big5Freq[28][108] = 83; Big5Freq[18][14] =\n         * 82; Big5Freq[19][109] = 81; Big5Freq[28][133] = 80; Big5Freq[21][97]\n         * = 79; Big5Freq[5][105] = 78; Big5Freq[18][114] = 77; Big5Freq[16][95]\n         * = 76; Big5Freq[5][51] = 75; Big5Freq[3][148] = 74; Big5Freq[22][102]\n         * = 73; Big5Freq[4][123] = 72; Big5Freq[8][88] = 71; Big5Freq[25][111]\n         * = 70; Big5Freq[8][149] = 69; Big5Freq[9][48] = 68; Big5Freq[16][126]\n         * = 67; Big5Freq[33][150] = 66; Big5Freq[9][54] = 65; Big5Freq[29][104]\n         * = 64; Big5Freq[3][3] = 63; Big5Freq[11][49] = 62; Big5Freq[24][109] =\n         * 61; Big5Freq[28][116] = 60; Big5Freq[34][113] = 59; Big5Freq[5][3] =\n         * 58; Big5Freq[21][106] = 57; Big5Freq[4][98] = 56; Big5Freq[12][135] =\n         * 55; Big5Freq[16][101] = 54; Big5Freq[12][147] = 53; Big5Freq[27][55]\n         * = 52; Big5Freq[3][5] = 51; Big5Freq[11][101] = 50; Big5Freq[16][157]\n         * = 49; Big5Freq[22][114] = 48; Big5Freq[18][46] = 47; Big5Freq[4][29]\n         * = 46; Big5Freq[8][103] = 45; Big5Freq[16][151] = 44; Big5Freq[8][29]\n         * = 43; Big5Freq[15][114] = 42; Big5Freq[22][70] = 41;\n         * Big5Freq[13][121] = 40; Big5Freq[7][112] = 39; Big5Freq[20][83] = 38;\n         * Big5Freq[3][36] = 37; Big5Freq[10][103] = 36; Big5Freq[3][96] = 35;\n         * Big5Freq[21][79] = 34; Big5Freq[25][120] = 33; Big5Freq[29][121] =\n         * 32; Big5Freq[23][71] = 31; Big5Freq[21][22] = 30; Big5Freq[18][89] =\n         * 29; Big5Freq[25][104] = 28; Big5Freq[10][124] = 27; Big5Freq[26][4] =\n         * 26; Big5Freq[21][136] = 25; Big5Freq[6][112] = 24; Big5Freq[12][103]\n         * = 23; Big5Freq[17][66] = 22; Big5Freq[13][151] = 21;\n         * Big5Freq[33][152] = 20; Big5Freq[11][148] = 19; Big5Freq[13][57] =\n         * 18; Big5Freq[13][41] = 17; Big5Freq[7][60] = 16; Big5Freq[21][29] =\n         * 15; Big5Freq[9][157] = 14; Big5Freq[24][95] = 13; Big5Freq[15][148] =\n         * 12; Big5Freq[15][122] = 11; Big5Freq[6][125] = 10; Big5Freq[11][25] =\n         * 9; Big5Freq[20][55] = 8; Big5Freq[19][84] = 7; Big5Freq[21][82] = 6;\n         * Big5Freq[24][3] = 5; Big5Freq[13][70] = 4; Big5Freq[6][21] = 3;\n         * Big5Freq[21][86] = 2; Big5Freq[12][23] = 1; Big5Freq[3][85] = 0;\n         * EUC_TWFreq[45][90] = 600;\n         */\n        Big5PFreq[41][122] = 600;\n        Big5PFreq[35][0] = 599;\n        Big5PFreq[43][15] = 598;\n        Big5PFreq[35][99] = 597;\n        Big5PFreq[35][6] = 596;\n        Big5PFreq[35][8] = 595;\n        Big5PFreq[38][154] = 594;\n        Big5PFreq[37][34] = 593;\n        Big5PFreq[37][115] = 592;\n        Big5PFreq[36][12] = 591;\n        Big5PFreq[18][77] = 590;\n        Big5PFreq[35][100] = 589;\n        Big5PFreq[35][42] = 588;\n        Big5PFreq[120][75] = 587;\n        Big5PFreq[35][23] = 586;\n        Big5PFreq[13][72] = 585;\n        Big5PFreq[0][67] = 584;\n        Big5PFreq[39][172] = 583;\n        Big5PFreq[22][182] = 582;\n        Big5PFreq[15][186] = 581;\n        Big5PFreq[15][165] = 580;\n        Big5PFreq[35][44] = 579;\n        Big5PFreq[40][13] = 578;\n        Big5PFreq[38][1] = 577;\n        Big5PFreq[37][33] = 576;\n        Big5PFreq[36][24] = 575;\n        Big5PFreq[56][4] = 574;\n        Big5PFreq[35][29] = 573;\n        Big5PFreq[9][96] = 572;\n        Big5PFreq[37][62] = 571;\n        Big5PFreq[48][47] = 570;\n        Big5PFreq[51][14] = 569;\n        Big5PFreq[39][122] = 568;\n        Big5PFreq[44][46] = 567;\n        Big5PFreq[35][21] = 566;\n        Big5PFreq[36][8] = 565;\n        Big5PFreq[36][141] = 564;\n        Big5PFreq[3][81] = 563;\n        Big5PFreq[37][155] = 562;\n        Big5PFreq[42][84] = 561;\n        Big5PFreq[36][40] = 560;\n        Big5PFreq[35][103] = 559;\n        Big5PFreq[11][84] = 558;\n        Big5PFreq[45][33] = 557;\n        Big5PFreq[121][79] = 556;\n        Big5PFreq[2][77] = 555;\n        Big5PFreq[36][41] = 554;\n        Big5PFreq[37][47] = 553;\n        Big5PFreq[39][125] = 552;\n        Big5PFreq[37][26] = 551;\n        Big5PFreq[35][48] = 550;\n        Big5PFreq[35][28] = 549;\n        Big5PFreq[35][159] = 548;\n        Big5PFreq[37][40] = 547;\n        Big5PFreq[35][145] = 546;\n        Big5PFreq[37][147] = 545;\n        Big5PFreq[46][160] = 544;\n        Big5PFreq[37][46] = 543;\n        Big5PFreq[50][99] = 542;\n        Big5PFreq[52][13] = 541;\n        Big5PFreq[10][82] = 540;\n        Big5PFreq[35][169] = 539;\n        Big5PFreq[35][31] = 538;\n        Big5PFreq[47][31] = 537;\n        Big5PFreq[18][79] = 536;\n        Big5PFreq[16][113] = 535;\n        Big5PFreq[37][104] = 534;\n        Big5PFreq[39][134] = 533;\n        Big5PFreq[36][53] = 532;\n        Big5PFreq[38][0] = 531;\n        Big5PFreq[4][86] = 530;\n        Big5PFreq[54][17] = 529;\n        Big5PFreq[43][157] = 528;\n        Big5PFreq[35][165] = 527;\n        Big5PFreq[69][147] = 526;\n        Big5PFreq[117][95] = 525;\n        Big5PFreq[35][162] = 524;\n        Big5PFreq[35][17] = 523;\n        Big5PFreq[36][142] = 522;\n        Big5PFreq[36][4] = 521;\n        Big5PFreq[37][166] = 520;\n        Big5PFreq[35][168] = 519;\n        Big5PFreq[35][19] = 518;\n        Big5PFreq[37][48] = 517;\n        Big5PFreq[42][37] = 516;\n        Big5PFreq[40][146] = 515;\n        Big5PFreq[36][123] = 514;\n        Big5PFreq[22][41] = 513;\n        Big5PFreq[20][119] = 512;\n        Big5PFreq[2][74] = 511;\n        Big5PFreq[44][113] = 510;\n        Big5PFreq[35][125] = 509;\n        Big5PFreq[37][16] = 508;\n        Big5PFreq[35][20] = 507;\n        Big5PFreq[35][55] = 506;\n        Big5PFreq[37][145] = 505;\n        Big5PFreq[0][88] = 504;\n        Big5PFreq[3][94] = 503;\n        Big5PFreq[6][65] = 502;\n        Big5PFreq[26][15] = 501;\n        Big5PFreq[41][126] = 500;\n        Big5PFreq[36][129] = 499;\n        Big5PFreq[31][75] = 498;\n        Big5PFreq[19][61] = 497;\n        Big5PFreq[35][128] = 496;\n        Big5PFreq[29][79] = 495;\n        Big5PFreq[36][62] = 494;\n        Big5PFreq[37][189] = 493;\n        Big5PFreq[39][109] = 492;\n        Big5PFreq[39][135] = 491;\n        Big5PFreq[72][15] = 490;\n        Big5PFreq[47][106] = 489;\n        Big5PFreq[54][14] = 488;\n        Big5PFreq[24][52] = 487;\n        Big5PFreq[38][162] = 486;\n        Big5PFreq[41][43] = 485;\n        Big5PFreq[37][121] = 484;\n        Big5PFreq[14][66] = 483;\n        Big5PFreq[37][30] = 482;\n        Big5PFreq[35][7] = 481;\n        Big5PFreq[49][58] = 480;\n        Big5PFreq[43][188] = 479;\n        Big5PFreq[24][66] = 478;\n        Big5PFreq[35][171] = 477;\n        Big5PFreq[40][186] = 476;\n        Big5PFreq[39][164] = 475;\n        Big5PFreq[78][186] = 474;\n        Big5PFreq[8][72] = 473;\n        Big5PFreq[36][190] = 472;\n        Big5PFreq[35][53] = 471;\n        Big5PFreq[35][54] = 470;\n        Big5PFreq[22][159] = 469;\n        Big5PFreq[35][9] = 468;\n        Big5PFreq[41][140] = 467;\n        Big5PFreq[37][22] = 466;\n        Big5PFreq[48][97] = 465;\n        Big5PFreq[50][97] = 464;\n        Big5PFreq[36][127] = 463;\n        Big5PFreq[37][23] = 462;\n        Big5PFreq[40][55] = 461;\n        Big5PFreq[35][43] = 460;\n        Big5PFreq[26][22] = 459;\n        Big5PFreq[35][15] = 458;\n        Big5PFreq[72][179] = 457;\n        Big5PFreq[20][129] = 456;\n        Big5PFreq[52][101] = 455;\n        Big5PFreq[35][12] = 454;\n        Big5PFreq[42][156] = 453;\n        Big5PFreq[15][157] = 452;\n        Big5PFreq[50][140] = 451;\n        Big5PFreq[26][28] = 450;\n        Big5PFreq[54][51] = 449;\n        Big5PFreq[35][112] = 448;\n        Big5PFreq[36][116] = 447;\n        Big5PFreq[42][11] = 446;\n        Big5PFreq[37][172] = 445;\n        Big5PFreq[37][29] = 444;\n        Big5PFreq[44][107] = 443;\n        Big5PFreq[50][17] = 442;\n        Big5PFreq[39][107] = 441;\n        Big5PFreq[19][109] = 440;\n        Big5PFreq[36][60] = 439;\n        Big5PFreq[49][132] = 438;\n        Big5PFreq[26][16] = 437;\n        Big5PFreq[43][155] = 436;\n        Big5PFreq[37][120] = 435;\n        Big5PFreq[15][159] = 434;\n        Big5PFreq[43][6] = 433;\n        Big5PFreq[45][188] = 432;\n        Big5PFreq[35][38] = 431;\n        Big5PFreq[39][143] = 430;\n        Big5PFreq[48][144] = 429;\n        Big5PFreq[37][168] = 428;\n        Big5PFreq[37][1] = 427;\n        Big5PFreq[36][109] = 426;\n        Big5PFreq[46][53] = 425;\n        Big5PFreq[38][54] = 424;\n        Big5PFreq[36][0] = 423;\n        Big5PFreq[72][33] = 422;\n        Big5PFreq[42][8] = 421;\n        Big5PFreq[36][31] = 420;\n        Big5PFreq[35][150] = 419;\n        Big5PFreq[118][93] = 418;\n        Big5PFreq[37][61] = 417;\n        Big5PFreq[0][85] = 416;\n        Big5PFreq[36][27] = 415;\n        Big5PFreq[35][134] = 414;\n        Big5PFreq[36][145] = 413;\n        Big5PFreq[6][96] = 412;\n        Big5PFreq[36][14] = 411;\n        Big5PFreq[16][36] = 410;\n        Big5PFreq[15][175] = 409;\n        Big5PFreq[35][10] = 408;\n        Big5PFreq[36][189] = 407;\n        Big5PFreq[35][51] = 406;\n        Big5PFreq[35][109] = 405;\n        Big5PFreq[35][147] = 404;\n        Big5PFreq[35][180] = 403;\n        Big5PFreq[72][5] = 402;\n        Big5PFreq[36][107] = 401;\n        Big5PFreq[49][116] = 400;\n        Big5PFreq[73][30] = 399;\n        Big5PFreq[6][90] = 398;\n        Big5PFreq[2][70] = 397;\n        Big5PFreq[17][141] = 396;\n        Big5PFreq[35][62] = 395;\n        Big5PFreq[16][180] = 394;\n        Big5PFreq[4][91] = 393;\n        Big5PFreq[15][171] = 392;\n        Big5PFreq[35][177] = 391;\n        Big5PFreq[37][173] = 390;\n        Big5PFreq[16][121] = 389;\n        Big5PFreq[35][5] = 388;\n        Big5PFreq[46][122] = 387;\n        Big5PFreq[40][138] = 386;\n        Big5PFreq[50][49] = 385;\n        Big5PFreq[36][152] = 384;\n        Big5PFreq[13][43] = 383;\n        Big5PFreq[9][88] = 382;\n        Big5PFreq[36][159] = 381;\n        Big5PFreq[27][62] = 380;\n        Big5PFreq[40][18] = 379;\n        Big5PFreq[17][129] = 378;\n        Big5PFreq[43][97] = 377;\n        Big5PFreq[13][131] = 376;\n        Big5PFreq[46][107] = 375;\n        Big5PFreq[60][64] = 374;\n        Big5PFreq[36][179] = 373;\n        Big5PFreq[37][55] = 372;\n        Big5PFreq[41][173] = 371;\n        Big5PFreq[44][172] = 370;\n        Big5PFreq[23][187] = 369;\n        Big5PFreq[36][149] = 368;\n        Big5PFreq[17][125] = 367;\n        Big5PFreq[55][180] = 366;\n        Big5PFreq[51][129] = 365;\n        Big5PFreq[36][51] = 364;\n        Big5PFreq[37][122] = 363;\n        Big5PFreq[48][32] = 362;\n        Big5PFreq[51][99] = 361;\n        Big5PFreq[54][16] = 360;\n        Big5PFreq[41][183] = 359;\n        Big5PFreq[37][179] = 358;\n        Big5PFreq[38][179] = 357;\n        Big5PFreq[35][143] = 356;\n        Big5PFreq[37][24] = 355;\n        Big5PFreq[40][177] = 354;\n        Big5PFreq[47][117] = 353;\n        Big5PFreq[39][52] = 352;\n        Big5PFreq[22][99] = 351;\n        Big5PFreq[40][142] = 350;\n        Big5PFreq[36][49] = 349;\n        Big5PFreq[38][17] = 348;\n        Big5PFreq[39][188] = 347;\n        Big5PFreq[36][186] = 346;\n        Big5PFreq[35][189] = 345;\n        Big5PFreq[41][7] = 344;\n        Big5PFreq[18][91] = 343;\n        Big5PFreq[43][137] = 342;\n        Big5PFreq[35][142] = 341;\n        Big5PFreq[35][117] = 340;\n        Big5PFreq[39][138] = 339;\n        Big5PFreq[16][59] = 338;\n        Big5PFreq[39][174] = 337;\n        Big5PFreq[55][145] = 336;\n        Big5PFreq[37][21] = 335;\n        Big5PFreq[36][180] = 334;\n        Big5PFreq[37][156] = 333;\n        Big5PFreq[49][13] = 332;\n        Big5PFreq[41][107] = 331;\n        Big5PFreq[36][56] = 330;\n        Big5PFreq[53][8] = 329;\n        Big5PFreq[22][114] = 328;\n        Big5PFreq[5][95] = 327;\n        Big5PFreq[37][0] = 326;\n        Big5PFreq[26][183] = 325;\n        Big5PFreq[22][66] = 324;\n        Big5PFreq[35][58] = 323;\n        Big5PFreq[48][117] = 322;\n        Big5PFreq[36][102] = 321;\n        Big5PFreq[22][122] = 320;\n        Big5PFreq[35][11] = 319;\n        Big5PFreq[46][19] = 318;\n        Big5PFreq[22][49] = 317;\n        Big5PFreq[48][166] = 316;\n        Big5PFreq[41][125] = 315;\n        Big5PFreq[41][1] = 314;\n        Big5PFreq[35][178] = 313;\n        Big5PFreq[41][12] = 312;\n        Big5PFreq[26][167] = 311;\n        Big5PFreq[42][152] = 310;\n        Big5PFreq[42][46] = 309;\n        Big5PFreq[42][151] = 308;\n        Big5PFreq[20][135] = 307;\n        Big5PFreq[37][162] = 306;\n        Big5PFreq[37][50] = 305;\n        Big5PFreq[22][185] = 304;\n        Big5PFreq[36][166] = 303;\n        Big5PFreq[19][40] = 302;\n        Big5PFreq[22][107] = 301;\n        Big5PFreq[22][102] = 300;\n        Big5PFreq[57][162] = 299;\n        Big5PFreq[22][124] = 298;\n        Big5PFreq[37][138] = 297;\n        Big5PFreq[37][25] = 296;\n        Big5PFreq[0][69] = 295;\n        Big5PFreq[43][172] = 294;\n        Big5PFreq[42][167] = 293;\n        Big5PFreq[35][120] = 292;\n        Big5PFreq[41][128] = 291;\n        Big5PFreq[2][88] = 290;\n        Big5PFreq[20][123] = 289;\n        Big5PFreq[35][123] = 288;\n        Big5PFreq[36][28] = 287;\n        Big5PFreq[42][188] = 286;\n        Big5PFreq[42][164] = 285;\n        Big5PFreq[42][4] = 284;\n        Big5PFreq[43][57] = 283;\n        Big5PFreq[39][3] = 282;\n        Big5PFreq[42][3] = 281;\n        Big5PFreq[57][158] = 280;\n        Big5PFreq[35][146] = 279;\n        Big5PFreq[24][54] = 278;\n        Big5PFreq[13][110] = 277;\n        Big5PFreq[23][132] = 276;\n        Big5PFreq[26][102] = 275;\n        Big5PFreq[55][178] = 274;\n        Big5PFreq[17][117] = 273;\n        Big5PFreq[41][161] = 272;\n        Big5PFreq[38][150] = 271;\n        Big5PFreq[10][71] = 270;\n        Big5PFreq[47][60] = 269;\n        Big5PFreq[16][114] = 268;\n        Big5PFreq[21][47] = 267;\n        Big5PFreq[39][101] = 266;\n        Big5PFreq[18][45] = 265;\n        Big5PFreq[40][121] = 264;\n        Big5PFreq[45][41] = 263;\n        Big5PFreq[22][167] = 262;\n        Big5PFreq[26][149] = 261;\n        Big5PFreq[15][189] = 260;\n        Big5PFreq[41][177] = 259;\n        Big5PFreq[46][36] = 258;\n        Big5PFreq[20][40] = 257;\n        Big5PFreq[41][54] = 256;\n        Big5PFreq[3][87] = 255;\n        Big5PFreq[40][16] = 254;\n        Big5PFreq[42][15] = 253;\n        Big5PFreq[11][83] = 252;\n        Big5PFreq[0][94] = 251;\n        Big5PFreq[122][81] = 250;\n        Big5PFreq[41][26] = 249;\n        Big5PFreq[36][34] = 248;\n        Big5PFreq[44][148] = 247;\n        Big5PFreq[35][3] = 246;\n        Big5PFreq[36][114] = 245;\n        Big5PFreq[42][112] = 244;\n        Big5PFreq[35][183] = 243;\n        Big5PFreq[49][73] = 242;\n        Big5PFreq[39][2] = 241;\n        Big5PFreq[38][121] = 240;\n        Big5PFreq[44][114] = 239;\n        Big5PFreq[49][32] = 238;\n        Big5PFreq[1][65] = 237;\n        Big5PFreq[38][25] = 236;\n        Big5PFreq[39][4] = 235;\n        Big5PFreq[42][62] = 234;\n        Big5PFreq[35][40] = 233;\n        Big5PFreq[24][2] = 232;\n        Big5PFreq[53][49] = 231;\n        Big5PFreq[41][133] = 230;\n        Big5PFreq[43][134] = 229;\n        Big5PFreq[3][83] = 228;\n        Big5PFreq[38][158] = 227;\n        Big5PFreq[24][17] = 226;\n        Big5PFreq[52][59] = 225;\n        Big5PFreq[38][41] = 224;\n        Big5PFreq[37][127] = 223;\n        Big5PFreq[22][175] = 222;\n        Big5PFreq[44][30] = 221;\n        Big5PFreq[47][178] = 220;\n        Big5PFreq[43][99] = 219;\n        Big5PFreq[19][4] = 218;\n        Big5PFreq[37][97] = 217;\n        Big5PFreq[38][181] = 216;\n        Big5PFreq[45][103] = 215;\n        Big5PFreq[1][86] = 214;\n        Big5PFreq[40][15] = 213;\n        Big5PFreq[22][136] = 212;\n        Big5PFreq[75][165] = 211;\n        Big5PFreq[36][15] = 210;\n        Big5PFreq[46][80] = 209;\n        Big5PFreq[59][55] = 208;\n        Big5PFreq[37][108] = 207;\n        Big5PFreq[21][109] = 206;\n        Big5PFreq[24][165] = 205;\n        Big5PFreq[79][158] = 204;\n        Big5PFreq[44][139] = 203;\n        Big5PFreq[36][124] = 202;\n        Big5PFreq[42][185] = 201;\n        Big5PFreq[39][186] = 200;\n        Big5PFreq[22][128] = 199;\n        Big5PFreq[40][44] = 198;\n        Big5PFreq[41][105] = 197;\n        Big5PFreq[1][70] = 196;\n        Big5PFreq[1][68] = 195;\n        Big5PFreq[53][22] = 194;\n        Big5PFreq[36][54] = 193;\n        Big5PFreq[47][147] = 192;\n        Big5PFreq[35][36] = 191;\n        Big5PFreq[35][185] = 190;\n        Big5PFreq[45][37] = 189;\n        Big5PFreq[43][163] = 188;\n        Big5PFreq[56][115] = 187;\n        Big5PFreq[38][164] = 186;\n        Big5PFreq[35][141] = 185;\n        Big5PFreq[42][132] = 184;\n        Big5PFreq[46][120] = 183;\n        Big5PFreq[69][142] = 182;\n        Big5PFreq[38][175] = 181;\n        Big5PFreq[22][112] = 180;\n        Big5PFreq[38][142] = 179;\n        Big5PFreq[40][37] = 178;\n        Big5PFreq[37][109] = 177;\n        Big5PFreq[40][144] = 176;\n        Big5PFreq[44][117] = 175;\n        Big5PFreq[35][181] = 174;\n        Big5PFreq[26][105] = 173;\n        Big5PFreq[16][48] = 172;\n        Big5PFreq[44][122] = 171;\n        Big5PFreq[12][86] = 170;\n        Big5PFreq[84][53] = 169;\n        Big5PFreq[17][44] = 168;\n        Big5PFreq[59][54] = 167;\n        Big5PFreq[36][98] = 166;\n        Big5PFreq[45][115] = 165;\n        Big5PFreq[73][9] = 164;\n        Big5PFreq[44][123] = 163;\n        Big5PFreq[37][188] = 162;\n        Big5PFreq[51][117] = 161;\n        Big5PFreq[15][156] = 160;\n        Big5PFreq[36][155] = 159;\n        Big5PFreq[44][25] = 158;\n        Big5PFreq[38][12] = 157;\n        Big5PFreq[38][140] = 156;\n        Big5PFreq[23][4] = 155;\n        Big5PFreq[45][149] = 154;\n        Big5PFreq[22][189] = 153;\n        Big5PFreq[38][147] = 152;\n        Big5PFreq[27][5] = 151;\n        Big5PFreq[22][42] = 150;\n        Big5PFreq[3][68] = 149;\n        Big5PFreq[39][51] = 148;\n        Big5PFreq[36][29] = 147;\n        Big5PFreq[20][108] = 146;\n        Big5PFreq[50][57] = 145;\n        Big5PFreq[55][104] = 144;\n        Big5PFreq[22][46] = 143;\n        Big5PFreq[18][164] = 142;\n        Big5PFreq[50][159] = 141;\n        Big5PFreq[85][131] = 140;\n        Big5PFreq[26][79] = 139;\n        Big5PFreq[38][100] = 138;\n        Big5PFreq[53][112] = 137;\n        Big5PFreq[20][190] = 136;\n        Big5PFreq[14][69] = 135;\n        Big5PFreq[23][11] = 134;\n        Big5PFreq[40][114] = 133;\n        Big5PFreq[40][148] = 132;\n        Big5PFreq[53][130] = 131;\n        Big5PFreq[36][2] = 130;\n        Big5PFreq[66][82] = 129;\n        Big5PFreq[45][166] = 128;\n        Big5PFreq[4][88] = 127;\n        Big5PFreq[16][57] = 126;\n        Big5PFreq[22][116] = 125;\n        Big5PFreq[36][108] = 124;\n        Big5PFreq[13][48] = 123;\n        Big5PFreq[54][12] = 122;\n        Big5PFreq[40][136] = 121;\n        Big5PFreq[36][128] = 120;\n        Big5PFreq[23][6] = 119;\n        Big5PFreq[38][125] = 118;\n        Big5PFreq[45][154] = 117;\n        Big5PFreq[51][127] = 116;\n        Big5PFreq[44][163] = 115;\n        Big5PFreq[16][173] = 114;\n        Big5PFreq[43][49] = 113;\n        Big5PFreq[20][112] = 112;\n        Big5PFreq[15][168] = 111;\n        Big5PFreq[35][129] = 110;\n        Big5PFreq[20][45] = 109;\n        Big5PFreq[38][10] = 108;\n        Big5PFreq[57][171] = 107;\n        Big5PFreq[44][190] = 106;\n        Big5PFreq[40][56] = 105;\n        Big5PFreq[36][156] = 104;\n        Big5PFreq[3][88] = 103;\n        Big5PFreq[50][122] = 102;\n        Big5PFreq[36][7] = 101;\n        Big5PFreq[39][43] = 100;\n        Big5PFreq[15][166] = 99;\n        Big5PFreq[42][136] = 98;\n        Big5PFreq[22][131] = 97;\n        Big5PFreq[44][23] = 96;\n        Big5PFreq[54][147] = 95;\n        Big5PFreq[41][32] = 94;\n        Big5PFreq[23][121] = 93;\n        Big5PFreq[39][108] = 92;\n        Big5PFreq[2][78] = 91;\n        Big5PFreq[40][155] = 90;\n        Big5PFreq[55][51] = 89;\n        Big5PFreq[19][34] = 88;\n        Big5PFreq[48][128] = 87;\n        Big5PFreq[48][159] = 86;\n        Big5PFreq[20][70] = 85;\n        Big5PFreq[34][71] = 84;\n        Big5PFreq[16][31] = 83;\n        Big5PFreq[42][157] = 82;\n        Big5PFreq[20][44] = 81;\n        Big5PFreq[11][92] = 80;\n        Big5PFreq[44][180] = 79;\n        Big5PFreq[84][33] = 78;\n        Big5PFreq[16][116] = 77;\n        Big5PFreq[61][163] = 76;\n        Big5PFreq[35][164] = 75;\n        Big5PFreq[36][42] = 74;\n        Big5PFreq[13][40] = 73;\n        Big5PFreq[43][176] = 72;\n        Big5PFreq[2][66] = 71;\n        Big5PFreq[20][133] = 70;\n        Big5PFreq[36][65] = 69;\n        Big5PFreq[38][33] = 68;\n        Big5PFreq[12][91] = 67;\n        Big5PFreq[36][26] = 66;\n        Big5PFreq[15][174] = 65;\n        Big5PFreq[77][32] = 64;\n        Big5PFreq[16][1] = 63;\n        Big5PFreq[25][86] = 62;\n        Big5PFreq[17][13] = 61;\n        Big5PFreq[5][75] = 60;\n        Big5PFreq[36][52] = 59;\n        Big5PFreq[51][164] = 58;\n        Big5PFreq[12][85] = 57;\n        Big5PFreq[39][168] = 56;\n        Big5PFreq[43][16] = 55;\n        Big5PFreq[40][69] = 54;\n        Big5PFreq[26][108] = 53;\n        Big5PFreq[51][56] = 52;\n        Big5PFreq[16][37] = 51;\n        Big5PFreq[40][29] = 50;\n        Big5PFreq[46][171] = 49;\n        Big5PFreq[40][128] = 48;\n        Big5PFreq[72][114] = 47;\n        Big5PFreq[21][103] = 46;\n        Big5PFreq[22][44] = 45;\n        Big5PFreq[40][115] = 44;\n        Big5PFreq[43][7] = 43;\n        Big5PFreq[43][153] = 42;\n        Big5PFreq[17][20] = 41;\n        Big5PFreq[16][49] = 40;\n        Big5PFreq[36][57] = 39;\n        Big5PFreq[18][38] = 38;\n        Big5PFreq[45][184] = 37;\n        Big5PFreq[37][167] = 36;\n        Big5PFreq[26][106] = 35;\n        Big5PFreq[61][121] = 34;\n        Big5PFreq[89][140] = 33;\n        Big5PFreq[46][61] = 32;\n        Big5PFreq[39][163] = 31;\n        Big5PFreq[40][62] = 30;\n        Big5PFreq[38][165] = 29;\n        Big5PFreq[47][37] = 28;\n        Big5PFreq[18][155] = 27;\n        Big5PFreq[20][33] = 26;\n        Big5PFreq[29][90] = 25;\n        Big5PFreq[20][103] = 24;\n        Big5PFreq[37][51] = 23;\n        Big5PFreq[57][0] = 22;\n        Big5PFreq[40][31] = 21;\n        Big5PFreq[45][32] = 20;\n        Big5PFreq[59][23] = 19;\n        Big5PFreq[18][47] = 18;\n        Big5PFreq[45][134] = 17;\n        Big5PFreq[37][59] = 16;\n        Big5PFreq[21][128] = 15;\n        Big5PFreq[36][106] = 14;\n        Big5PFreq[31][39] = 13;\n        Big5PFreq[40][182] = 12;\n        Big5PFreq[52][155] = 11;\n        Big5PFreq[42][166] = 10;\n        Big5PFreq[35][27] = 9;\n        Big5PFreq[38][3] = 8;\n        Big5PFreq[13][44] = 7;\n        Big5PFreq[58][157] = 6;\n        Big5PFreq[47][51] = 5;\n        Big5PFreq[41][37] = 4;\n        Big5PFreq[41][172] = 3;\n        Big5PFreq[51][165] = 2;\n        Big5PFreq[15][161] = 1;\n        Big5PFreq[24][181] = 0;\n        EUC_TWFreq[48][49] = 599;\n        EUC_TWFreq[35][65] = 598;\n        EUC_TWFreq[41][27] = 597;\n        EUC_TWFreq[35][0] = 596;\n        EUC_TWFreq[39][19] = 595;\n        EUC_TWFreq[35][42] = 594;\n        EUC_TWFreq[38][66] = 593;\n        EUC_TWFreq[35][8] = 592;\n        EUC_TWFreq[35][6] = 591;\n        EUC_TWFreq[35][66] = 590;\n        EUC_TWFreq[43][14] = 589;\n        EUC_TWFreq[69][80] = 588;\n        EUC_TWFreq[50][48] = 587;\n        EUC_TWFreq[36][71] = 586;\n        EUC_TWFreq[37][10] = 585;\n        EUC_TWFreq[60][52] = 584;\n        EUC_TWFreq[51][21] = 583;\n        EUC_TWFreq[40][2] = 582;\n        EUC_TWFreq[67][35] = 581;\n        EUC_TWFreq[38][78] = 580;\n        EUC_TWFreq[49][18] = 579;\n        EUC_TWFreq[35][23] = 578;\n        EUC_TWFreq[42][83] = 577;\n        EUC_TWFreq[79][47] = 576;\n        EUC_TWFreq[61][82] = 575;\n        EUC_TWFreq[38][7] = 574;\n        EUC_TWFreq[35][29] = 573;\n        EUC_TWFreq[37][77] = 572;\n        EUC_TWFreq[54][67] = 571;\n        EUC_TWFreq[38][80] = 570;\n        EUC_TWFreq[52][74] = 569;\n        EUC_TWFreq[36][37] = 568;\n        EUC_TWFreq[74][8] = 567;\n        EUC_TWFreq[41][83] = 566;\n        EUC_TWFreq[36][75] = 565;\n        EUC_TWFreq[49][63] = 564;\n        EUC_TWFreq[42][58] = 563;\n        EUC_TWFreq[56][33] = 562;\n        EUC_TWFreq[37][76] = 561;\n        EUC_TWFreq[62][39] = 560;\n        EUC_TWFreq[35][21] = 559;\n        EUC_TWFreq[70][19] = 558;\n        EUC_TWFreq[77][88] = 557;\n        EUC_TWFreq[51][14] = 556;\n        EUC_TWFreq[36][17] = 555;\n        EUC_TWFreq[44][51] = 554;\n        EUC_TWFreq[38][72] = 553;\n        EUC_TWFreq[74][90] = 552;\n        EUC_TWFreq[35][48] = 551;\n        EUC_TWFreq[35][69] = 550;\n        EUC_TWFreq[66][86] = 549;\n        EUC_TWFreq[57][20] = 548;\n        EUC_TWFreq[35][53] = 547;\n        EUC_TWFreq[36][87] = 546;\n        EUC_TWFreq[84][67] = 545;\n        EUC_TWFreq[70][56] = 544;\n        EUC_TWFreq[71][54] = 543;\n        EUC_TWFreq[60][70] = 542;\n        EUC_TWFreq[80][1] = 541;\n        EUC_TWFreq[39][59] = 540;\n        EUC_TWFreq[39][51] = 539;\n        EUC_TWFreq[35][44] = 538;\n        EUC_TWFreq[48][4] = 537;\n        EUC_TWFreq[55][24] = 536;\n        EUC_TWFreq[52][4] = 535;\n        EUC_TWFreq[54][26] = 534;\n        EUC_TWFreq[36][31] = 533;\n        EUC_TWFreq[37][22] = 532;\n        EUC_TWFreq[37][9] = 531;\n        EUC_TWFreq[46][0] = 530;\n        EUC_TWFreq[56][46] = 529;\n        EUC_TWFreq[47][93] = 528;\n        EUC_TWFreq[37][25] = 527;\n        EUC_TWFreq[39][8] = 526;\n        EUC_TWFreq[46][73] = 525;\n        EUC_TWFreq[38][48] = 524;\n        EUC_TWFreq[39][83] = 523;\n        EUC_TWFreq[60][92] = 522;\n        EUC_TWFreq[70][11] = 521;\n        EUC_TWFreq[63][84] = 520;\n        EUC_TWFreq[38][65] = 519;\n        EUC_TWFreq[45][45] = 518;\n        EUC_TWFreq[63][49] = 517;\n        EUC_TWFreq[63][50] = 516;\n        EUC_TWFreq[39][93] = 515;\n        EUC_TWFreq[68][20] = 514;\n        EUC_TWFreq[44][84] = 513;\n        EUC_TWFreq[66][34] = 512;\n        EUC_TWFreq[37][58] = 511;\n        EUC_TWFreq[39][0] = 510;\n        EUC_TWFreq[59][1] = 509;\n        EUC_TWFreq[47][8] = 508;\n        EUC_TWFreq[61][17] = 507;\n        EUC_TWFreq[53][87] = 506;\n        EUC_TWFreq[67][26] = 505;\n        EUC_TWFreq[43][46] = 504;\n        EUC_TWFreq[38][61] = 503;\n        EUC_TWFreq[45][9] = 502;\n        EUC_TWFreq[66][83] = 501;\n        EUC_TWFreq[43][88] = 500;\n        EUC_TWFreq[85][20] = 499;\n        EUC_TWFreq[57][36] = 498;\n        EUC_TWFreq[43][6] = 497;\n        EUC_TWFreq[86][77] = 496;\n        EUC_TWFreq[42][70] = 495;\n        EUC_TWFreq[49][78] = 494;\n        EUC_TWFreq[36][40] = 493;\n        EUC_TWFreq[42][71] = 492;\n        EUC_TWFreq[58][49] = 491;\n        EUC_TWFreq[35][20] = 490;\n        EUC_TWFreq[76][20] = 489;\n        EUC_TWFreq[39][25] = 488;\n        EUC_TWFreq[40][34] = 487;\n        EUC_TWFreq[39][76] = 486;\n        EUC_TWFreq[40][1] = 485;\n        EUC_TWFreq[59][0] = 484;\n        EUC_TWFreq[39][70] = 483;\n        EUC_TWFreq[46][14] = 482;\n        EUC_TWFreq[68][77] = 481;\n        EUC_TWFreq[38][55] = 480;\n        EUC_TWFreq[35][78] = 479;\n        EUC_TWFreq[84][44] = 478;\n        EUC_TWFreq[36][41] = 477;\n        EUC_TWFreq[37][62] = 476;\n        EUC_TWFreq[65][67] = 475;\n        EUC_TWFreq[69][66] = 474;\n        EUC_TWFreq[73][55] = 473;\n        EUC_TWFreq[71][49] = 472;\n        EUC_TWFreq[66][87] = 471;\n        EUC_TWFreq[38][33] = 470;\n        EUC_TWFreq[64][61] = 469;\n        EUC_TWFreq[35][7] = 468;\n        EUC_TWFreq[47][49] = 467;\n        EUC_TWFreq[56][14] = 466;\n        EUC_TWFreq[36][49] = 465;\n        EUC_TWFreq[50][81] = 464;\n        EUC_TWFreq[55][76] = 463;\n        EUC_TWFreq[35][19] = 462;\n        EUC_TWFreq[44][47] = 461;\n        EUC_TWFreq[35][15] = 460;\n        EUC_TWFreq[82][59] = 459;\n        EUC_TWFreq[35][43] = 458;\n        EUC_TWFreq[73][0] = 457;\n        EUC_TWFreq[57][83] = 456;\n        EUC_TWFreq[42][46] = 455;\n        EUC_TWFreq[36][0] = 454;\n        EUC_TWFreq[70][88] = 453;\n        EUC_TWFreq[42][22] = 452;\n        EUC_TWFreq[46][58] = 451;\n        EUC_TWFreq[36][34] = 450;\n        EUC_TWFreq[39][24] = 449;\n        EUC_TWFreq[35][55] = 448;\n        EUC_TWFreq[44][91] = 447;\n        EUC_TWFreq[37][51] = 446;\n        EUC_TWFreq[36][19] = 445;\n        EUC_TWFreq[69][90] = 444;\n        EUC_TWFreq[55][35] = 443;\n        EUC_TWFreq[35][54] = 442;\n        EUC_TWFreq[49][61] = 441;\n        EUC_TWFreq[36][67] = 440;\n        EUC_TWFreq[88][34] = 439;\n        EUC_TWFreq[35][17] = 438;\n        EUC_TWFreq[65][69] = 437;\n        EUC_TWFreq[74][89] = 436;\n        EUC_TWFreq[37][31] = 435;\n        EUC_TWFreq[43][48] = 434;\n        EUC_TWFreq[89][27] = 433;\n        EUC_TWFreq[42][79] = 432;\n        EUC_TWFreq[69][57] = 431;\n        EUC_TWFreq[36][13] = 430;\n        EUC_TWFreq[35][62] = 429;\n        EUC_TWFreq[65][47] = 428;\n        EUC_TWFreq[56][8] = 427;\n        EUC_TWFreq[38][79] = 426;\n        EUC_TWFreq[37][64] = 425;\n        EUC_TWFreq[64][64] = 424;\n        EUC_TWFreq[38][53] = 423;\n        EUC_TWFreq[38][31] = 422;\n        EUC_TWFreq[56][81] = 421;\n        EUC_TWFreq[36][22] = 420;\n        EUC_TWFreq[43][4] = 419;\n        EUC_TWFreq[36][90] = 418;\n        EUC_TWFreq[38][62] = 417;\n        EUC_TWFreq[66][85] = 416;\n        EUC_TWFreq[39][1] = 415;\n        EUC_TWFreq[59][40] = 414;\n        EUC_TWFreq[58][93] = 413;\n        EUC_TWFreq[44][43] = 412;\n        EUC_TWFreq[39][49] = 411;\n        EUC_TWFreq[64][2] = 410;\n        EUC_TWFreq[41][35] = 409;\n        EUC_TWFreq[60][22] = 408;\n        EUC_TWFreq[35][91] = 407;\n        EUC_TWFreq[78][1] = 406;\n        EUC_TWFreq[36][14] = 405;\n        EUC_TWFreq[82][29] = 404;\n        EUC_TWFreq[52][86] = 403;\n        EUC_TWFreq[40][16] = 402;\n        EUC_TWFreq[91][52] = 401;\n        EUC_TWFreq[50][75] = 400;\n        EUC_TWFreq[64][30] = 399;\n        EUC_TWFreq[90][78] = 398;\n        EUC_TWFreq[36][52] = 397;\n        EUC_TWFreq[55][87] = 396;\n        EUC_TWFreq[57][5] = 395;\n        EUC_TWFreq[57][31] = 394;\n        EUC_TWFreq[42][35] = 393;\n        EUC_TWFreq[69][50] = 392;\n        EUC_TWFreq[45][8] = 391;\n        EUC_TWFreq[50][87] = 390;\n        EUC_TWFreq[69][55] = 389;\n        EUC_TWFreq[92][3] = 388;\n        EUC_TWFreq[36][43] = 387;\n        EUC_TWFreq[64][10] = 386;\n        EUC_TWFreq[56][25] = 385;\n        EUC_TWFreq[60][68] = 384;\n        EUC_TWFreq[51][46] = 383;\n        EUC_TWFreq[50][0] = 382;\n        EUC_TWFreq[38][30] = 381;\n        EUC_TWFreq[50][85] = 380;\n        EUC_TWFreq[60][54] = 379;\n        EUC_TWFreq[73][6] = 378;\n        EUC_TWFreq[73][28] = 377;\n        EUC_TWFreq[56][19] = 376;\n        EUC_TWFreq[62][69] = 375;\n        EUC_TWFreq[81][66] = 374;\n        EUC_TWFreq[40][32] = 373;\n        EUC_TWFreq[76][31] = 372;\n        EUC_TWFreq[35][10] = 371;\n        EUC_TWFreq[41][37] = 370;\n        EUC_TWFreq[52][82] = 369;\n        EUC_TWFreq[91][72] = 368;\n        EUC_TWFreq[37][29] = 367;\n        EUC_TWFreq[56][30] = 366;\n        EUC_TWFreq[37][80] = 365;\n        EUC_TWFreq[81][56] = 364;\n        EUC_TWFreq[70][3] = 363;\n        EUC_TWFreq[76][15] = 362;\n        EUC_TWFreq[46][47] = 361;\n        EUC_TWFreq[35][88] = 360;\n        EUC_TWFreq[61][58] = 359;\n        EUC_TWFreq[37][37] = 358;\n        EUC_TWFreq[57][22] = 357;\n        EUC_TWFreq[41][23] = 356;\n        EUC_TWFreq[90][66] = 355;\n        EUC_TWFreq[39][60] = 354;\n        EUC_TWFreq[38][0] = 353;\n        EUC_TWFreq[37][87] = 352;\n        EUC_TWFreq[46][2] = 351;\n        EUC_TWFreq[38][56] = 350;\n        EUC_TWFreq[58][11] = 349;\n        EUC_TWFreq[48][10] = 348;\n        EUC_TWFreq[74][4] = 347;\n        EUC_TWFreq[40][42] = 346;\n        EUC_TWFreq[41][52] = 345;\n        EUC_TWFreq[61][92] = 344;\n        EUC_TWFreq[39][50] = 343;\n        EUC_TWFreq[47][88] = 342;\n        EUC_TWFreq[88][36] = 341;\n        EUC_TWFreq[45][73] = 340;\n        EUC_TWFreq[82][3] = 339;\n        EUC_TWFreq[61][36] = 338;\n        EUC_TWFreq[60][33] = 337;\n        EUC_TWFreq[38][27] = 336;\n        EUC_TWFreq[35][83] = 335;\n        EUC_TWFreq[65][24] = 334;\n        EUC_TWFreq[73][10] = 333;\n        EUC_TWFreq[41][13] = 332;\n        EUC_TWFreq[50][27] = 331;\n        EUC_TWFreq[59][50] = 330;\n        EUC_TWFreq[42][45] = 329;\n        EUC_TWFreq[55][19] = 328;\n        EUC_TWFreq[36][77] = 327;\n        EUC_TWFreq[69][31] = 326;\n        EUC_TWFreq[60][7] = 325;\n        EUC_TWFreq[40][88] = 324;\n        EUC_TWFreq[57][56] = 323;\n        EUC_TWFreq[50][50] = 322;\n        EUC_TWFreq[42][37] = 321;\n        EUC_TWFreq[38][82] = 320;\n        EUC_TWFreq[52][25] = 319;\n        EUC_TWFreq[42][67] = 318;\n        EUC_TWFreq[48][40] = 317;\n        EUC_TWFreq[45][81] = 316;\n        EUC_TWFreq[57][14] = 315;\n        EUC_TWFreq[42][13] = 314;\n        EUC_TWFreq[78][0] = 313;\n        EUC_TWFreq[35][51] = 312;\n        EUC_TWFreq[41][67] = 311;\n        EUC_TWFreq[64][23] = 310;\n        EUC_TWFreq[36][65] = 309;\n        EUC_TWFreq[48][50] = 308;\n        EUC_TWFreq[46][69] = 307;\n        EUC_TWFreq[47][89] = 306;\n        EUC_TWFreq[41][48] = 305;\n        EUC_TWFreq[60][56] = 304;\n        EUC_TWFreq[44][82] = 303;\n        EUC_TWFreq[47][35] = 302;\n        EUC_TWFreq[49][3] = 301;\n        EUC_TWFreq[49][69] = 300;\n        EUC_TWFreq[45][93] = 299;\n        EUC_TWFreq[60][34] = 298;\n        EUC_TWFreq[60][82] = 297;\n        EUC_TWFreq[61][61] = 296;\n        EUC_TWFreq[86][42] = 295;\n        EUC_TWFreq[89][60] = 294;\n        EUC_TWFreq[48][31] = 293;\n        EUC_TWFreq[35][75] = 292;\n        EUC_TWFreq[91][39] = 291;\n        EUC_TWFreq[53][19] = 290;\n        EUC_TWFreq[39][72] = 289;\n        EUC_TWFreq[69][59] = 288;\n        EUC_TWFreq[41][7] = 287;\n        EUC_TWFreq[54][13] = 286;\n        EUC_TWFreq[43][28] = 285;\n        EUC_TWFreq[36][6] = 284;\n        EUC_TWFreq[45][75] = 283;\n        EUC_TWFreq[36][61] = 282;\n        EUC_TWFreq[38][21] = 281;\n        EUC_TWFreq[45][14] = 280;\n        EUC_TWFreq[61][43] = 279;\n        EUC_TWFreq[36][63] = 278;\n        EUC_TWFreq[43][30] = 277;\n        EUC_TWFreq[46][51] = 276;\n        EUC_TWFreq[68][87] = 275;\n        EUC_TWFreq[39][26] = 274;\n        EUC_TWFreq[46][76] = 273;\n        EUC_TWFreq[36][15] = 272;\n        EUC_TWFreq[35][40] = 271;\n        EUC_TWFreq[79][60] = 270;\n        EUC_TWFreq[46][7] = 269;\n        EUC_TWFreq[65][72] = 268;\n        EUC_TWFreq[69][88] = 267;\n        EUC_TWFreq[47][18] = 266;\n        EUC_TWFreq[37][0] = 265;\n        EUC_TWFreq[37][49] = 264;\n        EUC_TWFreq[67][37] = 263;\n        EUC_TWFreq[36][91] = 262;\n        EUC_TWFreq[75][48] = 261;\n        EUC_TWFreq[75][63] = 260;\n        EUC_TWFreq[83][87] = 259;\n        EUC_TWFreq[37][44] = 258;\n        EUC_TWFreq[73][54] = 257;\n        EUC_TWFreq[51][61] = 256;\n        EUC_TWFreq[46][57] = 255;\n        EUC_TWFreq[55][21] = 254;\n        EUC_TWFreq[39][66] = 253;\n        EUC_TWFreq[47][11] = 252;\n        EUC_TWFreq[52][8] = 251;\n        EUC_TWFreq[82][81] = 250;\n        EUC_TWFreq[36][57] = 249;\n        EUC_TWFreq[38][54] = 248;\n        EUC_TWFreq[43][81] = 247;\n        EUC_TWFreq[37][42] = 246;\n        EUC_TWFreq[40][18] = 245;\n        EUC_TWFreq[80][90] = 244;\n        EUC_TWFreq[37][84] = 243;\n        EUC_TWFreq[57][15] = 242;\n        EUC_TWFreq[38][87] = 241;\n        EUC_TWFreq[37][32] = 240;\n        EUC_TWFreq[53][53] = 239;\n        EUC_TWFreq[89][29] = 238;\n        EUC_TWFreq[81][53] = 237;\n        EUC_TWFreq[75][3] = 236;\n        EUC_TWFreq[83][73] = 235;\n        EUC_TWFreq[66][13] = 234;\n        EUC_TWFreq[48][7] = 233;\n        EUC_TWFreq[46][35] = 232;\n        EUC_TWFreq[35][86] = 231;\n        EUC_TWFreq[37][20] = 230;\n        EUC_TWFreq[46][80] = 229;\n        EUC_TWFreq[38][24] = 228;\n        EUC_TWFreq[41][68] = 227;\n        EUC_TWFreq[42][21] = 226;\n        EUC_TWFreq[43][32] = 225;\n        EUC_TWFreq[38][20] = 224;\n        EUC_TWFreq[37][59] = 223;\n        EUC_TWFreq[41][77] = 222;\n        EUC_TWFreq[59][57] = 221;\n        EUC_TWFreq[68][59] = 220;\n        EUC_TWFreq[39][43] = 219;\n        EUC_TWFreq[54][39] = 218;\n        EUC_TWFreq[48][28] = 217;\n        EUC_TWFreq[54][28] = 216;\n        EUC_TWFreq[41][44] = 215;\n        EUC_TWFreq[51][64] = 214;\n        EUC_TWFreq[47][72] = 213;\n        EUC_TWFreq[62][67] = 212;\n        EUC_TWFreq[42][43] = 211;\n        EUC_TWFreq[61][38] = 210;\n        EUC_TWFreq[76][25] = 209;\n        EUC_TWFreq[48][91] = 208;\n        EUC_TWFreq[36][36] = 207;\n        EUC_TWFreq[80][32] = 206;\n        EUC_TWFreq[81][40] = 205;\n        EUC_TWFreq[37][5] = 204;\n        EUC_TWFreq[74][69] = 203;\n        EUC_TWFreq[36][82] = 202;\n        EUC_TWFreq[46][59] = 201;\n        /*\n         * EUC_TWFreq[38][32] = 200; EUC_TWFreq[74][2] = 199; EUC_TWFreq[53][31]\n         * = 198; EUC_TWFreq[35][38] = 197; EUC_TWFreq[46][62] = 196;\n         * EUC_TWFreq[77][31] = 195; EUC_TWFreq[55][74] = 194; EUC_TWFreq[66][6]\n         * = 193; EUC_TWFreq[56][21] = 192; EUC_TWFreq[54][78] = 191;\n         * EUC_TWFreq[43][51] = 190; EUC_TWFreq[64][93] = 189; EUC_TWFreq[92][7]\n         * = 188; EUC_TWFreq[83][89] = 187; EUC_TWFreq[69][9] = 186;\n         * EUC_TWFreq[45][4] = 185; EUC_TWFreq[53][9] = 184; EUC_TWFreq[43][2] =\n         * 183; EUC_TWFreq[35][11] = 182; EUC_TWFreq[51][25] = 181;\n         * EUC_TWFreq[52][71] = 180; EUC_TWFreq[81][67] = 179;\n         * EUC_TWFreq[37][33] = 178; EUC_TWFreq[38][57] = 177;\n         * EUC_TWFreq[39][77] = 176; EUC_TWFreq[40][26] = 175;\n         * EUC_TWFreq[37][21] = 174; EUC_TWFreq[81][70] = 173;\n         * EUC_TWFreq[56][80] = 172; EUC_TWFreq[65][14] = 171;\n         * EUC_TWFreq[62][47] = 170; EUC_TWFreq[56][54] = 169;\n         * EUC_TWFreq[45][17] = 168; EUC_TWFreq[52][52] = 167;\n         * EUC_TWFreq[74][30] = 166; EUC_TWFreq[60][57] = 165;\n         * EUC_TWFreq[41][15] = 164; EUC_TWFreq[47][69] = 163;\n         * EUC_TWFreq[61][11] = 162; EUC_TWFreq[72][25] = 161;\n         * EUC_TWFreq[82][56] = 160; EUC_TWFreq[76][92] = 159;\n         * EUC_TWFreq[51][22] = 158; EUC_TWFreq[55][69] = 157;\n         * EUC_TWFreq[49][43] = 156; EUC_TWFreq[69][49] = 155;\n         * EUC_TWFreq[88][42] = 154; EUC_TWFreq[84][41] = 153;\n         * EUC_TWFreq[79][33] = 152; EUC_TWFreq[47][17] = 151;\n         * EUC_TWFreq[52][88] = 150; EUC_TWFreq[63][74] = 149;\n         * EUC_TWFreq[50][32] = 148; EUC_TWFreq[65][10] = 147; EUC_TWFreq[57][6]\n         * = 146; EUC_TWFreq[52][23] = 145; EUC_TWFreq[36][70] = 144;\n         * EUC_TWFreq[65][55] = 143; EUC_TWFreq[35][27] = 142;\n         * EUC_TWFreq[57][63] = 141; EUC_TWFreq[39][92] = 140;\n         * EUC_TWFreq[79][75] = 139; EUC_TWFreq[36][30] = 138;\n         * EUC_TWFreq[53][60] = 137; EUC_TWFreq[55][43] = 136;\n         * EUC_TWFreq[71][22] = 135; EUC_TWFreq[43][16] = 134;\n         * EUC_TWFreq[65][21] = 133; EUC_TWFreq[84][51] = 132;\n         * EUC_TWFreq[43][64] = 131; EUC_TWFreq[87][91] = 130;\n         * EUC_TWFreq[47][45] = 129; EUC_TWFreq[65][29] = 128;\n         * EUC_TWFreq[88][16] = 127; EUC_TWFreq[50][5] = 126; EUC_TWFreq[47][33]\n         * = 125; EUC_TWFreq[46][27] = 124; EUC_TWFreq[85][2] = 123;\n         * EUC_TWFreq[43][77] = 122; EUC_TWFreq[70][9] = 121; EUC_TWFreq[41][54]\n         * = 120; EUC_TWFreq[56][12] = 119; EUC_TWFreq[90][65] = 118;\n         * EUC_TWFreq[91][50] = 117; EUC_TWFreq[48][41] = 116;\n         * EUC_TWFreq[35][89] = 115; EUC_TWFreq[90][83] = 114;\n         * EUC_TWFreq[44][40] = 113; EUC_TWFreq[50][88] = 112;\n         * EUC_TWFreq[72][39] = 111; EUC_TWFreq[45][3] = 110; EUC_TWFreq[71][33]\n         * = 109; EUC_TWFreq[39][12] = 108; EUC_TWFreq[59][24] = 107;\n         * EUC_TWFreq[60][62] = 106; EUC_TWFreq[44][33] = 105;\n         * EUC_TWFreq[53][70] = 104; EUC_TWFreq[77][90] = 103;\n         * EUC_TWFreq[50][58] = 102; EUC_TWFreq[54][1] = 101; EUC_TWFreq[73][19]\n         * = 100; EUC_TWFreq[37][3] = 99; EUC_TWFreq[49][91] = 98;\n         * EUC_TWFreq[88][43] = 97; EUC_TWFreq[36][78] = 96; EUC_TWFreq[44][20]\n         * = 95; EUC_TWFreq[64][15] = 94; EUC_TWFreq[72][28] = 93;\n         * EUC_TWFreq[70][13] = 92; EUC_TWFreq[65][83] = 91; EUC_TWFreq[58][68]\n         * = 90; EUC_TWFreq[59][32] = 89; EUC_TWFreq[39][13] = 88;\n         * EUC_TWFreq[55][64] = 87; EUC_TWFreq[56][59] = 86; EUC_TWFreq[39][17]\n         * = 85; EUC_TWFreq[55][84] = 84; EUC_TWFreq[77][85] = 83;\n         * EUC_TWFreq[60][19] = 82; EUC_TWFreq[62][82] = 81; EUC_TWFreq[78][16]\n         * = 80; EUC_TWFreq[66][8] = 79; EUC_TWFreq[39][42] = 78;\n         * EUC_TWFreq[61][24] = 77; EUC_TWFreq[57][67] = 76; EUC_TWFreq[38][83]\n         * = 75; EUC_TWFreq[36][53] = 74; EUC_TWFreq[67][76] = 73;\n         * EUC_TWFreq[37][91] = 72; EUC_TWFreq[44][26] = 71; EUC_TWFreq[72][86]\n         * = 70; EUC_TWFreq[44][87] = 69; EUC_TWFreq[45][50] = 68;\n         * EUC_TWFreq[58][4] = 67; EUC_TWFreq[86][65] = 66; EUC_TWFreq[45][56] =\n         * 65; EUC_TWFreq[79][49] = 64; EUC_TWFreq[35][3] = 63;\n         * EUC_TWFreq[48][83] = 62; EUC_TWFreq[71][21] = 61; EUC_TWFreq[77][93]\n         * = 60; EUC_TWFreq[87][92] = 59; EUC_TWFreq[38][35] = 58;\n         * EUC_TWFreq[66][17] = 57; EUC_TWFreq[37][66] = 56; EUC_TWFreq[51][42]\n         * = 55; EUC_TWFreq[57][73] = 54; EUC_TWFreq[51][54] = 53;\n         * EUC_TWFreq[75][64] = 52; EUC_TWFreq[35][5] = 51; EUC_TWFreq[49][40] =\n         * 50; EUC_TWFreq[58][35] = 49; EUC_TWFreq[67][88] = 48;\n         * EUC_TWFreq[60][51] = 47; EUC_TWFreq[36][92] = 46; EUC_TWFreq[44][41]\n         * = 45; EUC_TWFreq[58][29] = 44; EUC_TWFreq[43][62] = 43;\n         * EUC_TWFreq[56][23] = 42; EUC_TWFreq[67][44] = 41; EUC_TWFreq[52][91]\n         * = 40; EUC_TWFreq[42][81] = 39; EUC_TWFreq[64][25] = 38;\n         * EUC_TWFreq[35][36] = 37; EUC_TWFreq[47][73] = 36; EUC_TWFreq[36][1] =\n         * 35; EUC_TWFreq[65][84] = 34; EUC_TWFreq[73][1] = 33;\n         * EUC_TWFreq[79][66] = 32; EUC_TWFreq[69][14] = 31; EUC_TWFreq[65][28]\n         * = 30; EUC_TWFreq[60][93] = 29; EUC_TWFreq[72][79] = 28;\n         * EUC_TWFreq[48][0] = 27; EUC_TWFreq[73][43] = 26; EUC_TWFreq[66][47] =\n         * 25; EUC_TWFreq[41][18] = 24; EUC_TWFreq[51][10] = 23;\n         * EUC_TWFreq[59][7] = 22; EUC_TWFreq[53][27] = 21; EUC_TWFreq[86][67] =\n         * 20; EUC_TWFreq[49][87] = 19; EUC_TWFreq[52][28] = 18;\n         * EUC_TWFreq[52][12] = 17; EUC_TWFreq[42][30] = 16; EUC_TWFreq[65][35]\n         * = 15; EUC_TWFreq[46][64] = 14; EUC_TWFreq[71][7] = 13;\n         * EUC_TWFreq[56][57] = 12; EUC_TWFreq[56][31] = 11; EUC_TWFreq[41][31]\n         * = 10; EUC_TWFreq[48][59] = 9; EUC_TWFreq[63][92] = 8;\n         * EUC_TWFreq[62][57] = 7; EUC_TWFreq[65][87] = 6; EUC_TWFreq[70][10] =\n         * 5; EUC_TWFreq[52][40] = 4; EUC_TWFreq[40][22] = 3; EUC_TWFreq[65][91]\n         * = 2; EUC_TWFreq[50][25] = 1; EUC_TWFreq[35][84] = 0;\n         */\n        GBKFreq[52][132] = 600;\n        GBKFreq[73][135] = 599;\n        GBKFreq[49][123] = 598;\n        GBKFreq[77][146] = 597;\n        GBKFreq[81][123] = 596;\n        GBKFreq[82][144] = 595;\n        GBKFreq[51][179] = 594;\n        GBKFreq[83][154] = 593;\n        GBKFreq[71][139] = 592;\n        GBKFreq[64][139] = 591;\n        GBKFreq[85][144] = 590;\n        GBKFreq[52][125] = 589;\n        GBKFreq[88][25] = 588;\n        GBKFreq[81][106] = 587;\n        GBKFreq[81][148] = 586;\n        GBKFreq[62][137] = 585;\n        GBKFreq[94][0] = 584;\n        GBKFreq[1][64] = 583;\n        GBKFreq[67][163] = 582;\n        GBKFreq[20][190] = 581;\n        GBKFreq[57][131] = 580;\n        GBKFreq[29][169] = 579;\n        GBKFreq[72][143] = 578;\n        GBKFreq[0][173] = 577;\n        GBKFreq[11][23] = 576;\n        GBKFreq[61][141] = 575;\n        GBKFreq[60][123] = 574;\n        GBKFreq[81][114] = 573;\n        GBKFreq[82][131] = 572;\n        GBKFreq[67][156] = 571;\n        GBKFreq[71][167] = 570;\n        GBKFreq[20][50] = 569;\n        GBKFreq[77][132] = 568;\n        GBKFreq[84][38] = 567;\n        GBKFreq[26][29] = 566;\n        GBKFreq[74][187] = 565;\n        GBKFreq[62][116] = 564;\n        GBKFreq[67][135] = 563;\n        GBKFreq[5][86] = 562;\n        GBKFreq[72][186] = 561;\n        GBKFreq[75][161] = 560;\n        GBKFreq[78][130] = 559;\n        GBKFreq[94][30] = 558;\n        GBKFreq[84][72] = 557;\n        GBKFreq[1][67] = 556;\n        GBKFreq[75][172] = 555;\n        GBKFreq[74][185] = 554;\n        GBKFreq[53][160] = 553;\n        GBKFreq[123][14] = 552;\n        GBKFreq[79][97] = 551;\n        GBKFreq[85][110] = 550;\n        GBKFreq[78][171] = 549;\n        GBKFreq[52][131] = 548;\n        GBKFreq[56][100] = 547;\n        GBKFreq[50][182] = 546;\n        GBKFreq[94][64] = 545;\n        GBKFreq[106][74] = 544;\n        GBKFreq[11][102] = 543;\n        GBKFreq[53][124] = 542;\n        GBKFreq[24][3] = 541;\n        GBKFreq[86][148] = 540;\n        GBKFreq[53][184] = 539;\n        GBKFreq[86][147] = 538;\n        GBKFreq[96][161] = 537;\n        GBKFreq[82][77] = 536;\n        GBKFreq[59][146] = 535;\n        GBKFreq[84][126] = 534;\n        GBKFreq[79][132] = 533;\n        GBKFreq[85][123] = 532;\n        GBKFreq[71][101] = 531;\n        GBKFreq[85][106] = 530;\n        GBKFreq[6][184] = 529;\n        GBKFreq[57][156] = 528;\n        GBKFreq[75][104] = 527;\n        GBKFreq[50][137] = 526;\n        GBKFreq[79][133] = 525;\n        GBKFreq[76][108] = 524;\n        GBKFreq[57][142] = 523;\n        GBKFreq[84][130] = 522;\n        GBKFreq[52][128] = 521;\n        GBKFreq[47][44] = 520;\n        GBKFreq[52][152] = 519;\n        GBKFreq[54][104] = 518;\n        GBKFreq[30][47] = 517;\n        GBKFreq[71][123] = 516;\n        GBKFreq[52][107] = 515;\n        GBKFreq[45][84] = 514;\n        GBKFreq[107][118] = 513;\n        GBKFreq[5][161] = 512;\n        GBKFreq[48][126] = 511;\n        GBKFreq[67][170] = 510;\n        GBKFreq[43][6] = 509;\n        GBKFreq[70][112] = 508;\n        GBKFreq[86][174] = 507;\n        GBKFreq[84][166] = 506;\n        GBKFreq[79][130] = 505;\n        GBKFreq[57][141] = 504;\n        GBKFreq[81][178] = 503;\n        GBKFreq[56][187] = 502;\n        GBKFreq[81][162] = 501;\n        GBKFreq[53][104] = 500;\n        GBKFreq[123][35] = 499;\n        GBKFreq[70][169] = 498;\n        GBKFreq[69][164] = 497;\n        GBKFreq[109][61] = 496;\n        GBKFreq[73][130] = 495;\n        GBKFreq[62][134] = 494;\n        GBKFreq[54][125] = 493;\n        GBKFreq[79][105] = 492;\n        GBKFreq[70][165] = 491;\n        GBKFreq[71][189] = 490;\n        GBKFreq[23][147] = 489;\n        GBKFreq[51][139] = 488;\n        GBKFreq[47][137] = 487;\n        GBKFreq[77][123] = 486;\n        GBKFreq[86][183] = 485;\n        GBKFreq[63][173] = 484;\n        GBKFreq[79][144] = 483;\n        GBKFreq[84][159] = 482;\n        GBKFreq[60][91] = 481;\n        GBKFreq[66][187] = 480;\n        GBKFreq[73][114] = 479;\n        GBKFreq[85][56] = 478;\n        GBKFreq[71][149] = 477;\n        GBKFreq[84][189] = 476;\n        GBKFreq[104][31] = 475;\n        GBKFreq[83][82] = 474;\n        GBKFreq[68][35] = 473;\n        GBKFreq[11][77] = 472;\n        GBKFreq[15][155] = 471;\n        GBKFreq[83][153] = 470;\n        GBKFreq[71][1] = 469;\n        GBKFreq[53][190] = 468;\n        GBKFreq[50][135] = 467;\n        GBKFreq[3][147] = 466;\n        GBKFreq[48][136] = 465;\n        GBKFreq[66][166] = 464;\n        GBKFreq[55][159] = 463;\n        GBKFreq[82][150] = 462;\n        GBKFreq[58][178] = 461;\n        GBKFreq[64][102] = 460;\n        GBKFreq[16][106] = 459;\n        GBKFreq[68][110] = 458;\n        GBKFreq[54][14] = 457;\n        GBKFreq[60][140] = 456;\n        GBKFreq[91][71] = 455;\n        GBKFreq[54][150] = 454;\n        GBKFreq[78][177] = 453;\n        GBKFreq[78][117] = 452;\n        GBKFreq[104][12] = 451;\n        GBKFreq[73][150] = 450;\n        GBKFreq[51][142] = 449;\n        GBKFreq[81][145] = 448;\n        GBKFreq[66][183] = 447;\n        GBKFreq[51][178] = 446;\n        GBKFreq[75][107] = 445;\n        GBKFreq[65][119] = 444;\n        GBKFreq[69][176] = 443;\n        GBKFreq[59][122] = 442;\n        GBKFreq[78][160] = 441;\n        GBKFreq[85][183] = 440;\n        GBKFreq[105][16] = 439;\n        GBKFreq[73][110] = 438;\n        GBKFreq[104][39] = 437;\n        GBKFreq[119][16] = 436;\n        GBKFreq[76][162] = 435;\n        GBKFreq[67][152] = 434;\n        GBKFreq[82][24] = 433;\n        GBKFreq[73][121] = 432;\n        GBKFreq[83][83] = 431;\n        GBKFreq[82][145] = 430;\n        GBKFreq[49][133] = 429;\n        GBKFreq[94][13] = 428;\n        GBKFreq[58][139] = 427;\n        GBKFreq[74][189] = 426;\n        GBKFreq[66][177] = 425;\n        GBKFreq[85][184] = 424;\n        GBKFreq[55][183] = 423;\n        GBKFreq[71][107] = 422;\n        GBKFreq[11][98] = 421;\n        GBKFreq[72][153] = 420;\n        GBKFreq[2][137] = 419;\n        GBKFreq[59][147] = 418;\n        GBKFreq[58][152] = 417;\n        GBKFreq[55][144] = 416;\n        GBKFreq[73][125] = 415;\n        GBKFreq[52][154] = 414;\n        GBKFreq[70][178] = 413;\n        GBKFreq[79][148] = 412;\n        GBKFreq[63][143] = 411;\n        GBKFreq[50][140] = 410;\n        GBKFreq[47][145] = 409;\n        GBKFreq[48][123] = 408;\n        GBKFreq[56][107] = 407;\n        GBKFreq[84][83] = 406;\n        GBKFreq[59][112] = 405;\n        GBKFreq[124][72] = 404;\n        GBKFreq[79][99] = 403;\n        GBKFreq[3][37] = 402;\n        GBKFreq[114][55] = 401;\n        GBKFreq[85][152] = 400;\n        GBKFreq[60][47] = 399;\n        GBKFreq[65][96] = 398;\n        GBKFreq[74][110] = 397;\n        GBKFreq[86][182] = 396;\n        GBKFreq[50][99] = 395;\n        GBKFreq[67][186] = 394;\n        GBKFreq[81][74] = 393;\n        GBKFreq[80][37] = 392;\n        GBKFreq[21][60] = 391;\n        GBKFreq[110][12] = 390;\n        GBKFreq[60][162] = 389;\n        GBKFreq[29][115] = 388;\n        GBKFreq[83][130] = 387;\n        GBKFreq[52][136] = 386;\n        GBKFreq[63][114] = 385;\n        GBKFreq[49][127] = 384;\n        GBKFreq[83][109] = 383;\n        GBKFreq[66][128] = 382;\n        GBKFreq[78][136] = 381;\n        GBKFreq[81][180] = 380;\n        GBKFreq[76][104] = 379;\n        GBKFreq[56][156] = 378;\n        GBKFreq[61][23] = 377;\n        GBKFreq[4][30] = 376;\n        GBKFreq[69][154] = 375;\n        GBKFreq[100][37] = 374;\n        GBKFreq[54][177] = 373;\n        GBKFreq[23][119] = 372;\n        GBKFreq[71][171] = 371;\n        GBKFreq[84][146] = 370;\n        GBKFreq[20][184] = 369;\n        GBKFreq[86][76] = 368;\n        GBKFreq[74][132] = 367;\n        GBKFreq[47][97] = 366;\n        GBKFreq[82][137] = 365;\n        GBKFreq[94][56] = 364;\n        GBKFreq[92][30] = 363;\n        GBKFreq[19][117] = 362;\n        GBKFreq[48][173] = 361;\n        GBKFreq[2][136] = 360;\n        GBKFreq[7][182] = 359;\n        GBKFreq[74][188] = 358;\n        GBKFreq[14][132] = 357;\n        GBKFreq[62][172] = 356;\n        GBKFreq[25][39] = 355;\n        GBKFreq[85][129] = 354;\n        GBKFreq[64][98] = 353;\n        GBKFreq[67][127] = 352;\n        GBKFreq[72][167] = 351;\n        GBKFreq[57][143] = 350;\n        GBKFreq[76][187] = 349;\n        GBKFreq[83][181] = 348;\n        GBKFreq[84][10] = 347;\n        GBKFreq[55][166] = 346;\n        GBKFreq[55][188] = 345;\n        GBKFreq[13][151] = 344;\n        GBKFreq[62][124] = 343;\n        GBKFreq[53][136] = 342;\n        GBKFreq[106][57] = 341;\n        GBKFreq[47][166] = 340;\n        GBKFreq[109][30] = 339;\n        GBKFreq[78][114] = 338;\n        GBKFreq[83][19] = 337;\n        GBKFreq[56][162] = 336;\n        GBKFreq[60][177] = 335;\n        GBKFreq[88][9] = 334;\n        GBKFreq[74][163] = 333;\n        GBKFreq[52][156] = 332;\n        GBKFreq[71][180] = 331;\n        GBKFreq[60][57] = 330;\n        GBKFreq[72][173] = 329;\n        GBKFreq[82][91] = 328;\n        GBKFreq[51][186] = 327;\n        GBKFreq[75][86] = 326;\n        GBKFreq[75][78] = 325;\n        GBKFreq[76][170] = 324;\n        GBKFreq[60][147] = 323;\n        GBKFreq[82][75] = 322;\n        GBKFreq[80][148] = 321;\n        GBKFreq[86][150] = 320;\n        GBKFreq[13][95] = 319;\n        GBKFreq[0][11] = 318;\n        GBKFreq[84][190] = 317;\n        GBKFreq[76][166] = 316;\n        GBKFreq[14][72] = 315;\n        GBKFreq[67][144] = 314;\n        GBKFreq[84][44] = 313;\n        GBKFreq[72][125] = 312;\n        GBKFreq[66][127] = 311;\n        GBKFreq[60][25] = 310;\n        GBKFreq[70][146] = 309;\n        GBKFreq[79][135] = 308;\n        GBKFreq[54][135] = 307;\n        GBKFreq[60][104] = 306;\n        GBKFreq[55][132] = 305;\n        GBKFreq[94][2] = 304;\n        GBKFreq[54][133] = 303;\n        GBKFreq[56][190] = 302;\n        GBKFreq[58][174] = 301;\n        GBKFreq[80][144] = 300;\n        GBKFreq[85][113] = 299;\n        /*\n         * GBKFreq[83][15] = 298; GBKFreq[105][80] = 297; GBKFreq[7][179] = 296;\n         * GBKFreq[93][4] = 295; GBKFreq[123][40] = 294; GBKFreq[85][120] = 293;\n         * GBKFreq[77][165] = 292; GBKFreq[86][67] = 291; GBKFreq[25][162] =\n         * 290; GBKFreq[77][183] = 289; GBKFreq[83][71] = 288; GBKFreq[78][99] =\n         * 287; GBKFreq[72][177] = 286; GBKFreq[71][97] = 285; GBKFreq[58][111]\n         * = 284; GBKFreq[77][175] = 283; GBKFreq[76][181] = 282;\n         * GBKFreq[71][142] = 281; GBKFreq[64][150] = 280; GBKFreq[5][142] =\n         * 279; GBKFreq[73][128] = 278; GBKFreq[73][156] = 277; GBKFreq[60][188]\n         * = 276; GBKFreq[64][56] = 275; GBKFreq[74][128] = 274;\n         * GBKFreq[48][163] = 273; GBKFreq[54][116] = 272; GBKFreq[73][127] =\n         * 271; GBKFreq[16][176] = 270; GBKFreq[62][149] = 269; GBKFreq[105][96]\n         * = 268; GBKFreq[55][186] = 267; GBKFreq[4][51] = 266; GBKFreq[48][113]\n         * = 265; GBKFreq[48][152] = 264; GBKFreq[23][9] = 263; GBKFreq[56][102]\n         * = 262; GBKFreq[11][81] = 261; GBKFreq[82][112] = 260; GBKFreq[65][85]\n         * = 259; GBKFreq[69][125] = 258; GBKFreq[68][31] = 257; GBKFreq[5][20]\n         * = 256; GBKFreq[60][176] = 255; GBKFreq[82][81] = 254;\n         * GBKFreq[72][107] = 253; GBKFreq[3][52] = 252; GBKFreq[71][157] = 251;\n         * GBKFreq[24][46] = 250; GBKFreq[69][108] = 249; GBKFreq[78][178] =\n         * 248; GBKFreq[9][69] = 247; GBKFreq[73][144] = 246; GBKFreq[63][187] =\n         * 245; GBKFreq[68][36] = 244; GBKFreq[47][151] = 243; GBKFreq[14][74] =\n         * 242; GBKFreq[47][114] = 241; GBKFreq[80][171] = 240; GBKFreq[75][152]\n         * = 239; GBKFreq[86][40] = 238; GBKFreq[93][43] = 237; GBKFreq[2][50] =\n         * 236; GBKFreq[62][66] = 235; GBKFreq[1][183] = 234; GBKFreq[74][124] =\n         * 233; GBKFreq[58][104] = 232; GBKFreq[83][106] = 231; GBKFreq[60][144]\n         * = 230; GBKFreq[48][99] = 229; GBKFreq[54][157] = 228;\n         * GBKFreq[70][179] = 227; GBKFreq[61][127] = 226; GBKFreq[57][135] =\n         * 225; GBKFreq[59][190] = 224; GBKFreq[77][116] = 223; GBKFreq[26][17]\n         * = 222; GBKFreq[60][13] = 221; GBKFreq[71][38] = 220; GBKFreq[85][177]\n         * = 219; GBKFreq[59][73] = 218; GBKFreq[50][150] = 217;\n         * GBKFreq[79][102] = 216; GBKFreq[76][118] = 215; GBKFreq[67][132] =\n         * 214; GBKFreq[73][146] = 213; GBKFreq[83][184] = 212; GBKFreq[86][159]\n         * = 211; GBKFreq[95][120] = 210; GBKFreq[23][139] = 209;\n         * GBKFreq[64][183] = 208; GBKFreq[85][103] = 207; GBKFreq[41][90] =\n         * 206; GBKFreq[87][72] = 205; GBKFreq[62][104] = 204; GBKFreq[79][168]\n         * = 203; GBKFreq[79][150] = 202; GBKFreq[104][20] = 201;\n         * GBKFreq[56][114] = 200; GBKFreq[84][26] = 199; GBKFreq[57][99] = 198;\n         * GBKFreq[62][154] = 197; GBKFreq[47][98] = 196; GBKFreq[61][64] = 195;\n         * GBKFreq[112][18] = 194; GBKFreq[123][19] = 193; GBKFreq[4][98] = 192;\n         * GBKFreq[47][163] = 191; GBKFreq[66][188] = 190; GBKFreq[81][85] =\n         * 189; GBKFreq[82][30] = 188; GBKFreq[65][83] = 187; GBKFreq[67][24] =\n         * 186; GBKFreq[68][179] = 185; GBKFreq[55][177] = 184; GBKFreq[2][122]\n         * = 183; GBKFreq[47][139] = 182; GBKFreq[79][158] = 181;\n         * GBKFreq[64][143] = 180; GBKFreq[100][24] = 179; GBKFreq[73][103] =\n         * 178; GBKFreq[50][148] = 177; GBKFreq[86][97] = 176; GBKFreq[59][116]\n         * = 175; GBKFreq[64][173] = 174; GBKFreq[99][91] = 173; GBKFreq[11][99]\n         * = 172; GBKFreq[78][179] = 171; GBKFreq[18][17] = 170;\n         * GBKFreq[58][185] = 169; GBKFreq[47][165] = 168; GBKFreq[67][131] =\n         * 167; GBKFreq[94][40] = 166; GBKFreq[74][153] = 165; GBKFreq[79][142]\n         * = 164; GBKFreq[57][98] = 163; GBKFreq[1][164] = 162; GBKFreq[55][168]\n         * = 161; GBKFreq[13][141] = 160; GBKFreq[51][31] = 159;\n         * GBKFreq[57][178] = 158; GBKFreq[50][189] = 157; GBKFreq[60][167] =\n         * 156; GBKFreq[80][34] = 155; GBKFreq[109][80] = 154; GBKFreq[85][54] =\n         * 153; GBKFreq[69][183] = 152; GBKFreq[67][143] = 151; GBKFreq[47][120]\n         * = 150; GBKFreq[45][75] = 149; GBKFreq[82][98] = 148; GBKFreq[83][22]\n         * = 147; GBKFreq[13][103] = 146; GBKFreq[49][174] = 145;\n         * GBKFreq[57][181] = 144; GBKFreq[64][127] = 143; GBKFreq[61][131] =\n         * 142; GBKFreq[52][180] = 141; GBKFreq[74][134] = 140; GBKFreq[84][187]\n         * = 139; GBKFreq[81][189] = 138; GBKFreq[47][160] = 137;\n         * GBKFreq[66][148] = 136; GBKFreq[7][4] = 135; GBKFreq[85][134] = 134;\n         * GBKFreq[88][13] = 133; GBKFreq[88][80] = 132; GBKFreq[69][166] = 131;\n         * GBKFreq[86][18] = 130; GBKFreq[79][141] = 129; GBKFreq[50][108] =\n         * 128; GBKFreq[94][69] = 127; GBKFreq[81][110] = 126; GBKFreq[69][119]\n         * = 125; GBKFreq[72][161] = 124; GBKFreq[106][45] = 123;\n         * GBKFreq[73][124] = 122; GBKFreq[94][28] = 121; GBKFreq[63][174] =\n         * 120; GBKFreq[3][149] = 119; GBKFreq[24][160] = 118; GBKFreq[113][94]\n         * = 117; GBKFreq[56][138] = 116; GBKFreq[64][185] = 115;\n         * GBKFreq[86][56] = 114; GBKFreq[56][150] = 113; GBKFreq[110][55] =\n         * 112; GBKFreq[28][13] = 111; GBKFreq[54][190] = 110; GBKFreq[8][180] =\n         * 109; GBKFreq[73][149] = 108; GBKFreq[80][155] = 107; GBKFreq[83][172]\n         * = 106; GBKFreq[67][174] = 105; GBKFreq[64][180] = 104;\n         * GBKFreq[84][46] = 103; GBKFreq[91][74] = 102; GBKFreq[69][134] = 101;\n         * GBKFreq[61][107] = 100; GBKFreq[47][171] = 99; GBKFreq[59][51] = 98;\n         * GBKFreq[109][74] = 97; GBKFreq[64][174] = 96; GBKFreq[52][151] = 95;\n         * GBKFreq[51][176] = 94; GBKFreq[80][157] = 93; GBKFreq[94][31] = 92;\n         * GBKFreq[79][155] = 91; GBKFreq[72][174] = 90; GBKFreq[69][113] = 89;\n         * GBKFreq[83][167] = 88; GBKFreq[83][122] = 87; GBKFreq[8][178] = 86;\n         * GBKFreq[70][186] = 85; GBKFreq[59][153] = 84; GBKFreq[84][68] = 83;\n         * GBKFreq[79][39] = 82; GBKFreq[47][180] = 81; GBKFreq[88][53] = 80;\n         * GBKFreq[57][154] = 79; GBKFreq[47][153] = 78; GBKFreq[3][153] = 77;\n         * GBKFreq[76][134] = 76; GBKFreq[51][166] = 75; GBKFreq[58][176] = 74;\n         * GBKFreq[27][138] = 73; GBKFreq[73][126] = 72; GBKFreq[76][185] = 71;\n         * GBKFreq[52][186] = 70; GBKFreq[81][151] = 69; GBKFreq[26][50] = 68;\n         * GBKFreq[76][173] = 67; GBKFreq[106][56] = 66; GBKFreq[85][142] = 65;\n         * GBKFreq[11][103] = 64; GBKFreq[69][159] = 63; GBKFreq[53][142] = 62;\n         * GBKFreq[7][6] = 61; GBKFreq[84][59] = 60; GBKFreq[86][3] = 59;\n         * GBKFreq[64][144] = 58; GBKFreq[1][187] = 57; GBKFreq[82][128] = 56;\n         * GBKFreq[3][66] = 55; GBKFreq[68][133] = 54; GBKFreq[55][167] = 53;\n         * GBKFreq[52][130] = 52; GBKFreq[61][133] = 51; GBKFreq[72][181] = 50;\n         * GBKFreq[25][98] = 49; GBKFreq[84][149] = 48; GBKFreq[91][91] = 47;\n         * GBKFreq[47][188] = 46; GBKFreq[68][130] = 45; GBKFreq[22][44] = 44;\n         * GBKFreq[81][121] = 43; GBKFreq[72][140] = 42; GBKFreq[55][133] = 41;\n         * GBKFreq[55][185] = 40; GBKFreq[56][105] = 39; GBKFreq[60][30] = 38;\n         * GBKFreq[70][103] = 37; GBKFreq[62][141] = 36; GBKFreq[70][144] = 35;\n         * GBKFreq[59][111] = 34; GBKFreq[54][17] = 33; GBKFreq[18][190] = 32;\n         * GBKFreq[65][164] = 31; GBKFreq[83][125] = 30; GBKFreq[61][121] = 29;\n         * GBKFreq[48][13] = 28; GBKFreq[51][189] = 27; GBKFreq[65][68] = 26;\n         * GBKFreq[7][0] = 25; GBKFreq[76][188] = 24; GBKFreq[85][117] = 23;\n         * GBKFreq[45][33] = 22; GBKFreq[78][187] = 21; GBKFreq[106][48] = 20;\n         * GBKFreq[59][52] = 19; GBKFreq[86][185] = 18; GBKFreq[84][121] = 17;\n         * GBKFreq[82][189] = 16; GBKFreq[68][156] = 15; GBKFreq[55][125] = 14;\n         * GBKFreq[65][175] = 13; GBKFreq[7][140] = 12; GBKFreq[50][106] = 11;\n         * GBKFreq[59][124] = 10; GBKFreq[67][115] = 9; GBKFreq[82][114] = 8;\n         * GBKFreq[74][121] = 7; GBKFreq[106][69] = 6; GBKFreq[94][27] = 5;\n         * GBKFreq[78][98] = 4; GBKFreq[85][186] = 3; GBKFreq[108][90] = 2;\n         * GBKFreq[62][160] = 1; GBKFreq[60][169] = 0;\n         */\n        KRFreq[31][43] = 600;\n        KRFreq[19][56] = 599;\n        KRFreq[38][46] = 598;\n        KRFreq[3][3] = 597;\n        KRFreq[29][77] = 596;\n        KRFreq[19][33] = 595;\n        KRFreq[30][0] = 594;\n        KRFreq[29][89] = 593;\n        KRFreq[31][26] = 592;\n        KRFreq[31][38] = 591;\n        KRFreq[32][85] = 590;\n        KRFreq[15][0] = 589;\n        KRFreq[16][54] = 588;\n        KRFreq[15][76] = 587;\n        KRFreq[31][25] = 586;\n        KRFreq[23][13] = 585;\n        KRFreq[28][34] = 584;\n        KRFreq[18][9] = 583;\n        KRFreq[29][37] = 582;\n        KRFreq[22][45] = 581;\n        KRFreq[19][46] = 580;\n        KRFreq[16][65] = 579;\n        KRFreq[23][5] = 578;\n        KRFreq[26][70] = 577;\n        KRFreq[31][53] = 576;\n        KRFreq[27][12] = 575;\n        KRFreq[30][67] = 574;\n        KRFreq[31][57] = 573;\n        KRFreq[20][20] = 572;\n        KRFreq[30][31] = 571;\n        KRFreq[20][72] = 570;\n        KRFreq[15][51] = 569;\n        KRFreq[3][8] = 568;\n        KRFreq[32][53] = 567;\n        KRFreq[27][85] = 566;\n        KRFreq[25][23] = 565;\n        KRFreq[15][44] = 564;\n        KRFreq[32][3] = 563;\n        KRFreq[31][68] = 562;\n        KRFreq[30][24] = 561;\n        KRFreq[29][49] = 560;\n        KRFreq[27][49] = 559;\n        KRFreq[23][23] = 558;\n        KRFreq[31][91] = 557;\n        KRFreq[31][46] = 556;\n        KRFreq[19][74] = 555;\n        KRFreq[27][27] = 554;\n        KRFreq[3][17] = 553;\n        KRFreq[20][38] = 552;\n        KRFreq[21][82] = 551;\n        KRFreq[28][25] = 550;\n        KRFreq[32][5] = 549;\n        KRFreq[31][23] = 548;\n        KRFreq[25][45] = 547;\n        KRFreq[32][87] = 546;\n        KRFreq[18][26] = 545;\n        KRFreq[24][10] = 544;\n        KRFreq[26][82] = 543;\n        KRFreq[15][89] = 542;\n        KRFreq[28][36] = 541;\n        KRFreq[28][31] = 540;\n        KRFreq[16][23] = 539;\n        KRFreq[16][77] = 538;\n        KRFreq[19][84] = 537;\n        KRFreq[23][72] = 536;\n        KRFreq[38][48] = 535;\n        KRFreq[23][2] = 534;\n        KRFreq[30][20] = 533;\n        KRFreq[38][47] = 532;\n        KRFreq[39][12] = 531;\n        KRFreq[23][21] = 530;\n        KRFreq[18][17] = 529;\n        KRFreq[30][87] = 528;\n        KRFreq[29][62] = 527;\n        KRFreq[29][87] = 526;\n        KRFreq[34][53] = 525;\n        KRFreq[32][29] = 524;\n        KRFreq[35][0] = 523;\n        KRFreq[24][43] = 522;\n        KRFreq[36][44] = 521;\n        KRFreq[20][30] = 520;\n        KRFreq[39][86] = 519;\n        KRFreq[22][14] = 518;\n        KRFreq[29][39] = 517;\n        KRFreq[28][38] = 516;\n        KRFreq[23][79] = 515;\n        KRFreq[24][56] = 514;\n        KRFreq[29][63] = 513;\n        KRFreq[31][45] = 512;\n        KRFreq[23][26] = 511;\n        KRFreq[15][87] = 510;\n        KRFreq[30][74] = 509;\n        KRFreq[24][69] = 508;\n        KRFreq[20][4] = 507;\n        KRFreq[27][50] = 506;\n        KRFreq[30][75] = 505;\n        KRFreq[24][13] = 504;\n        KRFreq[30][8] = 503;\n        KRFreq[31][6] = 502;\n        KRFreq[25][80] = 501;\n        KRFreq[36][8] = 500;\n        KRFreq[15][18] = 499;\n        KRFreq[39][23] = 498;\n        KRFreq[16][24] = 497;\n        KRFreq[31][89] = 496;\n        KRFreq[15][71] = 495;\n        KRFreq[15][57] = 494;\n        KRFreq[30][11] = 493;\n        KRFreq[15][36] = 492;\n        KRFreq[16][60] = 491;\n        KRFreq[24][45] = 490;\n        KRFreq[37][35] = 489;\n        KRFreq[24][87] = 488;\n        KRFreq[20][45] = 487;\n        KRFreq[31][90] = 486;\n        KRFreq[32][21] = 485;\n        KRFreq[19][70] = 484;\n        KRFreq[24][15] = 483;\n        KRFreq[26][92] = 482;\n        KRFreq[37][13] = 481;\n        KRFreq[39][2] = 480;\n        KRFreq[23][70] = 479;\n        KRFreq[27][25] = 478;\n        KRFreq[15][69] = 477;\n        KRFreq[19][61] = 476;\n        KRFreq[31][58] = 475;\n        KRFreq[24][57] = 474;\n        KRFreq[36][74] = 473;\n        KRFreq[21][6] = 472;\n        KRFreq[30][44] = 471;\n        KRFreq[15][91] = 470;\n        KRFreq[27][16] = 469;\n        KRFreq[29][42] = 468;\n        KRFreq[33][86] = 467;\n        KRFreq[29][41] = 466;\n        KRFreq[20][68] = 465;\n        KRFreq[25][47] = 464;\n        KRFreq[22][0] = 463;\n        KRFreq[18][14] = 462;\n        KRFreq[31][28] = 461;\n        KRFreq[15][2] = 460;\n        KRFreq[23][76] = 459;\n        KRFreq[38][32] = 458;\n        KRFreq[29][82] = 457;\n        KRFreq[21][86] = 456;\n        KRFreq[24][62] = 455;\n        KRFreq[31][64] = 454;\n        KRFreq[38][26] = 453;\n        KRFreq[32][86] = 452;\n        KRFreq[22][32] = 451;\n        KRFreq[19][59] = 450;\n        KRFreq[34][18] = 449;\n        KRFreq[18][54] = 448;\n        KRFreq[38][63] = 447;\n        KRFreq[36][23] = 446;\n        KRFreq[35][35] = 445;\n        KRFreq[32][62] = 444;\n        KRFreq[28][35] = 443;\n        KRFreq[27][13] = 442;\n        KRFreq[31][59] = 441;\n        KRFreq[29][29] = 440;\n        KRFreq[15][64] = 439;\n        KRFreq[26][84] = 438;\n        KRFreq[21][90] = 437;\n        KRFreq[20][24] = 436;\n        KRFreq[16][18] = 435;\n        KRFreq[22][23] = 434;\n        KRFreq[31][14] = 433;\n        KRFreq[15][1] = 432;\n        KRFreq[18][63] = 431;\n        KRFreq[19][10] = 430;\n        KRFreq[25][49] = 429;\n        KRFreq[36][57] = 428;\n        KRFreq[20][22] = 427;\n        KRFreq[15][15] = 426;\n        KRFreq[31][51] = 425;\n        KRFreq[24][60] = 424;\n        KRFreq[31][70] = 423;\n        KRFreq[15][7] = 422;\n        KRFreq[28][40] = 421;\n        KRFreq[18][41] = 420;\n        KRFreq[15][38] = 419;\n        KRFreq[32][0] = 418;\n        KRFreq[19][51] = 417;\n        KRFreq[34][62] = 416;\n        KRFreq[16][27] = 415;\n        KRFreq[20][70] = 414;\n        KRFreq[22][33] = 413;\n        KRFreq[26][73] = 412;\n        KRFreq[20][79] = 411;\n        KRFreq[23][6] = 410;\n        KRFreq[24][85] = 409;\n        KRFreq[38][51] = 408;\n        KRFreq[29][88] = 407;\n        KRFreq[38][55] = 406;\n        KRFreq[32][32] = 405;\n        KRFreq[27][18] = 404;\n        KRFreq[23][87] = 403;\n        KRFreq[35][6] = 402;\n        KRFreq[34][27] = 401;\n        KRFreq[39][35] = 400;\n        KRFreq[30][88] = 399;\n        KRFreq[32][92] = 398;\n        KRFreq[32][49] = 397;\n        KRFreq[24][61] = 396;\n        KRFreq[18][74] = 395;\n        KRFreq[23][77] = 394;\n        KRFreq[23][50] = 393;\n        KRFreq[23][32] = 392;\n        KRFreq[23][36] = 391;\n        KRFreq[38][38] = 390;\n        KRFreq[29][86] = 389;\n        KRFreq[36][15] = 388;\n        KRFreq[31][50] = 387;\n        KRFreq[15][86] = 386;\n        KRFreq[39][13] = 385;\n        KRFreq[34][26] = 384;\n        KRFreq[19][34] = 383;\n        KRFreq[16][3] = 382;\n        KRFreq[26][93] = 381;\n        KRFreq[19][67] = 380;\n        KRFreq[24][72] = 379;\n        KRFreq[29][17] = 378;\n        KRFreq[23][24] = 377;\n        KRFreq[25][19] = 376;\n        KRFreq[18][65] = 375;\n        KRFreq[30][78] = 374;\n        KRFreq[27][52] = 373;\n        KRFreq[22][18] = 372;\n        KRFreq[16][38] = 371;\n        KRFreq[21][26] = 370;\n        KRFreq[34][20] = 369;\n        KRFreq[15][42] = 368;\n        KRFreq[16][71] = 367;\n        KRFreq[17][17] = 366;\n        KRFreq[24][71] = 365;\n        KRFreq[18][84] = 364;\n        KRFreq[15][40] = 363;\n        KRFreq[31][62] = 362;\n        KRFreq[15][8] = 361;\n        KRFreq[16][69] = 360;\n        KRFreq[29][79] = 359;\n        KRFreq[38][91] = 358;\n        KRFreq[31][92] = 357;\n        KRFreq[20][77] = 356;\n        KRFreq[3][16] = 355;\n        KRFreq[27][87] = 354;\n        KRFreq[16][25] = 353;\n        KRFreq[36][33] = 352;\n        KRFreq[37][76] = 351;\n        KRFreq[30][12] = 350;\n        KRFreq[26][75] = 349;\n        KRFreq[25][14] = 348;\n        KRFreq[32][26] = 347;\n        KRFreq[23][22] = 346;\n        KRFreq[20][90] = 345;\n        KRFreq[19][8] = 344;\n        KRFreq[38][41] = 343;\n        KRFreq[34][2] = 342;\n        KRFreq[39][4] = 341;\n        KRFreq[27][89] = 340;\n        KRFreq[28][41] = 339;\n        KRFreq[28][44] = 338;\n        KRFreq[24][92] = 337;\n        KRFreq[34][65] = 336;\n        KRFreq[39][14] = 335;\n        KRFreq[21][38] = 334;\n        KRFreq[19][31] = 333;\n        KRFreq[37][39] = 332;\n        KRFreq[33][41] = 331;\n        KRFreq[38][4] = 330;\n        KRFreq[23][80] = 329;\n        KRFreq[25][24] = 328;\n        KRFreq[37][17] = 327;\n        KRFreq[22][16] = 326;\n        KRFreq[22][46] = 325;\n        KRFreq[33][91] = 324;\n        KRFreq[24][89] = 323;\n        KRFreq[30][52] = 322;\n        KRFreq[29][38] = 321;\n        KRFreq[38][85] = 320;\n        KRFreq[15][12] = 319;\n        KRFreq[27][58] = 318;\n        KRFreq[29][52] = 317;\n        KRFreq[37][38] = 316;\n        KRFreq[34][41] = 315;\n        KRFreq[31][65] = 314;\n        KRFreq[29][53] = 313;\n        KRFreq[22][47] = 312;\n        KRFreq[22][19] = 311;\n        KRFreq[26][0] = 310;\n        KRFreq[37][86] = 309;\n        KRFreq[35][4] = 308;\n        KRFreq[36][54] = 307;\n        KRFreq[20][76] = 306;\n        KRFreq[30][9] = 305;\n        KRFreq[30][33] = 304;\n        KRFreq[23][17] = 303;\n        KRFreq[23][33] = 302;\n        KRFreq[38][52] = 301;\n        KRFreq[15][19] = 300;\n        KRFreq[28][45] = 299;\n        KRFreq[29][78] = 298;\n        KRFreq[23][15] = 297;\n        KRFreq[33][5] = 296;\n        KRFreq[17][40] = 295;\n        KRFreq[30][83] = 294;\n        KRFreq[18][1] = 293;\n        KRFreq[30][81] = 292;\n        KRFreq[19][40] = 291;\n        KRFreq[24][47] = 290;\n        KRFreq[17][56] = 289;\n        KRFreq[39][80] = 288;\n        KRFreq[30][46] = 287;\n        KRFreq[16][61] = 286;\n        KRFreq[26][78] = 285;\n        KRFreq[26][57] = 284;\n        KRFreq[20][46] = 283;\n        KRFreq[25][15] = 282;\n        KRFreq[25][91] = 281;\n        KRFreq[21][83] = 280;\n        KRFreq[30][77] = 279;\n        KRFreq[35][30] = 278;\n        KRFreq[30][34] = 277;\n        KRFreq[20][69] = 276;\n        KRFreq[35][10] = 275;\n        KRFreq[29][70] = 274;\n        KRFreq[22][50] = 273;\n        KRFreq[18][0] = 272;\n        KRFreq[22][64] = 271;\n        KRFreq[38][65] = 270;\n        KRFreq[22][70] = 269;\n        KRFreq[24][58] = 268;\n        KRFreq[19][66] = 267;\n        KRFreq[30][59] = 266;\n        KRFreq[37][14] = 265;\n        KRFreq[16][56] = 264;\n        KRFreq[29][85] = 263;\n        KRFreq[31][15] = 262;\n        KRFreq[36][84] = 261;\n        KRFreq[39][15] = 260;\n        KRFreq[39][90] = 259;\n        KRFreq[18][12] = 258;\n        KRFreq[21][93] = 257;\n        KRFreq[24][66] = 256;\n        KRFreq[27][90] = 255;\n        KRFreq[25][90] = 254;\n        KRFreq[22][24] = 253;\n        KRFreq[36][67] = 252;\n        KRFreq[33][90] = 251;\n        KRFreq[15][60] = 250;\n        KRFreq[23][85] = 249;\n        KRFreq[34][1] = 248;\n        KRFreq[39][37] = 247;\n        KRFreq[21][18] = 246;\n        KRFreq[34][4] = 245;\n        KRFreq[28][33] = 244;\n        KRFreq[15][13] = 243;\n        KRFreq[32][22] = 242;\n        KRFreq[30][76] = 241;\n        KRFreq[20][21] = 240;\n        KRFreq[38][66] = 239;\n        KRFreq[32][55] = 238;\n        KRFreq[32][89] = 237;\n        KRFreq[25][26] = 236;\n        KRFreq[16][80] = 235;\n        KRFreq[15][43] = 234;\n        KRFreq[38][54] = 233;\n        KRFreq[39][68] = 232;\n        KRFreq[22][88] = 231;\n        KRFreq[21][84] = 230;\n        KRFreq[21][17] = 229;\n        KRFreq[20][28] = 228;\n        KRFreq[32][1] = 227;\n        KRFreq[33][87] = 226;\n        KRFreq[38][71] = 225;\n        KRFreq[37][47] = 224;\n        KRFreq[18][77] = 223;\n        KRFreq[37][58] = 222;\n        KRFreq[34][74] = 221;\n        KRFreq[32][54] = 220;\n        KRFreq[27][33] = 219;\n        KRFreq[32][93] = 218;\n        KRFreq[23][51] = 217;\n        KRFreq[20][57] = 216;\n        KRFreq[22][37] = 215;\n        KRFreq[39][10] = 214;\n        KRFreq[39][17] = 213;\n        KRFreq[33][4] = 212;\n        KRFreq[32][84] = 211;\n        KRFreq[34][3] = 210;\n        KRFreq[28][27] = 209;\n        KRFreq[15][79] = 208;\n        KRFreq[34][21] = 207;\n        KRFreq[34][69] = 206;\n        KRFreq[21][62] = 205;\n        KRFreq[36][24] = 204;\n        KRFreq[16][89] = 203;\n        KRFreq[18][48] = 202;\n        KRFreq[38][15] = 201;\n        KRFreq[36][58] = 200;\n        KRFreq[21][56] = 199;\n        KRFreq[34][48] = 198;\n        KRFreq[21][15] = 197;\n        KRFreq[39][3] = 196;\n        KRFreq[16][44] = 195;\n        KRFreq[18][79] = 194;\n        KRFreq[25][13] = 193;\n        KRFreq[29][47] = 192;\n        KRFreq[38][88] = 191;\n        KRFreq[20][71] = 190;\n        KRFreq[16][58] = 189;\n        KRFreq[35][57] = 188;\n        KRFreq[29][30] = 187;\n        KRFreq[29][23] = 186;\n        KRFreq[34][93] = 185;\n        KRFreq[30][85] = 184;\n        KRFreq[15][80] = 183;\n        KRFreq[32][78] = 182;\n        KRFreq[37][82] = 181;\n        KRFreq[22][40] = 180;\n        KRFreq[21][69] = 179;\n        KRFreq[26][85] = 178;\n        KRFreq[31][31] = 177;\n        KRFreq[28][64] = 176;\n        KRFreq[38][13] = 175;\n        KRFreq[25][2] = 174;\n        KRFreq[22][34] = 173;\n        KRFreq[28][28] = 172;\n        KRFreq[24][91] = 171;\n        KRFreq[33][74] = 170;\n        KRFreq[29][40] = 169;\n        KRFreq[15][77] = 168;\n        KRFreq[32][80] = 167;\n        KRFreq[30][41] = 166;\n        KRFreq[23][30] = 165;\n        KRFreq[24][63] = 164;\n        KRFreq[30][53] = 163;\n        KRFreq[39][70] = 162;\n        KRFreq[23][61] = 161;\n        KRFreq[37][27] = 160;\n        KRFreq[16][55] = 159;\n        KRFreq[22][74] = 158;\n        KRFreq[26][50] = 157;\n        KRFreq[16][10] = 156;\n        KRFreq[34][63] = 155;\n        KRFreq[35][14] = 154;\n        KRFreq[17][7] = 153;\n        KRFreq[15][59] = 152;\n        KRFreq[27][23] = 151;\n        KRFreq[18][70] = 150;\n        KRFreq[32][56] = 149;\n        KRFreq[37][87] = 148;\n        KRFreq[17][61] = 147;\n        KRFreq[18][83] = 146;\n        KRFreq[23][86] = 145;\n        KRFreq[17][31] = 144;\n        KRFreq[23][83] = 143;\n        KRFreq[35][2] = 142;\n        KRFreq[18][64] = 141;\n        KRFreq[27][43] = 140;\n        KRFreq[32][42] = 139;\n        KRFreq[25][76] = 138;\n        KRFreq[19][85] = 137;\n        KRFreq[37][81] = 136;\n        KRFreq[38][83] = 135;\n        KRFreq[35][7] = 134;\n        KRFreq[16][51] = 133;\n        KRFreq[27][22] = 132;\n        KRFreq[16][76] = 131;\n        KRFreq[22][4] = 130;\n        KRFreq[38][84] = 129;\n        KRFreq[17][83] = 128;\n        KRFreq[24][46] = 127;\n        KRFreq[33][15] = 126;\n        KRFreq[20][48] = 125;\n        KRFreq[17][30] = 124;\n        KRFreq[30][93] = 123;\n        KRFreq[28][11] = 122;\n        KRFreq[28][30] = 121;\n        KRFreq[15][62] = 120;\n        KRFreq[17][87] = 119;\n        KRFreq[32][81] = 118;\n        KRFreq[23][37] = 117;\n        KRFreq[30][22] = 116;\n        KRFreq[32][66] = 115;\n        KRFreq[33][78] = 114;\n        KRFreq[21][4] = 113;\n        KRFreq[31][17] = 112;\n        KRFreq[39][61] = 111;\n        KRFreq[18][76] = 110;\n        KRFreq[15][85] = 109;\n        KRFreq[31][47] = 108;\n        KRFreq[19][57] = 107;\n        KRFreq[23][55] = 106;\n        KRFreq[27][29] = 105;\n        KRFreq[29][46] = 104;\n        KRFreq[33][0] = 103;\n        KRFreq[16][83] = 102;\n        KRFreq[39][78] = 101;\n        KRFreq[32][77] = 100;\n        KRFreq[36][25] = 99;\n        KRFreq[34][19] = 98;\n        KRFreq[38][49] = 97;\n        KRFreq[19][25] = 96;\n        KRFreq[23][53] = 95;\n        KRFreq[28][43] = 94;\n        KRFreq[31][44] = 93;\n        KRFreq[36][34] = 92;\n        KRFreq[16][34] = 91;\n        KRFreq[35][1] = 90;\n        KRFreq[19][87] = 89;\n        KRFreq[18][53] = 88;\n        KRFreq[29][54] = 87;\n        KRFreq[22][41] = 86;\n        KRFreq[38][18] = 85;\n        KRFreq[22][2] = 84;\n        KRFreq[20][3] = 83;\n        KRFreq[39][69] = 82;\n        KRFreq[30][29] = 81;\n        KRFreq[28][19] = 80;\n        KRFreq[29][90] = 79;\n        KRFreq[17][86] = 78;\n        KRFreq[15][9] = 77;\n        KRFreq[39][73] = 76;\n        KRFreq[15][37] = 75;\n        KRFreq[35][40] = 74;\n        KRFreq[33][77] = 73;\n        KRFreq[27][86] = 72;\n        KRFreq[36][79] = 71;\n        KRFreq[23][18] = 70;\n        KRFreq[34][87] = 69;\n        KRFreq[39][24] = 68;\n        KRFreq[26][8] = 67;\n        KRFreq[33][48] = 66;\n        KRFreq[39][30] = 65;\n        KRFreq[33][28] = 64;\n        KRFreq[16][67] = 63;\n        KRFreq[31][78] = 62;\n        KRFreq[32][23] = 61;\n        KRFreq[24][55] = 60;\n        KRFreq[30][68] = 59;\n        KRFreq[18][60] = 58;\n        KRFreq[15][17] = 57;\n        KRFreq[23][34] = 56;\n        KRFreq[20][49] = 55;\n        KRFreq[15][78] = 54;\n        KRFreq[24][14] = 53;\n        KRFreq[19][41] = 52;\n        KRFreq[31][55] = 51;\n        KRFreq[21][39] = 50;\n        KRFreq[35][9] = 49;\n        KRFreq[30][15] = 48;\n        KRFreq[20][52] = 47;\n        KRFreq[35][71] = 46;\n        KRFreq[20][7] = 45;\n        KRFreq[29][72] = 44;\n        KRFreq[37][77] = 43;\n        KRFreq[22][35] = 42;\n        KRFreq[20][61] = 41;\n        KRFreq[31][60] = 40;\n        KRFreq[20][93] = 39;\n        KRFreq[27][92] = 38;\n        KRFreq[28][16] = 37;\n        KRFreq[36][26] = 36;\n        KRFreq[18][89] = 35;\n        KRFreq[21][63] = 34;\n        KRFreq[22][52] = 33;\n        KRFreq[24][65] = 32;\n        KRFreq[31][8] = 31;\n        KRFreq[31][49] = 30;\n        KRFreq[33][30] = 29;\n        KRFreq[37][15] = 28;\n        KRFreq[18][18] = 27;\n        KRFreq[25][50] = 26;\n        KRFreq[29][20] = 25;\n        KRFreq[35][48] = 24;\n        KRFreq[38][75] = 23;\n        KRFreq[26][83] = 22;\n        KRFreq[21][87] = 21;\n        KRFreq[27][71] = 20;\n        KRFreq[32][91] = 19;\n        KRFreq[25][73] = 18;\n        KRFreq[16][84] = 17;\n        KRFreq[25][31] = 16;\n        KRFreq[17][90] = 15;\n        KRFreq[18][40] = 14;\n        KRFreq[17][77] = 13;\n        KRFreq[17][35] = 12;\n        KRFreq[23][52] = 11;\n        KRFreq[23][35] = 10;\n        KRFreq[16][5] = 9;\n        KRFreq[23][58] = 8;\n        KRFreq[19][60] = 7;\n        KRFreq[30][32] = 6;\n        KRFreq[38][34] = 5;\n        KRFreq[23][4] = 4;\n        KRFreq[23][1] = 3;\n        KRFreq[27][57] = 2;\n        KRFreq[39][38] = 1;\n        KRFreq[32][33] = 0;\n        JPFreq[3][74] = 600;\n        JPFreq[3][45] = 599;\n        JPFreq[3][3] = 598;\n        JPFreq[3][24] = 597;\n        JPFreq[3][30] = 596;\n        JPFreq[3][42] = 595;\n        JPFreq[3][46] = 594;\n        JPFreq[3][39] = 593;\n        JPFreq[3][11] = 592;\n        JPFreq[3][37] = 591;\n        JPFreq[3][38] = 590;\n        JPFreq[3][31] = 589;\n        JPFreq[3][41] = 588;\n        JPFreq[3][5] = 587;\n        JPFreq[3][10] = 586;\n        JPFreq[3][75] = 585;\n        JPFreq[3][65] = 584;\n        JPFreq[3][72] = 583;\n        JPFreq[37][91] = 582;\n        JPFreq[0][27] = 581;\n        JPFreq[3][18] = 580;\n        JPFreq[3][22] = 579;\n        JPFreq[3][61] = 578;\n        JPFreq[3][14] = 577;\n        JPFreq[24][80] = 576;\n        JPFreq[4][82] = 575;\n        JPFreq[17][80] = 574;\n        JPFreq[30][44] = 573;\n        JPFreq[3][73] = 572;\n        JPFreq[3][64] = 571;\n        JPFreq[38][14] = 570;\n        JPFreq[33][70] = 569;\n        JPFreq[3][1] = 568;\n        JPFreq[3][16] = 567;\n        JPFreq[3][35] = 566;\n        JPFreq[3][40] = 565;\n        JPFreq[4][74] = 564;\n        JPFreq[4][24] = 563;\n        JPFreq[42][59] = 562;\n        JPFreq[3][7] = 561;\n        JPFreq[3][71] = 560;\n        JPFreq[3][12] = 559;\n        JPFreq[15][75] = 558;\n        JPFreq[3][20] = 557;\n        JPFreq[4][39] = 556;\n        JPFreq[34][69] = 555;\n        JPFreq[3][28] = 554;\n        JPFreq[35][24] = 553;\n        JPFreq[3][82] = 552;\n        JPFreq[28][47] = 551;\n        JPFreq[3][67] = 550;\n        JPFreq[37][16] = 549;\n        JPFreq[26][93] = 548;\n        JPFreq[4][1] = 547;\n        JPFreq[26][85] = 546;\n        JPFreq[31][14] = 545;\n        JPFreq[4][3] = 544;\n        JPFreq[4][72] = 543;\n        JPFreq[24][51] = 542;\n        JPFreq[27][51] = 541;\n        JPFreq[27][49] = 540;\n        JPFreq[22][77] = 539;\n        JPFreq[27][10] = 538;\n        JPFreq[29][68] = 537;\n        JPFreq[20][35] = 536;\n        JPFreq[41][11] = 535;\n        JPFreq[24][70] = 534;\n        JPFreq[36][61] = 533;\n        JPFreq[31][23] = 532;\n        JPFreq[43][16] = 531;\n        JPFreq[23][68] = 530;\n        JPFreq[32][15] = 529;\n        JPFreq[3][32] = 528;\n        JPFreq[19][53] = 527;\n        JPFreq[40][83] = 526;\n        JPFreq[4][14] = 525;\n        JPFreq[36][9] = 524;\n        JPFreq[4][73] = 523;\n        JPFreq[23][10] = 522;\n        JPFreq[3][63] = 521;\n        JPFreq[39][14] = 520;\n        JPFreq[3][78] = 519;\n        JPFreq[33][47] = 518;\n        JPFreq[21][39] = 517;\n        JPFreq[34][46] = 516;\n        JPFreq[36][75] = 515;\n        JPFreq[41][92] = 514;\n        JPFreq[37][93] = 513;\n        JPFreq[4][34] = 512;\n        JPFreq[15][86] = 511;\n        JPFreq[46][1] = 510;\n        JPFreq[37][65] = 509;\n        JPFreq[3][62] = 508;\n        JPFreq[32][73] = 507;\n        JPFreq[21][65] = 506;\n        JPFreq[29][75] = 505;\n        JPFreq[26][51] = 504;\n        JPFreq[3][34] = 503;\n        JPFreq[4][10] = 502;\n        JPFreq[30][22] = 501;\n        JPFreq[35][73] = 500;\n        JPFreq[17][82] = 499;\n        JPFreq[45][8] = 498;\n        JPFreq[27][73] = 497;\n        JPFreq[18][55] = 496;\n        JPFreq[25][2] = 495;\n        JPFreq[3][26] = 494;\n        JPFreq[45][46] = 493;\n        JPFreq[4][22] = 492;\n        JPFreq[4][40] = 491;\n        JPFreq[18][10] = 490;\n        JPFreq[32][9] = 489;\n        JPFreq[26][49] = 488;\n        JPFreq[3][47] = 487;\n        JPFreq[24][65] = 486;\n        JPFreq[4][76] = 485;\n        JPFreq[43][67] = 484;\n        JPFreq[3][9] = 483;\n        JPFreq[41][37] = 482;\n        JPFreq[33][68] = 481;\n        JPFreq[43][31] = 480;\n        JPFreq[19][55] = 479;\n        JPFreq[4][30] = 478;\n        JPFreq[27][33] = 477;\n        JPFreq[16][62] = 476;\n        JPFreq[36][35] = 475;\n        JPFreq[37][15] = 474;\n        JPFreq[27][70] = 473;\n        JPFreq[22][71] = 472;\n        JPFreq[33][45] = 471;\n        JPFreq[31][78] = 470;\n        JPFreq[43][59] = 469;\n        JPFreq[32][19] = 468;\n        JPFreq[17][28] = 467;\n        JPFreq[40][28] = 466;\n        JPFreq[20][93] = 465;\n        JPFreq[18][15] = 464;\n        JPFreq[4][23] = 463;\n        JPFreq[3][23] = 462;\n        JPFreq[26][64] = 461;\n        JPFreq[44][92] = 460;\n        JPFreq[17][27] = 459;\n        JPFreq[3][56] = 458;\n        JPFreq[25][38] = 457;\n        JPFreq[23][31] = 456;\n        JPFreq[35][43] = 455;\n        JPFreq[4][54] = 454;\n        JPFreq[35][19] = 453;\n        JPFreq[22][47] = 452;\n        JPFreq[42][0] = 451;\n        JPFreq[23][28] = 450;\n        JPFreq[46][33] = 449;\n        JPFreq[36][85] = 448;\n        JPFreq[31][12] = 447;\n        JPFreq[3][76] = 446;\n        JPFreq[4][75] = 445;\n        JPFreq[36][56] = 444;\n        JPFreq[4][64] = 443;\n        JPFreq[25][77] = 442;\n        JPFreq[15][52] = 441;\n        JPFreq[33][73] = 440;\n        JPFreq[3][55] = 439;\n        JPFreq[43][82] = 438;\n        JPFreq[27][82] = 437;\n        JPFreq[20][3] = 436;\n        JPFreq[40][51] = 435;\n        JPFreq[3][17] = 434;\n        JPFreq[27][71] = 433;\n        JPFreq[4][52] = 432;\n        JPFreq[44][48] = 431;\n        JPFreq[27][2] = 430;\n        JPFreq[17][39] = 429;\n        JPFreq[31][8] = 428;\n        JPFreq[44][54] = 427;\n        JPFreq[43][18] = 426;\n        JPFreq[43][77] = 425;\n        JPFreq[4][61] = 424;\n        JPFreq[19][91] = 423;\n        JPFreq[31][13] = 422;\n        JPFreq[44][71] = 421;\n        JPFreq[20][0] = 420;\n        JPFreq[23][87] = 419;\n        JPFreq[21][14] = 418;\n        JPFreq[29][13] = 417;\n        JPFreq[3][58] = 416;\n        JPFreq[26][18] = 415;\n        JPFreq[4][47] = 414;\n        JPFreq[4][18] = 413;\n        JPFreq[3][53] = 412;\n        JPFreq[26][92] = 411;\n        JPFreq[21][7] = 410;\n        JPFreq[4][37] = 409;\n        JPFreq[4][63] = 408;\n        JPFreq[36][51] = 407;\n        JPFreq[4][32] = 406;\n        JPFreq[28][73] = 405;\n        JPFreq[4][50] = 404;\n        JPFreq[41][60] = 403;\n        JPFreq[23][1] = 402;\n        JPFreq[36][92] = 401;\n        JPFreq[15][41] = 400;\n        JPFreq[21][71] = 399;\n        JPFreq[41][30] = 398;\n        JPFreq[32][76] = 397;\n        JPFreq[17][34] = 396;\n        JPFreq[26][15] = 395;\n        JPFreq[26][25] = 394;\n        JPFreq[31][77] = 393;\n        JPFreq[31][3] = 392;\n        JPFreq[46][34] = 391;\n        JPFreq[27][84] = 390;\n        JPFreq[23][8] = 389;\n        JPFreq[16][0] = 388;\n        JPFreq[28][80] = 387;\n        JPFreq[26][54] = 386;\n        JPFreq[33][18] = 385;\n        JPFreq[31][20] = 384;\n        JPFreq[31][62] = 383;\n        JPFreq[30][41] = 382;\n        JPFreq[33][30] = 381;\n        JPFreq[45][45] = 380;\n        JPFreq[37][82] = 379;\n        JPFreq[15][33] = 378;\n        JPFreq[20][12] = 377;\n        JPFreq[18][5] = 376;\n        JPFreq[28][86] = 375;\n        JPFreq[30][19] = 374;\n        JPFreq[42][43] = 373;\n        JPFreq[36][31] = 372;\n        JPFreq[17][93] = 371;\n        JPFreq[4][15] = 370;\n        JPFreq[21][20] = 369;\n        JPFreq[23][21] = 368;\n        JPFreq[28][72] = 367;\n        JPFreq[4][20] = 366;\n        JPFreq[26][55] = 365;\n        JPFreq[21][5] = 364;\n        JPFreq[19][16] = 363;\n        JPFreq[23][64] = 362;\n        JPFreq[40][59] = 361;\n        JPFreq[37][26] = 360;\n        JPFreq[26][56] = 359;\n        JPFreq[4][12] = 358;\n        JPFreq[33][71] = 357;\n        JPFreq[32][39] = 356;\n        JPFreq[38][40] = 355;\n        JPFreq[22][74] = 354;\n        JPFreq[3][25] = 353;\n        JPFreq[15][48] = 352;\n        JPFreq[41][82] = 351;\n        JPFreq[41][9] = 350;\n        JPFreq[25][48] = 349;\n        JPFreq[31][71] = 348;\n        JPFreq[43][29] = 347;\n        JPFreq[26][80] = 346;\n        JPFreq[4][5] = 345;\n        JPFreq[18][71] = 344;\n        JPFreq[29][0] = 343;\n        JPFreq[43][43] = 342;\n        JPFreq[23][81] = 341;\n        JPFreq[4][42] = 340;\n        JPFreq[44][28] = 339;\n        JPFreq[23][93] = 338;\n        JPFreq[17][81] = 337;\n        JPFreq[25][25] = 336;\n        JPFreq[41][23] = 335;\n        JPFreq[34][35] = 334;\n        JPFreq[4][53] = 333;\n        JPFreq[28][36] = 332;\n        JPFreq[4][41] = 331;\n        JPFreq[25][60] = 330;\n        JPFreq[23][20] = 329;\n        JPFreq[3][43] = 328;\n        JPFreq[24][79] = 327;\n        JPFreq[29][41] = 326;\n        JPFreq[30][83] = 325;\n        JPFreq[3][50] = 324;\n        JPFreq[22][18] = 323;\n        JPFreq[18][3] = 322;\n        JPFreq[39][30] = 321;\n        JPFreq[4][28] = 320;\n        JPFreq[21][64] = 319;\n        JPFreq[4][68] = 318;\n        JPFreq[17][71] = 317;\n        JPFreq[27][0] = 316;\n        JPFreq[39][28] = 315;\n        JPFreq[30][13] = 314;\n        JPFreq[36][70] = 313;\n        JPFreq[20][82] = 312;\n        JPFreq[33][38] = 311;\n        JPFreq[44][87] = 310;\n        JPFreq[34][45] = 309;\n        JPFreq[4][26] = 308;\n        JPFreq[24][44] = 307;\n        JPFreq[38][67] = 306;\n        JPFreq[38][6] = 305;\n        JPFreq[30][68] = 304;\n        JPFreq[15][89] = 303;\n        JPFreq[24][93] = 302;\n        JPFreq[40][41] = 301;\n        JPFreq[38][3] = 300;\n        JPFreq[28][23] = 299;\n        JPFreq[26][17] = 298;\n        JPFreq[4][38] = 297;\n        JPFreq[22][78] = 296;\n        JPFreq[15][37] = 295;\n        JPFreq[25][85] = 294;\n        JPFreq[4][9] = 293;\n        JPFreq[4][7] = 292;\n        JPFreq[27][53] = 291;\n        JPFreq[39][29] = 290;\n        JPFreq[41][43] = 289;\n        JPFreq[25][62] = 288;\n        JPFreq[4][48] = 287;\n        JPFreq[28][28] = 286;\n        JPFreq[21][40] = 285;\n        JPFreq[36][73] = 284;\n        JPFreq[26][39] = 283;\n        JPFreq[22][54] = 282;\n        JPFreq[33][5] = 281;\n        JPFreq[19][21] = 280;\n        JPFreq[46][31] = 279;\n        JPFreq[20][64] = 278;\n        JPFreq[26][63] = 277;\n        JPFreq[22][23] = 276;\n        JPFreq[25][81] = 275;\n        JPFreq[4][62] = 274;\n        JPFreq[37][31] = 273;\n        JPFreq[40][52] = 272;\n        JPFreq[29][79] = 271;\n        JPFreq[41][48] = 270;\n        JPFreq[31][57] = 269;\n        JPFreq[32][92] = 268;\n        JPFreq[36][36] = 267;\n        JPFreq[27][7] = 266;\n        JPFreq[35][29] = 265;\n        JPFreq[37][34] = 264;\n        JPFreq[34][42] = 263;\n        JPFreq[27][15] = 262;\n        JPFreq[33][27] = 261;\n        JPFreq[31][38] = 260;\n        JPFreq[19][79] = 259;\n        JPFreq[4][31] = 258;\n        JPFreq[4][66] = 257;\n        JPFreq[17][32] = 256;\n        JPFreq[26][67] = 255;\n        JPFreq[16][30] = 254;\n        JPFreq[26][46] = 253;\n        JPFreq[24][26] = 252;\n        JPFreq[35][10] = 251;\n        JPFreq[18][37] = 250;\n        JPFreq[3][19] = 249;\n        JPFreq[33][69] = 248;\n        JPFreq[31][9] = 247;\n        JPFreq[45][29] = 246;\n        JPFreq[3][15] = 245;\n        JPFreq[18][54] = 244;\n        JPFreq[3][44] = 243;\n        JPFreq[31][29] = 242;\n        JPFreq[18][45] = 241;\n        JPFreq[38][28] = 240;\n        JPFreq[24][12] = 239;\n        JPFreq[35][82] = 238;\n        JPFreq[17][43] = 237;\n        JPFreq[28][9] = 236;\n        JPFreq[23][25] = 235;\n        JPFreq[44][37] = 234;\n        JPFreq[23][75] = 233;\n        JPFreq[23][92] = 232;\n        JPFreq[0][24] = 231;\n        JPFreq[19][74] = 230;\n        JPFreq[45][32] = 229;\n        JPFreq[16][72] = 228;\n        JPFreq[16][93] = 227;\n        JPFreq[45][13] = 226;\n        JPFreq[24][8] = 225;\n        JPFreq[25][47] = 224;\n        JPFreq[28][26] = 223;\n        JPFreq[43][81] = 222;\n        JPFreq[32][71] = 221;\n        JPFreq[18][41] = 220;\n        JPFreq[26][62] = 219;\n        JPFreq[41][24] = 218;\n        JPFreq[40][11] = 217;\n        JPFreq[43][57] = 216;\n        JPFreq[34][53] = 215;\n        JPFreq[20][32] = 214;\n        JPFreq[34][43] = 213;\n        JPFreq[41][91] = 212;\n        JPFreq[29][57] = 211;\n        JPFreq[15][43] = 210;\n        JPFreq[22][89] = 209;\n        JPFreq[33][83] = 208;\n        JPFreq[43][20] = 207;\n        JPFreq[25][58] = 206;\n        JPFreq[30][30] = 205;\n        JPFreq[4][56] = 204;\n        JPFreq[17][64] = 203;\n        JPFreq[23][0] = 202;\n        JPFreq[44][12] = 201;\n        JPFreq[25][37] = 200;\n        JPFreq[35][13] = 199;\n        JPFreq[20][30] = 198;\n        JPFreq[21][84] = 197;\n        JPFreq[29][14] = 196;\n        JPFreq[30][5] = 195;\n        JPFreq[37][2] = 194;\n        JPFreq[4][78] = 193;\n        JPFreq[29][78] = 192;\n        JPFreq[29][84] = 191;\n        JPFreq[32][86] = 190;\n        JPFreq[20][68] = 189;\n        JPFreq[30][39] = 188;\n        JPFreq[15][69] = 187;\n        JPFreq[4][60] = 186;\n        JPFreq[20][61] = 185;\n        JPFreq[41][67] = 184;\n        JPFreq[16][35] = 183;\n        JPFreq[36][57] = 182;\n        JPFreq[39][80] = 181;\n        JPFreq[4][59] = 180;\n        JPFreq[4][44] = 179;\n        JPFreq[40][54] = 178;\n        JPFreq[30][8] = 177;\n        JPFreq[44][30] = 176;\n        JPFreq[31][93] = 175;\n        JPFreq[31][47] = 174;\n        JPFreq[16][70] = 173;\n        JPFreq[21][0] = 172;\n        JPFreq[17][35] = 171;\n        JPFreq[21][67] = 170;\n        JPFreq[44][18] = 169;\n        JPFreq[36][29] = 168;\n        JPFreq[18][67] = 167;\n        JPFreq[24][28] = 166;\n        JPFreq[36][24] = 165;\n        JPFreq[23][5] = 164;\n        JPFreq[31][65] = 163;\n        JPFreq[26][59] = 162;\n        JPFreq[28][2] = 161;\n        JPFreq[39][69] = 160;\n        JPFreq[42][40] = 159;\n        JPFreq[37][80] = 158;\n        JPFreq[15][66] = 157;\n        JPFreq[34][38] = 156;\n        JPFreq[28][48] = 155;\n        JPFreq[37][77] = 154;\n        JPFreq[29][34] = 153;\n        JPFreq[33][12] = 152;\n        JPFreq[4][65] = 151;\n        JPFreq[30][31] = 150;\n        JPFreq[27][92] = 149;\n        JPFreq[4][2] = 148;\n        JPFreq[4][51] = 147;\n        JPFreq[23][77] = 146;\n        JPFreq[4][35] = 145;\n        JPFreq[3][13] = 144;\n        JPFreq[26][26] = 143;\n        JPFreq[44][4] = 142;\n        JPFreq[39][53] = 141;\n        JPFreq[20][11] = 140;\n        JPFreq[40][33] = 139;\n        JPFreq[45][7] = 138;\n        JPFreq[4][70] = 137;\n        JPFreq[3][49] = 136;\n        JPFreq[20][59] = 135;\n        JPFreq[21][12] = 134;\n        JPFreq[33][53] = 133;\n        JPFreq[20][14] = 132;\n        JPFreq[37][18] = 131;\n        JPFreq[18][17] = 130;\n        JPFreq[36][23] = 129;\n        JPFreq[18][57] = 128;\n        JPFreq[26][74] = 127;\n        JPFreq[35][2] = 126;\n        JPFreq[38][58] = 125;\n        JPFreq[34][68] = 124;\n        JPFreq[29][81] = 123;\n        JPFreq[20][69] = 122;\n        JPFreq[39][86] = 121;\n        JPFreq[4][16] = 120;\n        JPFreq[16][49] = 119;\n        JPFreq[15][72] = 118;\n        JPFreq[26][35] = 117;\n        JPFreq[32][14] = 116;\n        JPFreq[40][90] = 115;\n        JPFreq[33][79] = 114;\n        JPFreq[35][4] = 113;\n        JPFreq[23][33] = 112;\n        JPFreq[19][19] = 111;\n        JPFreq[31][41] = 110;\n        JPFreq[44][1] = 109;\n        JPFreq[22][56] = 108;\n        JPFreq[31][27] = 107;\n        JPFreq[32][18] = 106;\n        JPFreq[27][32] = 105;\n        JPFreq[37][39] = 104;\n        JPFreq[42][11] = 103;\n        JPFreq[29][71] = 102;\n        JPFreq[32][58] = 101;\n        JPFreq[46][10] = 100;\n        JPFreq[17][30] = 99;\n        JPFreq[38][15] = 98;\n        JPFreq[29][60] = 97;\n        JPFreq[4][11] = 96;\n        JPFreq[38][31] = 95;\n        JPFreq[40][79] = 94;\n        JPFreq[28][49] = 93;\n        JPFreq[28][84] = 92;\n        JPFreq[26][77] = 91;\n        JPFreq[22][32] = 90;\n        JPFreq[33][17] = 89;\n        JPFreq[23][18] = 88;\n        JPFreq[32][64] = 87;\n        JPFreq[4][6] = 86;\n        JPFreq[33][51] = 85;\n        JPFreq[44][77] = 84;\n        JPFreq[29][5] = 83;\n        JPFreq[46][25] = 82;\n        JPFreq[19][58] = 81;\n        JPFreq[4][46] = 80;\n        JPFreq[15][71] = 79;\n        JPFreq[18][58] = 78;\n        JPFreq[26][45] = 77;\n        JPFreq[45][66] = 76;\n        JPFreq[34][10] = 75;\n        JPFreq[19][37] = 74;\n        JPFreq[33][65] = 73;\n        JPFreq[44][52] = 72;\n        JPFreq[16][38] = 71;\n        JPFreq[36][46] = 70;\n        JPFreq[20][26] = 69;\n        JPFreq[30][37] = 68;\n        JPFreq[4][58] = 67;\n        JPFreq[43][2] = 66;\n        JPFreq[30][18] = 65;\n        JPFreq[19][35] = 64;\n        JPFreq[15][68] = 63;\n        JPFreq[3][36] = 62;\n        JPFreq[35][40] = 61;\n        JPFreq[36][32] = 60;\n        JPFreq[37][14] = 59;\n        JPFreq[17][11] = 58;\n        JPFreq[19][78] = 57;\n        JPFreq[37][11] = 56;\n        JPFreq[28][63] = 55;\n        JPFreq[29][61] = 54;\n        JPFreq[33][3] = 53;\n        JPFreq[41][52] = 52;\n        JPFreq[33][63] = 51;\n        JPFreq[22][41] = 50;\n        JPFreq[4][19] = 49;\n        JPFreq[32][41] = 48;\n        JPFreq[24][4] = 47;\n        JPFreq[31][28] = 46;\n        JPFreq[43][30] = 45;\n        JPFreq[17][3] = 44;\n        JPFreq[43][70] = 43;\n        JPFreq[34][19] = 42;\n        JPFreq[20][77] = 41;\n        JPFreq[18][83] = 40;\n        JPFreq[17][15] = 39;\n        JPFreq[23][61] = 38;\n        JPFreq[40][27] = 37;\n        JPFreq[16][48] = 36;\n        JPFreq[39][78] = 35;\n        JPFreq[41][53] = 34;\n        JPFreq[40][91] = 33;\n        JPFreq[40][72] = 32;\n        JPFreq[18][52] = 31;\n        JPFreq[35][66] = 30;\n        JPFreq[39][93] = 29;\n        JPFreq[19][48] = 28;\n        JPFreq[26][36] = 27;\n        JPFreq[27][25] = 26;\n        JPFreq[42][71] = 25;\n        JPFreq[42][85] = 24;\n        JPFreq[26][48] = 23;\n        JPFreq[28][15] = 22;\n        JPFreq[3][66] = 21;\n        JPFreq[25][24] = 20;\n        JPFreq[27][43] = 19;\n        JPFreq[27][78] = 18;\n        JPFreq[45][43] = 17;\n        JPFreq[27][72] = 16;\n        JPFreq[40][29] = 15;\n        JPFreq[41][0] = 14;\n        JPFreq[19][57] = 13;\n        JPFreq[15][59] = 12;\n        JPFreq[29][29] = 11;\n        JPFreq[4][25] = 10;\n        JPFreq[21][42] = 9;\n        JPFreq[23][35] = 8;\n        JPFreq[33][1] = 7;\n        JPFreq[4][57] = 6;\n        JPFreq[17][60] = 5;\n        JPFreq[25][19] = 4;\n        JPFreq[22][65] = 3;\n        JPFreq[42][29] = 2;\n        JPFreq[27][66] = 1;\n        JPFreq[26][89] = 0;\n    }\n}\n\nclass Encoding {\n    // Supported Encoding Types\n    public static int GB2312 = 0;\n\n    public static int GBK = 1;\n\n    public static int GB18030 = 2;\n\n    public static int HZ = 3;\n\n    public static int BIG5 = 4;\n\n    public static int CNS11643 = 5;\n\n    public static int UTF8 = 6;\n\n    public static int UTF8T = 7;\n\n    public static int UTF8S = 8;\n\n    public static int UNICODE = 9;\n\n    public static int UNICODET = 10;\n\n    public static int UNICODES = 11;\n\n    public static int ISO2022CN = 12;\n\n    public static int ISO2022CN_CNS = 13;\n\n    public static int ISO2022CN_GB = 14;\n\n    public static int EUC_KR = 15;\n\n    public static int CP949 = 16;\n\n    public static int ISO2022KR = 17;\n\n    public static int JOHAB = 18;\n\n    public static int SJIS = 19;\n\n    public static int EUC_JP = 20;\n\n    public static int ISO2022JP = 21;\n\n    public static int ASCII = 22;\n\n    public static int OTHER = 23;\n\n    public static int TOTALTYPES = 24;\n\n    public final static int SIMP = 0;\n\n    public final static int TRAD = 1;\n\n    // Names of the encodings as understood by Java\n    public static String[] javaname;\n\n    // Names of the encodings for human viewing\n    public static String[] nicename;\n\n    // Names of charsets as used in charset parameter of HTML Meta tag\n    public static String[] htmlname;\n\n    // Constructor\n    public Encoding() {\n        javaname = new String[TOTALTYPES];\n        nicename = new String[TOTALTYPES];\n        htmlname = new String[TOTALTYPES];\n        // Assign encoding names\n        javaname[GB2312] = \"GB2312\";\n        javaname[GBK] = \"GBK\";\n        javaname[GB18030] = \"GB18030\";\n        javaname[HZ] = \"ASCII\"; // What to put here? Sun doesn't support HZ\n        javaname[ISO2022CN_GB] = \"ISO2022CN_GB\";\n        javaname[BIG5] = \"BIG5\";\n        javaname[CNS11643] = \"EUC-TW\";\n        javaname[ISO2022CN_CNS] = \"ISO2022CN_CNS\";\n        javaname[ISO2022CN] = \"ISO2022CN\";\n        javaname[UTF8] = \"UTF-8\";\n        javaname[UTF8T] = \"UTF-8\";\n        javaname[UTF8S] = \"UTF-8\";\n        javaname[UNICODE] = \"Unicode\";\n        javaname[UNICODET] = \"Unicode\";\n        javaname[UNICODES] = \"Unicode\";\n        javaname[EUC_KR] = \"EUC_KR\";\n        javaname[CP949] = \"MS949\";\n        javaname[ISO2022KR] = \"ISO2022KR\";\n        javaname[JOHAB] = \"Johab\";\n        javaname[SJIS] = \"SJIS\";\n        javaname[EUC_JP] = \"EUC_JP\";\n        javaname[ISO2022JP] = \"ISO2022JP\";\n        javaname[ASCII] = \"ASCII\";\n        javaname[OTHER] = \"ISO8859_1\";\n        // Assign encoding names\n        htmlname[GB2312] = \"GB2312\";\n        htmlname[GBK] = \"GBK\";\n        htmlname[GB18030] = \"GB18030\";\n        htmlname[HZ] = \"HZ-GB-2312\";\n        htmlname[ISO2022CN_GB] = \"ISO-2022-CN-EXT\";\n        htmlname[BIG5] = \"BIG5\";\n        htmlname[CNS11643] = \"EUC-TW\";\n        htmlname[ISO2022CN_CNS] = \"ISO-2022-CN-EXT\";\n        htmlname[ISO2022CN] = \"ISO-2022-CN\";\n        htmlname[UTF8] = \"UTF-8\";\n        htmlname[UTF8T] = \"UTF-8\";\n        htmlname[UTF8S] = \"UTF-8\";\n        htmlname[UNICODE] = \"UTF-16\";\n        htmlname[UNICODET] = \"UTF-16\";\n        htmlname[UNICODES] = \"UTF-16\";\n        htmlname[EUC_KR] = \"EUC-KR\";\n        htmlname[CP949] = \"x-windows-949\";\n        htmlname[ISO2022KR] = \"ISO-2022-KR\";\n        htmlname[JOHAB] = \"x-Johab\";\n        htmlname[SJIS] = \"Shift_JIS\";\n        htmlname[EUC_JP] = \"EUC-JP\";\n        htmlname[ISO2022JP] = \"ISO-2022-JP\";\n        htmlname[ASCII] = \"ASCII\";\n        htmlname[OTHER] = \"ISO8859-1\";\n        // Assign Human readable names\n        nicename[GB2312] = \"GB-2312\";\n        nicename[GBK] = \"GBK\";\n        nicename[GB18030] = \"GB18030\";\n        nicename[HZ] = \"HZ\";\n        nicename[ISO2022CN_GB] = \"ISO2022CN-GB\";\n        nicename[BIG5] = \"Big5\";\n        nicename[CNS11643] = \"CNS11643\";\n        nicename[ISO2022CN_CNS] = \"ISO2022CN-CNS\";\n        nicename[ISO2022CN] = \"ISO2022 CN\";\n        nicename[UTF8] = \"UTF-8\";\n        nicename[UTF8T] = \"UTF-8 (Trad)\";\n        nicename[UTF8S] = \"UTF-8 (Simp)\";\n        nicename[UNICODE] = \"Unicode\";\n        nicename[UNICODET] = \"Unicode (Trad)\";\n        nicename[UNICODES] = \"Unicode (Simp)\";\n        nicename[EUC_KR] = \"EUC-KR\";\n        nicename[CP949] = \"CP949\";\n        nicename[ISO2022KR] = \"ISO 2022 KR\";\n        nicename[JOHAB] = \"Johab\";\n        nicename[SJIS] = \"Shift-JIS\";\n        nicename[EUC_JP] = \"EUC-JP\";\n        nicename[ISO2022JP] = \"ISO 2022 JP\";\n        nicename[ASCII] = \"ASCII\";\n        nicename[OTHER] = \"OTHER\";\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/FastXmlSerializer.java",
    "content": "package com.kunfei.bookshelf.utils;\n/*\n * Copyright (C) 2006 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport org.xmlpull.v1.XmlSerializer;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.OutputStreamWriter;\nimport java.io.UnsupportedEncodingException;\nimport java.io.Writer;\nimport java.nio.ByteBuffer;\nimport java.nio.CharBuffer;\nimport java.nio.charset.Charset;\nimport java.nio.charset.CharsetEncoder;\nimport java.nio.charset.CoderResult;\nimport java.nio.charset.IllegalCharsetNameException;\nimport java.nio.charset.UnsupportedCharsetException;\n\n/**\n * This is a quick and dirty implementation of XmlSerializer that isn't horribly\n * painfully slow like the normal one.  It only does what is needed for the\n * specific XML files being written with it.\n * {@hide}\n */\npublic class FastXmlSerializer implements XmlSerializer {\n    private static final String ESCAPE_TABLE[] = new String[]{\n            null, null, null, null, null, null, null, null,  // 0-7\n            null, null, null, null, null, null, null, null,  // 8-15\n            null, null, null, null, null, null, null, null,  // 16-23\n            null, null, null, null, null, null, null, null,  // 24-31\n            null, null, \"&quot;\", null, null, null, \"&amp;\", null,  // 32-39\n            null, null, null, null, null, null, null, null,  // 40-47\n            null, null, null, null, null, null, null, null,  // 48-55\n            null, null, null, null, \"&lt;\", null, \"&gt;\", null,  // 56-63\n    };\n\n    private static final int BUFFER_LEN = 8192;\n\n    private static String sSpace = \"                                                              \";\n\n    private final char[] mText = new char[BUFFER_LEN];\n    private int mPos;\n\n    private Writer mWriter;\n\n    private OutputStream mOutputStream;\n    private CharsetEncoder mCharset;\n    private ByteBuffer mBytes = ByteBuffer.allocate(BUFFER_LEN);\n\n    private boolean mIndent = false;\n    private boolean mInTag;\n\n    private int mNesting = 0;\n    private boolean mLineStart = true;\n\n    private void append(char c) throws IOException {\n        int pos = mPos;\n        if (pos >= (BUFFER_LEN - 1)) {\n            flush();\n            pos = mPos;\n        }\n        mText[pos] = c;\n        mPos = pos + 1;\n    }\n\n    private void append(String str, int i, final int length) throws IOException {\n        if (length > BUFFER_LEN) {\n            final int end = i + length;\n            while (i < end) {\n                int next = i + BUFFER_LEN;\n                append(str, i, next < end ? BUFFER_LEN : (end - i));\n                i = next;\n            }\n            return;\n        }\n        int pos = mPos;\n        if ((pos + length) > BUFFER_LEN) {\n            flush();\n            pos = mPos;\n        }\n        str.getChars(i, i + length, mText, pos);\n        mPos = pos + length;\n    }\n\n    private void append(char[] buf, int i, final int length) throws IOException {\n        if (length > BUFFER_LEN) {\n            final int end = i + length;\n            while (i < end) {\n                int next = i + BUFFER_LEN;\n                append(buf, i, next < end ? BUFFER_LEN : (end - i));\n                i = next;\n            }\n            return;\n        }\n        int pos = mPos;\n        if ((pos + length) > BUFFER_LEN) {\n            flush();\n            pos = mPos;\n        }\n        System.arraycopy(buf, i, mText, pos, length);\n        mPos = pos + length;\n    }\n\n    private void append(String str) throws IOException {\n        append(str, 0, str.length());\n    }\n\n    private void appendIndent(int indent) throws IOException {\n        indent *= 4;\n        if (indent > sSpace.length()) {\n            indent = sSpace.length();\n        }\n        append(sSpace, 0, indent);\n    }\n\n    private void escapeAndAppendString(final String string) throws IOException {\n        final int N = string.length();\n        final char NE = (char) ESCAPE_TABLE.length;\n        final String[] escapes = ESCAPE_TABLE;\n        int lastPos = 0;\n        int pos;\n        for (pos = 0; pos < N; pos++) {\n            char c = string.charAt(pos);\n            if (c >= NE) continue;\n            String escape = escapes[c];\n            if (escape == null) continue;\n            if (lastPos < pos) append(string, lastPos, pos - lastPos);\n            lastPos = pos + 1;\n            append(escape);\n        }\n        if (lastPos < pos) append(string, lastPos, pos - lastPos);\n    }\n\n    private void escapeAndAppendString(char[] buf, int start, int len) throws IOException {\n        final char NE = (char) ESCAPE_TABLE.length;\n        final String[] escapes = ESCAPE_TABLE;\n        int end = start + len;\n        int lastPos = start;\n        int pos;\n        for (pos = start; pos < end; pos++) {\n            char c = buf[pos];\n            if (c >= NE) continue;\n            String escape = escapes[c];\n            if (escape == null) continue;\n            if (lastPos < pos) append(buf, lastPos, pos - lastPos);\n            lastPos = pos + 1;\n            append(escape);\n        }\n        if (lastPos < pos) append(buf, lastPos, pos - lastPos);\n    }\n\n    public XmlSerializer attribute(String namespace, String name, String value) throws IOException,\n            IllegalArgumentException, IllegalStateException {\n        append(' ');\n        if (namespace != null) {\n            append(namespace);\n            append(':');\n        }\n        append(name);\n        append(\"=\\\"\");\n\n        escapeAndAppendString(value);\n        append('\"');\n        mLineStart = false;\n        return this;\n    }\n\n    public void cdsect(String text) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    public void comment(String text) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    public void docdecl(String text) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    public void endDocument() throws IOException, IllegalArgumentException, IllegalStateException {\n        flush();\n    }\n\n    public XmlSerializer endTag(String namespace, String name) throws IOException,\n            IllegalArgumentException, IllegalStateException {\n        mNesting--;\n        if (mInTag) {\n            append(\" />\\n\");\n        } else {\n            if (mIndent && mLineStart) {\n                appendIndent(mNesting);\n            }\n            append(\"</\");\n            if (namespace != null) {\n                append(namespace);\n                append(':');\n            }\n            append(name);\n            append(\">\\n\");\n        }\n        mLineStart = true;\n        mInTag = false;\n        return this;\n    }\n\n    public void entityRef(String text) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    private void flushBytes() throws IOException {\n        int position;\n        if ((position = mBytes.position()) > 0) {\n            mBytes.flip();\n            mOutputStream.write(mBytes.array(), 0, position);\n            mBytes.clear();\n        }\n    }\n\n    public void flush() throws IOException {\n        //Log.i(\"PackageManager\", \"flush mPos=\" + mPos);\n        if (mPos > 0) {\n            if (mOutputStream != null) {\n                CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos);\n                CoderResult result = mCharset.encode(charBuffer, mBytes, true);\n                while (true) {\n                    if (result.isError()) {\n                        throw new IOException(result.toString());\n                    } else if (result.isOverflow()) {\n                        flushBytes();\n                        result = mCharset.encode(charBuffer, mBytes, true);\n                        continue;\n                    }\n                    break;\n                }\n                flushBytes();\n                mOutputStream.flush();\n            } else {\n                mWriter.write(mText, 0, mPos);\n                mWriter.flush();\n            }\n            mPos = 0;\n        }\n    }\n\n    public int getDepth() {\n        throw new UnsupportedOperationException();\n    }\n\n    public boolean getFeature(String name) {\n        throw new UnsupportedOperationException();\n    }\n\n    public String getName() {\n        throw new UnsupportedOperationException();\n    }\n\n    public String getNamespace() {\n        throw new UnsupportedOperationException();\n    }\n\n    public String getPrefix(String namespace, boolean generatePrefix)\n            throws IllegalArgumentException {\n        throw new UnsupportedOperationException();\n    }\n\n    public Object getProperty(String name) {\n        throw new UnsupportedOperationException();\n    }\n\n    public void ignorableWhitespace(String text) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    public void processingInstruction(String text) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    public void setFeature(String name, boolean state) throws IllegalArgumentException,\n            IllegalStateException {\n        if (name.equals(\"http://xmlpull.org/v1/doc/features.html#indent-output\")) {\n            mIndent = true;\n            return;\n        }\n        throw new UnsupportedOperationException();\n    }\n\n    public void setOutput(OutputStream os, String encoding) throws IOException,\n            IllegalArgumentException, IllegalStateException {\n        if (os == null)\n            throw new IllegalArgumentException();\n        if (true) {\n            try {\n                mCharset = Charset.forName(encoding).newEncoder();\n            } catch (IllegalCharsetNameException e) {\n                throw (UnsupportedEncodingException) (new UnsupportedEncodingException(\n                        encoding).initCause(e));\n            } catch (UnsupportedCharsetException e) {\n                throw (UnsupportedEncodingException) (new UnsupportedEncodingException(\n                        encoding).initCause(e));\n            }\n            mOutputStream = os;\n        } else {\n            setOutput(\n                    encoding == null\n                            ? new OutputStreamWriter(os)\n                            : new OutputStreamWriter(os, encoding));\n        }\n    }\n\n    public void setOutput(Writer writer) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        mWriter = writer;\n    }\n\n    public void setPrefix(String prefix, String namespace) throws IOException,\n            IllegalArgumentException, IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    public void setProperty(String name, Object value) throws IllegalArgumentException,\n            IllegalStateException {\n        throw new UnsupportedOperationException();\n    }\n\n    public void startDocument(String encoding, Boolean standalone) throws IOException,\n            IllegalArgumentException, IllegalStateException {\n        append(\"<?xml version='1.0' encoding='utf-8' standalone='\"\n                + (standalone ? \"yes\" : \"no\") + \"' ?>\\n\");\n        mLineStart = true;\n    }\n\n    public XmlSerializer startTag(String namespace, String name) throws IOException,\n            IllegalArgumentException, IllegalStateException {\n        if (mInTag) {\n            append(\">\\n\");\n        }\n        if (mIndent) {\n            appendIndent(mNesting);\n        }\n        mNesting++;\n        append('<');\n        if (namespace != null) {\n            append(namespace);\n            append(':');\n        }\n        append(name);\n        mInTag = true;\n        mLineStart = false;\n        return this;\n    }\n\n    public XmlSerializer text(char[] buf, int start, int len) throws IOException,\n            IllegalArgumentException, IllegalStateException {\n        if (mInTag) {\n            append(\">\");\n            mInTag = false;\n        }\n        escapeAndAppendString(buf, start, len);\n        if (mIndent) {\n            mLineStart = buf[start + len - 1] == '\\n';\n        }\n        return this;\n    }\n\n    public XmlSerializer text(String text) throws IOException, IllegalArgumentException,\n            IllegalStateException {\n        if (mInTag) {\n            append(\">\");\n            mInTag = false;\n        }\n        escapeAndAppendString(text);\n        if (mIndent) {\n            mLineStart = text.length() > 0 && (text.charAt(text.length() - 1) == '\\n');\n        }\n        return this;\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/FileStack.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport java.io.File;\nimport java.util.List;\n\n/**\n * Created by newbiechen on 17-5-28.\n */\n\npublic class FileStack {\n\n    private Node node = null;\n    private int count = 0;\n\n    public void push(FileSnapshot fileSnapshot) {\n        if (fileSnapshot == null) return;\n        Node fileNode = new Node();\n        fileNode.fileSnapshot = fileSnapshot;\n        fileNode.next = node;\n        node = fileNode;\n        ++count;\n    }\n\n    public FileSnapshot pop() {\n        Node fileNode = node;\n        if (fileNode == null) return null;\n        FileSnapshot fileSnapshot = fileNode.fileSnapshot;\n        node = fileNode.next;\n        --count;\n        return fileSnapshot;\n    }\n\n    public int getSize() {\n        return count;\n    }\n\n    //文件快照\n    public static class FileSnapshot {\n        public String filePath;\n        public List<File> files;\n        public int scrollOffset;\n    }\n\n    //节点\n    public class Node {\n        FileSnapshot fileSnapshot;\n        Node next;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/FileUtils.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.content.Context\nimport android.os.Environment\nimport android.os.storage.StorageManager\nimport android.webkit.MimeTypeMap\nimport androidx.annotation.IntDef\nimport splitties.init.appCtx\nimport timber.log.Timber\nimport java.io.*\nimport java.nio.charset.Charset\nimport java.text.SimpleDateFormat\nimport java.util.*\nimport java.util.regex.Pattern\n\n@Suppress(\"unused\", \"MemberVisibilityCanBePrivate\")\nobject FileUtils {\n\n    @JvmStatic\n    fun createFileIfNotExist(root: File, vararg subDirFiles: String): File {\n        val filePath = getPath(root, *subDirFiles)\n        return createFileIfNotExist(filePath)\n    }\n\n    @JvmStatic\n    fun createFolderIfNotExist(root: File, vararg subDirs: String): File {\n        val filePath = getPath(root, *subDirs)\n        return createFolderIfNotExist(filePath)\n    }\n\n    @JvmStatic\n    fun createFolderIfNotExist(filePath: String): File {\n        val file = File(filePath)\n        //如果文件夹不存在，就创建它\n        if (!file.exists()) {\n            file.mkdirs()\n        }\n        return file\n    }\n\n    @Synchronized\n    fun createFileIfNotExist(filePath: String): File {\n        val file = File(filePath)\n        try {\n            if (!file.exists()) {\n                //创建父类文件夹\n                file.parent?.let {\n                    createFolderIfNotExist(it)\n                }\n                //创建文件\n                file.createNewFile()\n            }\n        } catch (e: IOException) {\n            Timber.e(e)\n        }\n        return file\n    }\n\n    fun createFileWithReplace(filePath: String): File {\n        val file = File(filePath)\n        if (!file.exists()) {\n            //创建父类文件夹\n            file.parent?.let {\n                createFolderIfNotExist(it)\n            }\n            //创建文件\n            file.createNewFile()\n        } else {\n            file.delete()\n            file.createNewFile()\n        }\n        return file\n    }\n\n    fun getPath(rootPath: String, vararg subDirFiles: String): String {\n        val path = StringBuilder(rootPath)\n        subDirFiles.forEach {\n            if (it.isNotEmpty()) {\n                if (!path.endsWith(File.separator)) {\n                    path.append(File.separator)\n                }\n                path.append(it)\n            }\n        }\n        return path.toString()\n    }\n\n    fun getPath(root: File, vararg subDirFiles: String): String {\n        val path = StringBuilder(root.absolutePath)\n        subDirFiles.forEach {\n            if (it.isNotEmpty()) {\n                path.append(File.separator).append(it)\n            }\n        }\n        return path.toString()\n    }\n\n    //递归删除文件夹下的数据\n    @Synchronized\n    fun deleteFile(filePath: String) {\n        val file = File(filePath)\n        if (!file.exists()) return\n\n        if (file.isDirectory) {\n            val files = file.listFiles()\n            files?.forEach { subFile ->\n                val path = subFile.path\n                deleteFile(path)\n            }\n        }\n        //删除文件\n        file.delete()\n    }\n\n    fun getCachePath(): String {\n        return appCtx.externalCache.absolutePath\n    }\n\n    @JvmStatic\n    fun getStorageData(pContext: Context): ArrayList<String>? {\n        val storageManager = pContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager\n        try {\n            val getVolumeList = storageManager.javaClass.getMethod(\"getVolumeList\")\n            val storageValumeClazz = Class.forName(\"android.os.storage.StorageVolume\")\n            val getPath = storageValumeClazz.getMethod(\"getPath\")\n            val invokeVolumeList = getVolumeList.invoke(storageManager)\n            val length = java.lang.reflect.Array.getLength(invokeVolumeList)\n            val list = ArrayList<String>()\n            for (i in 0 until length) {\n                val storageValume =\n                    java.lang.reflect.Array.get(invokeVolumeList, i) //得到StorageVolume对象\n                val path = getPath.invoke(storageValume) as String\n                list.add(path)\n            }\n            return list\n        } catch (e: java.lang.Exception) {\n            e.printStackTrace()\n        }\n        return null\n    }\n\n    @JvmStatic\n    fun getSdCardPath(): String {\n        @Suppress(\"DEPRECATION\")\n        var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath\n        try {\n            sdCardDirectory = File(sdCardDirectory).canonicalPath\n        } catch (e: IOException) {\n            Timber.e(e)\n        }\n        return sdCardDirectory\n    }\n\n    const val BY_NAME_ASC = 0\n    const val BY_NAME_DESC = 1\n    const val BY_TIME_ASC = 2\n    const val BY_TIME_DESC = 3\n    const val BY_SIZE_ASC = 4\n    const val BY_SIZE_DESC = 5\n    const val BY_EXTENSION_ASC = 6\n    const val BY_EXTENSION_DESC = 7\n\n    @IntDef(value = [BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC])\n    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)\n    annotation class SortType\n\n    /**\n     * 将目录分隔符统一为平台默认的分隔符，并为目录结尾添加分隔符\n     */\n    fun separator(path: String): String {\n        var path1 = path\n        val separator = File.separator\n        path1 = path1.replace(\"\\\\\", separator)\n        if (!path1.endsWith(separator)) {\n            path1 += separator\n        }\n        return path1\n    }\n\n    fun closeSilently(c: Closeable?) {\n        if (c == null) {\n            return\n        }\n        try {\n            c.close()\n        } catch (ignored: IOException) {\n        }\n\n    }\n\n    /**\n     * 列出指定目录下的所有子目录\n     */\n    @JvmOverloads\n    fun listDirs(\n        startDirPath: String,\n        excludeDirs: Array<String>? = null, @SortType sortType: Int = BY_NAME_ASC\n    ): Array<File> {\n        var excludeDirs1 = excludeDirs\n        val dirList = ArrayList<File>()\n        val startDir = File(startDirPath)\n        if (!startDir.isDirectory) {\n            return arrayOf()\n        }\n        val dirs = startDir.listFiles(FileFilter { f ->\n            if (f == null) {\n                return@FileFilter false\n            }\n            f.isDirectory\n        }) ?: return arrayOf()\n        if (excludeDirs1 == null) {\n            excludeDirs1 = arrayOf()\n        }\n        for (dir in dirs) {\n            val file = dir.absoluteFile\n            if (!excludeDirs1.contentDeepToString().contains(file.name)) {\n                dirList.add(file)\n            }\n        }\n        when (sortType) {\n            BY_NAME_ASC -> Collections.sort(dirList, SortByName())\n            BY_NAME_DESC -> {\n                Collections.sort(dirList, SortByName())\n                dirList.reverse()\n            }\n            BY_TIME_ASC -> Collections.sort(dirList, SortByTime())\n            BY_TIME_DESC -> {\n                Collections.sort(dirList, SortByTime())\n                dirList.reverse()\n            }\n            BY_SIZE_ASC -> Collections.sort(dirList, SortBySize())\n            BY_SIZE_DESC -> {\n                Collections.sort(dirList, SortBySize())\n                dirList.reverse()\n            }\n            BY_EXTENSION_ASC -> Collections.sort(dirList, SortByExtension())\n            BY_EXTENSION_DESC -> {\n                Collections.sort(dirList, SortByExtension())\n                dirList.reverse()\n            }\n        }\n        return dirList.toTypedArray()\n    }\n\n    /**\n     * 列出指定目录下的所有子目录及所有文件\n     */\n    @JvmOverloads\n    fun listDirsAndFiles(\n        startDirPath: String,\n        allowExtensions: Array<String>? = null\n    ): Array<File>? {\n        val dirs: Array<File>?\n        val files: Array<File>? = if (allowExtensions == null) {\n            listFiles(startDirPath)\n        } else {\n            listFiles(startDirPath, allowExtensions)\n        }\n        dirs = listDirs(startDirPath)\n        if (files == null) {\n            return null\n        }\n        return dirs + files\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    @JvmOverloads\n    fun listFiles(\n        startDirPath: String,\n        filterPattern: Pattern? = null, @SortType sortType: Int = BY_NAME_ASC\n    ): Array<File> {\n        val fileList = ArrayList<File>()\n        val f = File(startDirPath)\n        if (!f.isDirectory) {\n            return arrayOf()\n        }\n        val files = f.listFiles(FileFilter { file ->\n            if (file == null) {\n                return@FileFilter false\n            }\n            if (file.isDirectory) {\n                return@FileFilter false\n            }\n\n            filterPattern?.matcher(file.name)?.find() ?: true\n        })\n            ?: return arrayOf()\n        for (file in files) {\n            fileList.add(file.absoluteFile)\n        }\n        when (sortType) {\n            BY_NAME_ASC -> Collections.sort(fileList, SortByName())\n            BY_NAME_DESC -> {\n                Collections.sort(fileList, SortByName())\n                fileList.reverse()\n            }\n            BY_TIME_ASC -> Collections.sort(fileList, SortByTime())\n            BY_TIME_DESC -> {\n                Collections.sort(fileList, SortByTime())\n                fileList.reverse()\n            }\n            BY_SIZE_ASC -> Collections.sort(fileList, SortBySize())\n            BY_SIZE_DESC -> {\n                Collections.sort(fileList, SortBySize())\n                fileList.reverse()\n            }\n            BY_EXTENSION_ASC -> Collections.sort(fileList, SortByExtension())\n            BY_EXTENSION_DESC -> {\n                Collections.sort(fileList, SortByExtension())\n                fileList.reverse()\n            }\n        }\n        return fileList.toTypedArray()\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    fun listFiles(startDirPath: String, allowExtensions: Array<String>?): Array<File>? {\n        val file = File(startDirPath)\n        return file.listFiles { _, name ->\n            //返回当前目录所有以某些扩展名结尾的文件\n            val extension = getExtension(name)\n            allowExtensions?.contentDeepToString()?.contains(extension) == true\n                    || allowExtensions == null\n        }\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    fun listFiles(startDirPath: String, allowExtension: String?): Array<File>? {\n        return if (allowExtension == null)\n            listFiles(startDirPath, allowExtension = null)\n        else\n            listFiles(startDirPath, arrayOf(allowExtension))\n    }\n\n    /**\n     * 判断文件或目录是否存在\n     */\n    fun exist(path: String): Boolean {\n        val file = File(path)\n        return file.exists()\n    }\n\n    /**\n     * 删除文件或目录\n     */\n    @JvmOverloads\n    fun delete(file: File, deleteRootDir: Boolean = false): Boolean {\n        var result = false\n        if (file.isFile) {\n            //是文件\n            result = deleteResolveEBUSY(file)\n        } else {\n            //是目录\n            val files = file.listFiles() ?: return false\n            if (files.isEmpty()) {\n                result = deleteRootDir && deleteResolveEBUSY(file)\n            } else {\n                for (f in files) {\n                    delete(f, deleteRootDir)\n                    result = deleteResolveEBUSY(f)\n                }\n            }\n            if (deleteRootDir) {\n                result = deleteResolveEBUSY(file)\n            }\n        }\n        return result\n    }\n\n    /**\n     * bug: open failed: EBUSY (Device or resource busy)\n     * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy\n     */\n    private fun deleteResolveEBUSY(file: File): Boolean {\n        // Before you delete a Directory or File: rename it!\n        val to = File(file.absolutePath + System.currentTimeMillis())\n\n        file.renameTo(to)\n        return to.delete()\n    }\n\n    /**\n     * 删除文件或目录\n     */\n    @JvmOverloads\n    fun delete(path: String, deleteRootDir: Boolean = false): Boolean {\n        val file = File(path)\n\n        return if (file.exists()) {\n            delete(file, deleteRootDir)\n        } else false\n    }\n\n    /**\n     * 复制文件为另一个文件，或复制某目录下的所有文件及目录到另一个目录下\n     */\n    fun copy(src: String, tar: String): Boolean {\n        val srcFile = File(src)\n        return srcFile.exists() && copy(srcFile, File(tar))\n    }\n\n    /**\n     * 复制文件或目录\n     */\n    fun copy(src: File, tar: File): Boolean {\n        try {\n            if (src.isFile) {\n                val `is` = FileInputStream(src)\n                val op = FileOutputStream(tar)\n                val bis = BufferedInputStream(`is`)\n                val bos = BufferedOutputStream(op)\n                val bt = ByteArray(1024 * 8)\n                while (true) {\n                    val len = bis.read(bt)\n                    if (len == -1) {\n                        break\n                    } else {\n                        bos.write(bt, 0, len)\n                    }\n                }\n                bis.close()\n                bos.close()\n            } else if (src.isDirectory) {\n                tar.mkdirs()\n                src.listFiles()?.forEach { file ->\n                    copy(file.absoluteFile, File(tar.absoluteFile, file.name))\n                }\n            }\n            return true\n        } catch (e: Exception) {\n            return false\n        }\n\n    }\n\n    /**\n     * 移动文件或目录\n     */\n    fun move(src: String, tar: String): Boolean {\n        return move(File(src), File(tar))\n    }\n\n    /**\n     * 移动文件或目录\n     */\n    fun move(src: File, tar: File): Boolean {\n        return rename(src, tar)\n    }\n\n    /**\n     * 文件重命名\n     */\n    fun rename(oldPath: String, newPath: String): Boolean {\n        return rename(File(oldPath), File(newPath))\n    }\n\n    /**\n     * 文件重命名\n     */\n    fun rename(src: File, tar: File): Boolean {\n        return src.renameTo(tar)\n    }\n\n    /**\n     * 读取文本文件, 失败将返回空串\n     */\n    @JvmOverloads\n    fun readText(filepath: String, charset: String = \"utf-8\"): String {\n        try {\n            val data = readBytes(filepath)\n            if (data != null) {\n                return String(data, Charset.forName(charset)).trim { it <= ' ' }\n            }\n        } catch (ignored: UnsupportedEncodingException) {\n        }\n\n        return \"\"\n    }\n\n    /**\n     * 读取文件内容, 失败将返回空串\n     */\n    fun readBytes(filepath: String): ByteArray? {\n        var fis: FileInputStream? = null\n        try {\n            fis = FileInputStream(filepath)\n            val baos = ByteArrayOutputStream()\n            val buffer = ByteArray(1024)\n            while (true) {\n                val len = fis.read(buffer, 0, buffer.size)\n                if (len == -1) {\n                    break\n                } else {\n                    baos.write(buffer, 0, len)\n                }\n            }\n            val data = baos.toByteArray()\n            baos.close()\n            return data\n        } catch (e: IOException) {\n            return null\n        } finally {\n            closeSilently(fis)\n        }\n    }\n\n    /**\n     * 保存文本内容\n     */\n    @JvmOverloads\n    fun writeText(filepath: String, content: String, charset: String = \"utf-8\"): Boolean {\n        return try {\n            writeBytes(filepath, content.toByteArray(charset(charset)))\n        } catch (e: UnsupportedEncodingException) {\n            false\n        }\n\n    }\n\n    /**\n     * 保存文件内容\n     */\n    fun writeBytes(filepath: String, data: ByteArray): Boolean {\n        val file = File(filepath)\n        var fos: FileOutputStream? = null\n        return try {\n            if (!file.exists()) {\n                file.parentFile?.mkdirs()\n                file.createNewFile()\n            }\n            fos = FileOutputStream(filepath)\n            fos.write(data)\n            true\n        } catch (e: IOException) {\n            false\n        } finally {\n            closeSilently(fos)\n        }\n    }\n\n    /**\n     * 保存文件内容\n     */\n    fun writeInputStream(filepath: String, data: InputStream): Boolean {\n        val file = File(filepath)\n        return writeInputStream(file, data)\n    }\n\n    /**\n     * 保存文件内容\n     */\n    fun writeInputStream(file: File, data: InputStream): Boolean {\n        var fos: FileOutputStream? = null\n        return try {\n            if (!file.exists()) {\n                file.parentFile?.mkdirs()\n                file.createNewFile()\n            }\n            val buffer = ByteArray(1024 * 4)\n            fos = FileOutputStream(file)\n            while (true) {\n                val len = data.read(buffer, 0, buffer.size)\n                if (len == -1) {\n                    break\n                } else {\n                    fos.write(buffer, 0, len)\n                }\n            }\n            data.close()\n            fos.flush()\n\n            true\n        } catch (e: IOException) {\n            false\n        } finally {\n            closeSilently(fos)\n        }\n    }\n\n    /**\n     * 追加文本内容\n     */\n    fun appendText(path: String, content: String): Boolean {\n        val file = File(path)\n        var writer: FileWriter? = null\n        return try {\n            if (!file.exists()) {\n\n                file.createNewFile()\n            }\n            writer = FileWriter(file, true)\n            writer.write(content)\n            true\n        } catch (e: IOException) {\n            false\n        } finally {\n            closeSilently(writer)\n        }\n    }\n\n    /**\n     * 获取文件大小\n     */\n    fun getLength(path: String): Long {\n        val file = File(path)\n        return if (!file.isFile || !file.exists()) {\n            0\n        } else file.length()\n    }\n\n    /**\n     * 获取文件或网址的名称（包括后缀）\n     */\n    fun getName(pathOrUrl: String?): String {\n        if (pathOrUrl == null) {\n            return \"\"\n        }\n        val pos = pathOrUrl.lastIndexOf('/')\n        return if (0 <= pos) {\n            pathOrUrl.substring(pos + 1)\n        } else {\n            System.currentTimeMillis().toString() + \".\" + getExtension(pathOrUrl)\n        }\n    }\n\n    /**\n     * 获取文件名（不包括扩展名）\n     */\n    fun getNameExcludeExtension(path: String): String {\n        return try {\n            var fileName = File(path).name\n            val lastIndexOf = fileName.lastIndexOf(\".\")\n            if (lastIndexOf != -1) {\n                fileName = fileName.substring(0, lastIndexOf)\n            }\n            fileName\n        } catch (e: Exception) {\n            \"\"\n        }\n\n    }\n\n    /**\n     * 获取格式化后的文件大小\n     */\n    fun getSize(path: String): String {\n        val fileSize = getLength(path)\n        return ConvertUtils.toFileSizeString(fileSize)\n    }\n\n    /**\n     * 获取文件后缀,不包括“.”\n     */\n    fun getExtension(pathOrUrl: String): String {\n        val dotPos = pathOrUrl.lastIndexOf('.')\n        return if (0 <= dotPos) {\n            pathOrUrl.substring(dotPos + 1)\n        } else {\n            \"ext\"\n        }\n    }\n\n    /**\n     * 获取文件的MIME类型\n     */\n    fun getMimeType(pathOrUrl: String): String {\n        val ext = getExtension(pathOrUrl)\n        val map = MimeTypeMap.getSingleton()\n        return map.getMimeTypeFromExtension(ext) ?: \"*/*\"\n    }\n\n    /**\n     * 获取格式化后的文件/目录创建或最后修改时间\n     */\n    @JvmOverloads\n    fun getDateTime(path: String, format: String = \"yyyy年MM月dd日HH:mm\"): String {\n        val file = File(path)\n        return getDateTime(file, format)\n    }\n\n    /**\n     * 获取格式化后的文件/目录创建或最后修改时间\n     */\n    fun getDateTime(file: File, format: String): String {\n        val cal = Calendar.getInstance()\n        cal.timeInMillis = file.lastModified()\n        return SimpleDateFormat(format, Locale.PRC).format(cal.time)\n    }\n\n    /**\n     * 比较两个文件的最后修改时间\n     */\n    fun compareLastModified(path1: String, path2: String): Int {\n        val stamp1 = File(path1).lastModified()\n        val stamp2 = File(path2).lastModified()\n        return when {\n            stamp1 > stamp2 -> {\n                1\n            }\n            stamp1 < stamp2 -> {\n                -1\n            }\n            else -> {\n                0\n            }\n        }\n    }\n\n    /**\n     * 创建多级别的目录\n     */\n    fun makeDirs(path: String): Boolean {\n        return makeDirs(File(path))\n    }\n\n    /**\n     * 创建多级别的目录\n     */\n    fun makeDirs(file: File): Boolean {\n        return file.mkdirs()\n    }\n\n    class SortByExtension : Comparator<File> {\n\n        override fun compare(f1: File?, f2: File?): Int {\n            return if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    f1.name.compareTo(f2.name, ignoreCase = true)\n                }\n            }\n        }\n\n    }\n\n    class SortByName : Comparator<File> {\n        private var caseSensitive: Boolean = false\n\n        constructor(caseSensitive: Boolean) {\n            this.caseSensitive = caseSensitive\n        }\n\n        constructor() {\n            this.caseSensitive = false\n        }\n\n        override fun compare(f1: File?, f2: File?): Int {\n            if (f1 == null || f2 == null) {\n                return if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                return if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    val s1 = f1.name\n                    val s2 = f2.name\n                    if (caseSensitive) {\n                        s1.cnCompare(s2)\n                    } else {\n                        s1.compareTo(s2, ignoreCase = true)\n                    }\n                }\n            }\n        }\n\n    }\n\n    class SortBySize : Comparator<File> {\n\n        override fun compare(f1: File?, f2: File?): Int {\n            return if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    if (f1.length() < f2.length()) {\n                        -1\n                    } else {\n                        1\n                    }\n                }\n            }\n        }\n\n    }\n\n    class SortByTime : Comparator<File> {\n\n        override fun compare(f1: File?, f2: File?): Int {\n            return if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    -1\n                } else {\n                    1\n                }\n            } else {\n                if (f1.isDirectory && f2.isFile) {\n                    -1\n                } else if (f1.isFile && f2.isDirectory) {\n                    1\n                } else {\n                    if (f1.lastModified() > f2.lastModified()) {\n                        -1\n                    } else {\n                        1\n                    }\n                }\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/FloatExtensions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils\n\nimport android.content.res.Resources\n\nval Float.dp: Float\n    get() = android.util.TypedValue.applyDimension(\n        android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics\n    )\n\nval Float.sp: Float\n    get() = android.util.TypedValue.applyDimension(\n        android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics\n    )\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/GsonExtensions.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport com.google.gson.Gson\nimport com.google.gson.GsonBuilder\nimport com.google.gson.JsonSyntaxException\nimport com.google.gson.reflect.TypeToken\nimport org.jetbrains.anko.attempt\nimport java.lang.reflect.ParameterizedType\nimport java.lang.reflect.Type\n\nval GSON: Gson by lazy {\n    GsonBuilder()\n            .disableHtmlEscaping()\n            .setPrettyPrinting()\n            .create()\n}\n\ninline fun <reified T> genericType() = object : TypeToken<T>() {}.type\n\n\n@Throws(JsonSyntaxException::class)\ninline fun <reified T> Gson.fromJsonObject(json: String?): T? {//可转成任意类型\n    return attempt {\n        val result: T? = fromJson(json, genericType<T>())\n        result\n    }.value\n}\n\n@Throws(JsonSyntaxException::class)\ninline fun <reified T> Gson.fromJsonArray(json: String?): List<T>? {\n    return attempt {\n        val result: List<T>? = fromJson(json, ParameterizedTypeImpl(T::class.java))\n        result\n    }.value\n}\n\nclass ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType {\n    override fun getRawType(): Type = List::class.java\n\n    override fun getOwnerType(): Type? = null\n\n    override fun getActualTypeArguments(): Array<Type> = arrayOf(clazz)\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/GsonUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonParser;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n@SuppressWarnings({\"unused\"})\npublic class GsonUtils {\n    /**\n     * 将Json数据解析成相应的映射对象\n     */\n    public static <T> T parseJObject(String jsonData, Class<T> type) {\n        T result = null;\n        if (!TextUtils.isEmpty(jsonData)) {\n            Gson gson = new GsonBuilder().create();\n            try {\n                result = gson.fromJson(jsonData, type);\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 将Json数组解析成相应的映射对象List\n     */\n    public static <T> List<T> parseJArray(String jsonData, Class<T> type) {\n        List<T> result = null;\n        if (!TextUtils.isEmpty(jsonData)) {\n            Gson gson = new GsonBuilder().create();\n            try {\n                JsonParser parser = new JsonParser();\n                JsonArray JArray = parser.parse(jsonData).getAsJsonArray();\n                if (JArray != null) {\n                    result = new ArrayList<>();\n                    for (JsonElement obj : JArray) {\n                        try {\n                            T cse = gson.fromJson(obj, type);\n                            result.add(cse);\n                        } catch (Exception e) {\n                            e.printStackTrace();\n                        }\n                    }\n                }\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 将对象转换成Json\n     */\n    public static <T> String toJsonWithSerializeNulls(T entity) {\n        entity.getClass();\n        Gson gson = new GsonBuilder().serializeNulls().create();\n        String result = \"\";\n        try {\n            result = gson.toJson(entity);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return result;\n    }\n\n    /**\n     * 将list排除值为null的字段转换成Json数组\n     */\n    public static <T> String toJsonArrayWithSerializeNulls(List<T> list) {\n        Gson gson = new GsonBuilder().serializeNulls().create();\n        String result = \"\";\n        try {\n            result = gson.toJson(list);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return result;\n    }\n\n    /**\n     * 将list中将Expose注解的字段转换成Json数组\n     */\n    public static <T> String toJsonArrayWithExpose(List<T> list) {\n        Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();\n        String result = \"\";\n        try {\n            result = gson.toJson(list);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/HandlerUtils.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils\n\nimport android.os.Build.VERSION.SDK_INT\nimport android.os.Handler\nimport android.os.Looper\n\n/** This main looper cache avoids synchronization overhead when accessed repeatedly. */\n@JvmField\nval mainLooper: Looper = Looper.getMainLooper()!!\n\n@JvmField\nval mainThread: Thread = mainLooper.thread\n\nval isMainThread: Boolean inline get() = mainThread === Thread.currentThread()\n\n@PublishedApi\ninternal val currentThread: Any?\n    inline get() = Thread.currentThread()\n\n@JvmField\nval mainHandler: Handler = if (SDK_INT >= 28) Handler.createAsync(mainLooper) else try {\n    Handler::class.java.getDeclaredConstructor(\n        Looper::class.java,\n        Handler.Callback::class.java,\n        Boolean::class.javaPrimitiveType // async\n    ).newInstance(mainLooper, null, true)\n} catch (ignored: NoSuchMethodException) {\n    Handler(mainLooper) // Hidden constructor absent. Fall back to non-async constructor.\n}\n\nfun runOnUI(function: () -> Unit) {\n    if (isMainThread) {\n        function()\n    } else {\n        mainHandler.post(function)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/IOUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Scanner;\n\n/**\n * Created by newbiechen on 17-5-11.\n */\n\npublic class IOUtils {\n\n    public static void close(Closeable closeable) {\n        if (closeable == null) return;\n        try {\n            closeable.close();\n        } catch (IOException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public static String toString(InputStream inputStream) {\n        Scanner s = new Scanner(inputStream).useDelimiter(\"\\\\A\");\n        return s.hasNext() ? s.next() : \"\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/IntentExtensions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils\n\nimport android.content.Intent\n\nfun Intent.putJson(key: String, any: Any?) {\n    any?.let {\n        putExtra(key, GSON.toJson(any))\n    }\n}\n\ninline fun <reified T> Intent.getJsonObject(key: String): T? {\n    val value = getStringExtra(key)\n    return GSON.fromJsonObject<T>(value)\n}\n\ninline fun <reified T> Intent.getJsonArray(key: String): List<T>? {\n    val value = getStringExtra(key)\n    return GSON.fromJsonArray(value)\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ListUtil.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ListUtil {\n\n    public static <T> List<T> filter(List<T> list, ListLook<T> hook) {\n        ArrayList<T> r = new ArrayList<>();\n        for (T t : list) {\n            if (hook.test(t)) {\n                r.add(t);\n            }\n        }\n        r.trimToSize();\n        return r;\n    }\n\n    public interface ListLook<T> {\n        boolean test(T t);\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/MD5Utils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\n/**\n * Created by newbiechen on 2018/1/1.\n */\n\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\n\n/**\n * 将字符串转化为MD5\n */\n\npublic class MD5Utils {\n\n    public static String strToMd5By32(String str) {\n        String reStr = null;\n        try {\n            MessageDigest md5 = MessageDigest.getInstance(\"MD5\");\n            byte[] bytes = md5.digest(str.getBytes());\n            StringBuilder stringBuffer = new StringBuilder();\n            for (byte b : bytes) {\n                int bt = b & 0xff;\n                if (bt < 16) {\n                    stringBuffer.append(0);\n                }\n                stringBuffer.append(Integer.toHexString(bt));\n            }\n            reStr = stringBuffer.toString();\n        } catch (NoSuchAlgorithmException e) {\n            e.printStackTrace();\n        }\n        return reStr;\n    }\n\n    public static String strToMd5By16(String str) {\n        String reStr = strToMd5By32(str);\n        if (reStr != null) {\n            reStr = reStr.substring(8, 24);\n        }\n        return reStr;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/MarkdownUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.os.Build;\nimport android.text.Html;\nimport android.widget.TextView;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class MarkdownUtils {\n\n    @SuppressWarnings(\"deprecation\")\n    public static CharSequence simpleMarkdownConverter(String text) {\n        Pattern listPtn = Pattern.compile(\"^[\\\\-*] \");\n        Pattern headPtn = Pattern.compile(\"^(#{1,6}) \");\n        String strongemPtn = \"\\\\*\\\\*\\\\*([^*]+)\\\\*\\\\*\\\\*\";\n        String strongPtn = \"\\\\*\\\\*([^*]+)\\\\*\\\\*\";\n        String emPtn = \"\\\\*([^*]+)\\\\*\";\n        boolean isInList = false;\n        StringBuilder builder = new StringBuilder();\n        for (String line : text.split(\"\\\\n\")) {\n            Matcher listMtc = listPtn.matcher(line);\n            Matcher headMtc = headPtn.matcher(line);\n            boolean isList = listMtc.find();\n            if (!isInList && isList) {\n                builder.append(\"<ul>\\n\");\n                isInList = true;\n            } else if (isInList && !isList) {\n                builder.append(\"</ul>\\n\");\n                isInList = false;\n            }\n            if (isList) {\n                line = \"<li>\" + line.substring(2) + \"</li>\\n\";\n            } else if (headMtc.find()) {\n                final int level = headMtc.group(1).length();\n                line = \"<h\" + level + \">\" + line.substring(level + 1) + \"</h\" + level + \">\\n\";\n            } else {\n                line = \"<div>\" + line + \"</div>\\n\";\n            }\n            line = line.replaceAll(strongemPtn, \"<strong><em>$1</em></strong>\");\n            line = line.replaceAll(strongPtn, \"<strong>$1</strong>\");\n            line = line.replaceAll(emPtn, \"<em>$1</em>\");\n            builder.append(line);\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            return Html.fromHtml(builder.toString(), Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM);\n        } else {\n            return Html.fromHtml(builder.toString());\n        }\n    }\n\n    public static void setText(TextView view, String text) {\n        view.setText(simpleMarkdownConverter(text));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/MeUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\r\n\r\nimport android.content.res.AssetManager;\r\nimport android.graphics.Bitmap;\r\nimport android.graphics.BitmapFactory;\r\n\r\nimport java.io.IOException;\r\nimport java.io.InputStream;\r\nimport java.util.Scanner;\r\n\r\n/**\r\n * by yangyxd\r\n * data: 2019.08.29\r\n */\r\npublic class MeUtils {\r\n\r\n    /** 获取 assets 中指定目录中的文件名称列表 */\r\n    public static CharSequence[] getAssetsFileList(AssetManager am, String path) throws IOException {\r\n        final String[] fs = am.list(path);\r\n        if (fs == null || fs.length == 0)\r\n            return null;\r\n        final CharSequence[] items = new CharSequence[fs.length];\r\n        for (int i=0; i<fs.length; i++) {\r\n            items[i] = MeUtils.getFileName(fs[i]);\r\n        }\r\n        return items;\r\n    }\r\n\r\n    public static String getFileName(String pathandname){\r\n        int start=pathandname.lastIndexOf(\"/\");\r\n        int end=pathandname.lastIndexOf(\".\");\r\n        if (end < 0) end = pathandname.length();\r\n        return pathandname.substring(start+1,end);\r\n    }\r\n\r\n    public static String getOriginalFundData(AssetManager am, String filename) {\r\n        InputStream input = null;\r\n        try {\r\n            input = am.open(filename);\r\n            String json = convertStreamToString(input);\r\n            return json;\r\n        } catch (Exception e) {\r\n            e.printStackTrace();\r\n        }\r\n        return null;\r\n    }\r\n\r\n    public static String convertStreamToString(InputStream is) {\r\n        String s = null;\r\n        try {\r\n            Scanner scanner = new Scanner(is, \"UTF-8\").useDelimiter(\"\\\\A\");\r\n            if (scanner.hasNext())\r\n                s = scanner.next();\r\n            is.close();\r\n        } catch (IOException e) {\r\n            e.printStackTrace();\r\n        }\r\n        return s;\r\n    }\r\n\r\n    public static Bitmap getFitAssetsSampleBitmap(AssetManager am, String file, int width, int height) {\r\n        InputStream assetFile = null;\r\n        try {\r\n            assetFile = am.open(file);\r\n            BitmapFactory.Options options = new BitmapFactory.Options();\r\n            options.inJustDecodeBounds = true;\r\n            BitmapFactory.decodeStream(assetFile, null, options);\r\n            options.inSampleSize = getFitInSampleSize(width, height, options);\r\n            options.inJustDecodeBounds = false;\r\n            assetFile.close();\r\n            assetFile = am.open(file);\r\n            Bitmap bm = BitmapFactory.decodeStream(assetFile, null, options);\r\n            assetFile.close();\r\n            return bm;\r\n        } catch (Exception e) {\r\n            e.printStackTrace();\r\n            try {\r\n                if (assetFile != null) assetFile.close();\r\n            } catch (Exception ee) {}\r\n            return null;\r\n        }\r\n    }\r\n\r\n    public static int getFitInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options) {\r\n        int inSampleSize = 1;\r\n        if (options.outWidth > reqWidth || options.outHeight > reqHeight) {\r\n            int widthRatio = Math.round((float) options.outWidth / (float) reqWidth);\r\n            int heightRatio = Math.round((float) options.outHeight / (float) reqHeight);\r\n            inSampleSize = Math.min(widthRatio, heightRatio);\r\n        }\r\n        return inSampleSize;\r\n    }\r\n}\r\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/NetworkUtils.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.utils;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.net.ConnectivityManager;\nimport android.net.Network;\nimport android.net.NetworkCapabilities;\nimport android.net.NetworkInfo;\nimport android.os.Build;\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\n\nimport java.net.InetAddress;\nimport java.net.NetworkInterface;\nimport java.net.SocketException;\nimport java.net.URL;\nimport java.util.Enumeration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport retrofit2.Response;\n\npublic class NetworkUtils {\n    public static final Pattern headerPattern = Pattern.compile(\"@Header:\\\\{.+?\\\\}\", Pattern.CASE_INSENSITIVE);\n    public static final int SUCCESS = 10000;\n    public static final int ERROR_CODE_NONET = 10001;\n    public static final int ERROR_CODE_OUTTIME = 10002;\n    public static final int ERROR_CODE_ANALY = 10003;\n    @SuppressLint(\"UseSparseArrays\")\n    private static final Map<Integer, String> errorMap = new HashMap<>();\n\n    static {\n        errorMap.put(ERROR_CODE_NONET, MApplication.getInstance().getString(R.string.net_error_10001));\n        errorMap.put(ERROR_CODE_OUTTIME, MApplication.getInstance().getString(R.string.net_error_10002));\n        errorMap.put(ERROR_CODE_ANALY, MApplication.getInstance().getString(R.string.net_error_10003));\n    }\n\n    public static String getErrorTip(int code) {\n        return errorMap.get(code);\n    }\n\n    public static boolean isNetWorkAvailable() {\n        ConnectivityManager cm = (ConnectivityManager) MApplication.getInstance().getSystemService(Context.CONNECTIVITY_SERVICE);\n        if (Build.VERSION.SDK_INT < 23) {\n            NetworkInfo mWiFiNetworkInfo = cm.getActiveNetworkInfo();\n            if (mWiFiNetworkInfo != null) {\n                //移动数据\n                if (mWiFiNetworkInfo.getType() == ConnectivityManager.TYPE_WIFI) {//WIFI\n                    return true;\n                } else return mWiFiNetworkInfo.getType() == ConnectivityManager.TYPE_MOBILE;\n            }\n        } else {\n            Network network = cm.getActiveNetwork();\n            if (network != null) {\n                NetworkCapabilities nc = cm.getNetworkCapabilities(network);\n                if (nc != null) {\n                    //移动数据\n                    if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {//WIFI\n                        return true;\n                    } else return nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR);\n                }\n            }\n        }\n        return false;\n    }\n\n    public static String getUrl(Response response) {\n        okhttp3.Response networkResponse = response.raw().networkResponse();\n        if (networkResponse != null) {\n            return networkResponse.request().url().toString();\n        } else {\n            return response.raw().request().url().toString();\n        }\n    }\n\n    /**\n     * 获取绝对地址\n     */\n    public static String getAbsoluteURL(String baseURL, String relativePath) {\n        if (TextUtils.isEmpty(relativePath)) return \"\";\n        if (TextUtils.isEmpty(baseURL)) return relativePath;\n        String header = null;\n        if (StringUtils.startWithIgnoreCase(relativePath, \"@header:\")) {\n            header = relativePath.substring(0, relativePath.indexOf(\"}\") + 1);\n            relativePath = relativePath.substring(header.length());\n        }\n        try {\n            URL absoluteUrl = new URL(baseURL);\n            URL parseUrl = new URL(absoluteUrl, relativePath);\n            relativePath = parseUrl.toString();\n            if (header != null) {\n                relativePath = header + relativePath;\n            }\n            return relativePath;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return relativePath;\n    }\n\n    public static String getAbsoluteURL(URL baseURL, String relativePath) {\n        if (baseURL == null) return relativePath;\n        String header = null;\n        if (StringUtils.startWithIgnoreCase(relativePath, \"@header:\")) {\n            header = relativePath.substring(0, relativePath.indexOf(\"}\") + 1);\n            relativePath = relativePath.substring(header.length());\n        }\n        try {\n            URL parseUrl = new URL(baseURL, relativePath);\n            relativePath = parseUrl.toString();\n            if (header != null) {\n                relativePath = header + relativePath;\n            }\n            return relativePath;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return relativePath;\n    }\n\n    public static boolean isUrl(String urlStr) {\n        String regex = \"^(https?)://.+$\";//设置正则表达式\n        return urlStr.matches(regex);\n    }\n\n    /**\n     * Ipv4 address check.\n     */\n    private static final Pattern IPV4_PATTERN = Pattern.compile(\n            \"^(\" + \"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\\\.){3}\" +\n                    \"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$\");\n\n    /**\n     * Check if valid IPV4 address.\n     *\n     * @param input the address string to check for validity.\n     * @return True if the input parameter is a valid IPv4 address.\n     */\n    public static boolean isIPv4Address(String input) {\n        return IPV4_PATTERN.matcher(input).matches();\n    }\n\n    /**\n     * Get local Ip address.\n     */\n    public static InetAddress getLocalIPAddress() {\n        Enumeration<NetworkInterface> enumeration = null;\n        try {\n            enumeration = NetworkInterface.getNetworkInterfaces();\n        } catch (SocketException e) {\n            e.printStackTrace();\n        }\n        if (enumeration != null) {\n            while (enumeration.hasMoreElements()) {\n                NetworkInterface nif = enumeration.nextElement();\n                Enumeration<InetAddress> inetAddresses = nif.getInetAddresses();\n                if (inetAddresses != null) {\n                    while (inetAddresses.hasMoreElements()) {\n                        InetAddress inetAddress = inetAddresses.nextElement();\n                        if (!inetAddress.isLoopbackAddress() && isIPv4Address(inetAddress.getHostAddress())) {\n                            return inetAddress;\n                        }\n                    }\n                }\n            }\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ReadAssets.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.content.Context;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\n\npublic class ReadAssets {\n\n    @SuppressWarnings(\"ResultOfMethodCallIgnored\")\n    public static String getText(Context context, String fileName) {\n        try {\n            //Return an AssetManager instance for your application's package\n            InputStream is = context.getAssets().open(fileName);\n            int size = is.available();\n            // Read the entire asset into a local byte buffer.\n            byte[] buffer = new byte[size];\n            is.read(buffer);\n            is.close();\n            // Convert the buffer into a string.\n            // Finally stick the string into the text view.\n            return new String(buffer, StandardCharsets.UTF_8);\n        } catch (IOException e) {\n            // Should never happen!\n            e.printStackTrace();\n        }\n        return \"读取错误，请检查文件名\";\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/RealPathUtil.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.annotation.SuppressLint\nimport android.content.ContentUris\nimport android.content.Context\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.DocumentsContract\nimport android.provider.MediaStore\nimport timber.log.Timber\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.io.IOException\n\n@Suppress(\"unused\")\nobject RealPathUtil {\n    /**\n     * Method for return file path of Gallery image\n     * @return path of the selected image file from gallery\n     */\n    private var filePathUri: Uri? = null\n\n    @Suppress(\"DEPRECATION\")\n    @JvmStatic\n    fun getPath(context: Context, uri: Uri): String? {\n        //check here to KITKAT or new version\n        @SuppressLint(\"ObsoleteSdkInt\")\n        val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT\n        filePathUri = uri\n        // DocumentProvider\n        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider\n            if (isExternalStorageDocument(uri)) {\n                val docId = DocumentsContract.getDocumentId(uri)\n                val split = docId.split(\":\").toTypedArray()\n                val type = split[0]\n                if (\"primary\".equals(type, ignoreCase = true)) {\n                    return Environment.getExternalStorageDirectory().toString() + \"/\" + split[1]\n                }\n            } else if (isDownloadsDocument(uri)) {\n                val id = DocumentsContract.getDocumentId(uri)\n                val contentUri = ContentUris.withAppendedId(\n                    Uri.parse(\"content://downloads/public_downloads\"),\n                    java.lang.Long.valueOf(id)\n                )\n                //return getDataColumn(context, uri, null, null);\n                return getDataColumn(context, contentUri, null, null)\n            } else if (isMediaDocument(uri)) {\n                val docId = DocumentsContract.getDocumentId(uri)\n                val split = docId.split(\":\").toTypedArray()\n                val type = split[0]\n                var contentUri: Uri? = null\n                when (type) {\n                    \"image\" -> {\n                        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI\n                    }\n                    \"video\" -> {\n                        contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI\n                    }\n                    \"audio\" -> {\n                        contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI\n                    }\n                }\n                val selection = \"_id=?\"\n                val selectionArgs = arrayOf(\n                    split[1]\n                )\n                return getDataColumn(context, contentUri, selection, selectionArgs)\n            }\n        } else if (\"content\".equals(\n                uri.scheme,\n                ignoreCase = true\n            )\n        ) { // Return the remote address\n            return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(\n                context,\n                uri,\n                null,\n                null\n            )\n        } else if (\"file\".equals(uri.scheme, ignoreCase = true)) {\n            return uri.path\n        }\n        return null\n    }\n\n    /**\n     * Get the value of the data column for this Uri. This is useful for\n     * MediaStore Uris, and other file-based ContentProviders.\n     *\n     * @param context The context.\n     * @param uri The Uri to query.\n     * @param selection (Optional) Filter used in the query.\n     * @param selectionArgs (Optional) Selection arguments used in the query.\n     * @return The value of the _data column, which is typically a file path.\n     */\n    private fun getDataColumn(\n        context: Context, uri: Uri?, selection: String?,\n        selectionArgs: Array<String>?\n    ): String? {\n        var cursor: Cursor? = null\n        val column = \"_data\"\n        val projection = arrayOf(\n            column\n        )\n        try {\n            cursor =\n                context.contentResolver.query(uri!!, projection, selection, selectionArgs, null)\n            if (cursor != null && cursor.moveToFirst()) {\n                val index = cursor.getColumnIndexOrThrow(column)\n                return cursor.getString(index)\n            }\n        } catch (e: IllegalArgumentException) {\n            Timber.e(e)\n            val file = File(context.cacheDir, \"tmp\")\n            val filePath = file.absolutePath\n            var input: FileInputStream? = null\n            var output: FileOutputStream? = null\n            try {\n                val pfd =\n                    context.contentResolver.openFileDescriptor(filePathUri!!, \"r\")\n                        ?: return null\n                val fd = pfd.fileDescriptor\n                input = FileInputStream(fd)\n                output = FileOutputStream(filePath)\n                var read: Int\n                val bytes = ByteArray(4096)\n                while (input.read(bytes).also { read = it } != -1) {\n                    output.write(bytes, 0, read)\n                }\n                return File(filePath).absolutePath\n            } catch (ignored: IOException) {\n                Timber.e(ignored)\n            } finally {\n                input?.close()\n                output?.close()\n            }\n        } finally {\n            cursor?.close()\n        }\n        return null\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is ExternalStorageProvider.\n     */\n    private fun isExternalStorageDocument(uri: Uri): Boolean {\n        return \"com.android.externalstorage.documents\" == uri.authority\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is DownloadsProvider.\n     */\n    private fun isDownloadsDocument(uri: Uri): Boolean {\n        return \"com.android.providers.downloads.documents\" == uri.authority\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is MediaProvider.\n     */\n    private fun isMediaDocument(uri: Uri): Boolean {\n        return \"com.android.providers.media.documents\" == uri.authority\n    }\n\n    /**\n     * @param uri The Uri to check.\n     * @return Whether the Uri authority is Google Photos.\n     */\n    private fun isGooglePhotosUri(uri: Uri): Boolean {\n        return \"com.google.android.apps.photos.content\" == uri.authority\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/RxUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\n\nimport androidx.annotation.NonNull;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableSource;\nimport io.reactivex.Single;\nimport io.reactivex.SingleSource;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * Created by newbiechen on 17-4-29.\n */\n\npublic class RxUtils {\n\n    public static <T> SingleSource<T> toSimpleSingle(Single<T> upstream) {\n        return upstream.subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread());\n    }\n\n    @NonNull\n    public static <T> ObservableSource<T> toSimpleSingle(Observable<T> upstream) {\n        return upstream.subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread());\n    }\n\n    public static <T, R> TwoTuple<T, R> twoTuple(T first, R second) {\n        return new TwoTuple<T, R>(first, second);\n    }\n\n    public static class TwoTuple<A, B> {\n        public final A first;\n        public final B second;\n\n        public TwoTuple(A a, B b) {\n            this.first = a;\n            this.second = b;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ScreenUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.annotation.SuppressLint;\nimport android.content.res.Resources;\nimport android.util.DisplayMetrics;\nimport android.view.View;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.kunfei.bookshelf.MApplication;\n\nimport java.lang.reflect.Method;\n\n/**\n * Created by newbiechen on 17-5-1.\n */\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class ScreenUtils {\n\n    public static int dpToPx(int dp) {\n        DisplayMetrics metrics = getDisplayMetrics();\n        return (int) (dp * metrics.density + 0.5f * (dp >= 0 ? 1 : -1));\n    }\n\n    public static int pxToDp(int px) {\n        DisplayMetrics metrics = getDisplayMetrics();\n        return (int) (px / metrics.density);\n    }\n\n    public static int spToPx(int sp) {\n        float fontScale = getDisplayMetrics().scaledDensity;\n        return (int) (sp * fontScale + 0.5f);\n    }\n\n    public static int pxToSp(int px) {\n        DisplayMetrics metrics = getDisplayMetrics();\n        return (int) (px / metrics.scaledDensity);\n    }\n\n    /**\n     * 获取手机显示App区域的大小（头部导航栏+ActionBar+根布局），不包括虚拟按钮\n     *\n     * @return\n     */\n    public static int[] getAppSize() {\n        int[] size = new int[2];\n        DisplayMetrics metrics = getDisplayMetrics();\n        size[0] = metrics.widthPixels;\n        size[1] = metrics.heightPixels;\n        return size;\n    }\n\n    /**\n     * 获取整个手机屏幕的大小(包括虚拟按钮)\n     * 必须在onWindowFocus方法之后使用\n     *\n     * @param activity\n     * @return\n     */\n    public static int[] getScreenSize(AppCompatActivity activity) {\n        int[] size = new int[2];\n        View decorView = activity.getWindow().getDecorView();\n        size[0] = decorView.getWidth();\n        size[1] = decorView.getHeight();\n        return size;\n    }\n\n    /**\n     * 获取状态栏的高度\n     */\n    public static int getStatusBarHeight() {\n        Resources resources = MApplication.getInstance().getResources();\n        int resourceId = resources.getIdentifier(\"status_bar_height\", \"dimen\", \"android\");\n        return resources.getDimensionPixelSize(resourceId);\n    }\n\n    /**\n     * 获取虚拟按键的高度\n     */\n    public static int getNavigationBarHeight() {\n        int navigationBarHeight = 0;\n        Resources rs = MApplication.getInstance().getResources();\n        int id = rs.getIdentifier(\"navigation_bar_height\", \"dimen\", \"android\");\n        if (id > 0 && hasNavigationBar()) {\n            navigationBarHeight = rs.getDimensionPixelSize(id);\n        }\n        return navigationBarHeight;\n    }\n\n    /**\n     * 是否存在虚拟按键\n     *\n     * @return\n     */\n    @SuppressWarnings(\"unchecked\")\n    private static boolean hasNavigationBar() {\n        boolean hasNavigationBar = false;\n        Resources rs = MApplication.getInstance().getResources();\n        int id = rs.getIdentifier(\"config_showNavigationBar\", \"bool\", \"android\");\n        if (id > 0) {\n            hasNavigationBar = rs.getBoolean(id);\n        }\n        try {\n            @SuppressLint(\"PrivateApi\") Class systemPropertiesClass = Class.forName(\"android.os.SystemProperties\");\n            Method m = systemPropertiesClass.getMethod(\"get\", String.class);\n            String navBarOverride = (String) m.invoke(systemPropertiesClass, \"qemu.hw.mainkeys\");\n            if (\"1\".equals(navBarOverride)) {\n                hasNavigationBar = false;\n            } else if (\"0\".equals(navBarOverride)) {\n                hasNavigationBar = true;\n            }\n        } catch (Exception ignored) {\n        }\n        return hasNavigationBar;\n    }\n\n    public static DisplayMetrics getDisplayMetrics() {\n        return MApplication\n                .getInstance()\n                .getResources()\n                .getDisplayMetrics();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/Selector.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.graphics.drawable.StateListDrawable;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.Dimension;\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.IntDef;\nimport androidx.core.content.ContextCompat;\n\npublic class Selector {\n    public static ShapeSelector shapeBuild() {\n        return new ShapeSelector();\n    }\n\n    public static ColorSelector colorBuild() {\n        return new ColorSelector();\n    }\n\n    public static DrawableSelector drawableBuild() {\n        return new DrawableSelector();\n    }\n\n    /**\n     * 形状ShapeSelector\n     *\n     * @author hjy\n     * created at 2017/12/11 22:26\n     */\n    public static final class ShapeSelector {\n        @IntDef({GradientDrawable.RECTANGLE, GradientDrawable.OVAL,\n                GradientDrawable.LINE, GradientDrawable.RING})\n        private @interface Shape {\n        }\n\n        private int mShape;               //the shape of background\n        private int mDefaultBgColor;      //default background color\n        private int mDisabledBgColor;     //state_enabled = false\n        private int mPressedBgColor;      //state_pressed = true\n        private int mSelectedBgColor;     //state_selected = true\n        private int mFocusedBgColor;      //state_focused = true\n        private int mCheckedBgColor;      //state_checked = true\n        private int mStrokeWidth;         //stroke width in pixel\n        private int mDefaultStrokeColor;  //default stroke color\n        private int mDisabledStrokeColor; //state_enabled = false\n        private int mPressedStrokeColor;  //state_pressed = true\n        private int mSelectedStrokeColor; //state_selected = true\n        private int mFocusedStrokeColor;  //state_focused = true\n        private int mCheckedStrokeColor;  //state_checked = true\n        private int mCornerRadius;        //corner radius\n\n        private boolean hasSetDisabledBgColor = false;\n        private boolean hasSetPressedBgColor = false;\n        private boolean hasSetSelectedBgColor = false;\n        private boolean hasSetFocusedBgColor = false;\n        private boolean hasSetCheckedBgColor = false;\n\n        private boolean hasSetDisabledStrokeColor = false;\n        private boolean hasSetPressedStrokeColor = false;\n        private boolean hasSetSelectedStrokeColor = false;\n        private boolean hasSetFocusedStrokeColor = false;\n        private boolean hasSetCheckedStrokeColor = false;\n\n        public ShapeSelector() {\n            //initialize default values\n            mShape = GradientDrawable.RECTANGLE;\n            mDefaultBgColor = Color.TRANSPARENT;\n            mDisabledBgColor = Color.TRANSPARENT;\n            mPressedBgColor = Color.TRANSPARENT;\n            mSelectedBgColor = Color.TRANSPARENT;\n            mFocusedBgColor = Color.TRANSPARENT;\n            mStrokeWidth = 0;\n            mDefaultStrokeColor = Color.TRANSPARENT;\n            mDisabledStrokeColor = Color.TRANSPARENT;\n            mPressedStrokeColor = Color.TRANSPARENT;\n            mSelectedStrokeColor = Color.TRANSPARENT;\n            mFocusedStrokeColor = Color.TRANSPARENT;\n            mCornerRadius = 0;\n        }\n\n        public ShapeSelector setShape(@Shape int shape) {\n            mShape = shape;\n            return this;\n        }\n\n        public ShapeSelector setDefaultBgColor(@ColorInt int color) {\n            mDefaultBgColor = color;\n            if (!hasSetDisabledBgColor)\n                mDisabledBgColor = color;\n            if (!hasSetPressedBgColor)\n                mPressedBgColor = color;\n            if (!hasSetSelectedBgColor)\n                mSelectedBgColor = color;\n            if (!hasSetFocusedBgColor)\n                mFocusedBgColor = color;\n            return this;\n        }\n\n        public ShapeSelector setDisabledBgColor(@ColorInt int color) {\n            mDisabledBgColor = color;\n            hasSetDisabledBgColor = true;\n            return this;\n        }\n\n        public ShapeSelector setPressedBgColor(@ColorInt int color) {\n            mPressedBgColor = color;\n            hasSetPressedBgColor = true;\n            return this;\n        }\n\n        public ShapeSelector setSelectedBgColor(@ColorInt int color) {\n            mSelectedBgColor = color;\n            hasSetSelectedBgColor = true;\n            return this;\n        }\n\n        public ShapeSelector setFocusedBgColor(@ColorInt int color) {\n            mFocusedBgColor = color;\n            hasSetPressedBgColor = true;\n            return this;\n        }\n\n        public ShapeSelector setCheckedBgColor(@ColorInt int color) {\n            mCheckedBgColor = color;\n            hasSetCheckedBgColor = true;\n            return this;\n        }\n\n        public ShapeSelector setStrokeWidth(@Dimension int width) {\n            mStrokeWidth = width;\n            return this;\n        }\n\n        public ShapeSelector setDefaultStrokeColor(@ColorInt int color) {\n            mDefaultStrokeColor = color;\n            if (!hasSetDisabledStrokeColor)\n                mDisabledStrokeColor = color;\n            if (!hasSetPressedStrokeColor)\n                mPressedStrokeColor = color;\n            if (!hasSetSelectedStrokeColor)\n                mSelectedStrokeColor = color;\n            if (!hasSetFocusedStrokeColor)\n                mFocusedStrokeColor = color;\n            return this;\n        }\n\n        public ShapeSelector setDisabledStrokeColor(@ColorInt int color) {\n            mDisabledStrokeColor = color;\n            hasSetDisabledStrokeColor = true;\n            return this;\n        }\n\n        public ShapeSelector setPressedStrokeColor(@ColorInt int color) {\n            mPressedStrokeColor = color;\n            hasSetPressedStrokeColor = true;\n            return this;\n        }\n\n        public ShapeSelector setSelectedStrokeColor(@ColorInt int color) {\n            mSelectedStrokeColor = color;\n            hasSetSelectedStrokeColor = true;\n            return this;\n        }\n\n        public ShapeSelector setCheckedStrokeColor(@ColorInt int color) {\n            mCheckedStrokeColor = color;\n            hasSetCheckedStrokeColor = true;\n            return this;\n        }\n\n        public ShapeSelector setFocusedStrokeColor(@ColorInt int color) {\n            mFocusedStrokeColor = color;\n            hasSetFocusedStrokeColor = true;\n            return this;\n        }\n\n        public ShapeSelector setCornerRadius(@Dimension int radius) {\n            mCornerRadius = radius;\n            return this;\n        }\n\n        public StateListDrawable create() {\n            StateListDrawable selector = new StateListDrawable();\n\n            //enabled = false\n            if (hasSetDisabledBgColor || hasSetDisabledStrokeColor) {\n                GradientDrawable disabledShape = getItemShape(mShape, mCornerRadius,\n                        mDisabledBgColor, mStrokeWidth, mDisabledStrokeColor);\n                selector.addState(new int[]{-android.R.attr.state_enabled}, disabledShape);\n            }\n\n            //pressed = true\n            if (hasSetPressedBgColor || hasSetPressedStrokeColor) {\n                GradientDrawable pressedShape = getItemShape(mShape, mCornerRadius,\n                        mPressedBgColor, mStrokeWidth, mPressedStrokeColor);\n                selector.addState(new int[]{android.R.attr.state_pressed}, pressedShape);\n            }\n\n            //selected = true\n            if (hasSetSelectedBgColor || hasSetSelectedStrokeColor) {\n                GradientDrawable selectedShape = getItemShape(mShape, mCornerRadius,\n                        mSelectedBgColor, mStrokeWidth, mSelectedStrokeColor);\n                selector.addState(new int[]{android.R.attr.state_selected}, selectedShape);\n            }\n\n            //focused = true\n            if (hasSetFocusedBgColor || hasSetFocusedStrokeColor) {\n                GradientDrawable focusedShape = getItemShape(mShape, mCornerRadius,\n                        mFocusedBgColor, mStrokeWidth, mFocusedStrokeColor);\n                selector.addState(new int[]{android.R.attr.state_focused}, focusedShape);\n            }\n\n            //checked = true\n            if (hasSetCheckedBgColor || hasSetCheckedStrokeColor) {\n                GradientDrawable checkedShape = getItemShape(mShape, mCornerRadius,\n                        mCheckedBgColor, mStrokeWidth, mCheckedStrokeColor);\n                selector.addState(new int[]{android.R.attr.state_checked}, checkedShape);\n            }\n\n            //default\n            GradientDrawable defaultShape = getItemShape(mShape, mCornerRadius,\n                    mDefaultBgColor, mStrokeWidth, mDefaultStrokeColor);\n            selector.addState(new int[]{}, defaultShape);\n\n            return selector;\n        }\n\n        private GradientDrawable getItemShape(int shape, int cornerRadius,\n                                              int solidColor, int strokeWidth, int strokeColor) {\n            GradientDrawable drawable = new GradientDrawable();\n            drawable.setShape(shape);\n            drawable.setStroke(strokeWidth, strokeColor);\n            drawable.setCornerRadius(cornerRadius);\n            drawable.setColor(solidColor);\n            return drawable;\n        }\n    }\n\n    /**\n     * 资源DrawableSelector\n     *\n     * @author hjy\n     * created at 2017/12/11 22:34\n     */\n    public static final class DrawableSelector {\n\n        private Drawable mDefaultDrawable;\n        private Drawable mDisabledDrawable;\n        private Drawable mPressedDrawable;\n        private Drawable mSelectedDrawable;\n        private Drawable mFocusedDrawable;\n\n        private boolean hasSetDisabledDrawable = false;\n        private boolean hasSetPressedDrawable = false;\n        private boolean hasSetSelectedDrawable = false;\n        private boolean hasSetFocusedDrawable = false;\n\n        private DrawableSelector() {\n            mDefaultDrawable = new ColorDrawable(Color.TRANSPARENT);\n        }\n\n        public DrawableSelector setDefaultDrawable(Drawable drawable) {\n            mDefaultDrawable = drawable;\n            if (!hasSetDisabledDrawable)\n                mDisabledDrawable = drawable;\n            if (!hasSetPressedDrawable)\n                mPressedDrawable = drawable;\n            if (!hasSetSelectedDrawable)\n                mSelectedDrawable = drawable;\n            if (!hasSetFocusedDrawable)\n                mFocusedDrawable = drawable;\n            return this;\n        }\n\n        public DrawableSelector setDisabledDrawable(Drawable drawable) {\n            mDisabledDrawable = drawable;\n            hasSetDisabledDrawable = true;\n            return this;\n        }\n\n        public DrawableSelector setPressedDrawable(Drawable drawable) {\n            mPressedDrawable = drawable;\n            hasSetPressedDrawable = true;\n            return this;\n        }\n\n        public DrawableSelector setSelectedDrawable(Drawable drawable) {\n            mSelectedDrawable = drawable;\n            hasSetSelectedDrawable = true;\n            return this;\n        }\n\n        public DrawableSelector setFocusedDrawable(Drawable drawable) {\n            mFocusedDrawable = drawable;\n            hasSetFocusedDrawable = true;\n            return this;\n        }\n\n        public StateListDrawable create() {\n            StateListDrawable selector = new StateListDrawable();\n            if (hasSetDisabledDrawable)\n                selector.addState(new int[]{-android.R.attr.state_enabled}, mDisabledDrawable);\n            if (hasSetPressedDrawable)\n                selector.addState(new int[]{android.R.attr.state_pressed}, mPressedDrawable);\n            if (hasSetSelectedDrawable)\n                selector.addState(new int[]{android.R.attr.state_selected}, mSelectedDrawable);\n            if (hasSetFocusedDrawable)\n                selector.addState(new int[]{android.R.attr.state_focused}, mFocusedDrawable);\n            selector.addState(new int[]{}, mDefaultDrawable);\n            return selector;\n        }\n\n        public DrawableSelector setDefaultDrawable(Context context, @DrawableRes int drawableRes) {\n            return setDefaultDrawable(ContextCompat.getDrawable(context, drawableRes));\n        }\n\n        public DrawableSelector setDisabledDrawable(Context context, @DrawableRes int drawableRes) {\n            return setDisabledDrawable(ContextCompat.getDrawable(context, drawableRes));\n        }\n\n        public DrawableSelector setPressedDrawable(Context context, @DrawableRes int drawableRes) {\n            return setPressedDrawable(ContextCompat.getDrawable(context, drawableRes));\n        }\n\n        public DrawableSelector setSelectedDrawable(Context context, @DrawableRes int drawableRes) {\n            return setSelectedDrawable(ContextCompat.getDrawable(context, drawableRes));\n        }\n\n        public DrawableSelector setFocusedDrawable(Context context, @DrawableRes int drawableRes) {\n            return setFocusedDrawable(ContextCompat.getDrawable(context, drawableRes));\n        }\n    }\n\n    /**\n     * 颜色ColorSelector\n     *\n     * @author hjy\n     * created at 2017/12/11 22:26\n     */\n    public static final class ColorSelector {\n\n        private int mDefaultColor;\n        private int mDisabledColor;\n        private int mPressedColor;\n        private int mSelectedColor;\n        private int mFocusedColor;\n        private int mCheckedColor;\n\n        private boolean hasSetDisabledColor = false;\n        private boolean hasSetPressedColor = false;\n        private boolean hasSetSelectedColor = false;\n        private boolean hasSetFocusedColor = false;\n        private boolean hasSetCheckedColor = false;\n\n        private ColorSelector() {\n            mDefaultColor = Color.BLACK;\n            mDisabledColor = Color.GRAY;\n            mPressedColor = Color.BLACK;\n            mSelectedColor = Color.BLACK;\n            mFocusedColor = Color.BLACK;\n        }\n\n        public ColorSelector setDefaultColor(@ColorInt int color) {\n            mDefaultColor = color;\n            if (!hasSetDisabledColor)\n                mDisabledColor = color;\n            if (!hasSetPressedColor)\n                mPressedColor = color;\n            if (!hasSetSelectedColor)\n                mSelectedColor = color;\n            if (!hasSetFocusedColor)\n                mFocusedColor = color;\n            return this;\n        }\n\n        public ColorSelector setDisabledColor(@ColorInt int color) {\n            mDisabledColor = color;\n            hasSetDisabledColor = true;\n            return this;\n        }\n\n        public ColorSelector setPressedColor(@ColorInt int color) {\n            mPressedColor = color;\n            hasSetPressedColor = true;\n            return this;\n        }\n\n        public ColorSelector setSelectedColor(@ColorInt int color) {\n            mSelectedColor = color;\n            hasSetSelectedColor = true;\n            return this;\n        }\n\n        public ColorSelector setFocusedColor(@ColorInt int color) {\n            mFocusedColor = color;\n            hasSetFocusedColor = true;\n            return this;\n        }\n\n        public ColorSelector setCheckedColor(@ColorInt int color) {\n            mCheckedColor = color;\n            hasSetCheckedColor = true;\n            return this;\n        }\n\n        public ColorStateList create() {\n            int[] colors = new int[]{\n                    hasSetDisabledColor ? mDisabledColor : mDefaultColor,\n                    hasSetPressedColor ? mPressedColor : mDefaultColor,\n                    hasSetSelectedColor ? mSelectedColor : mDefaultColor,\n                    hasSetFocusedColor ? mFocusedColor : mDefaultColor,\n                    hasSetCheckedColor ? mCheckedColor : mDefaultColor,\n                    mDefaultColor\n            };\n            int[][] states = new int[6][];\n            states[0] = new int[]{-android.R.attr.state_enabled};\n            states[1] = new int[]{android.R.attr.state_pressed};\n            states[2] = new int[]{android.R.attr.state_selected};\n            states[3] = new int[]{android.R.attr.state_focused};\n            states[4] = new int[]{android.R.attr.state_checked};\n            states[5] = new int[]{};\n            return new ColorStateList(states, colors);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/SoftInputUtil.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Rect;\nimport android.util.DisplayMetrics;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.WindowManager;\nimport android.view.inputmethod.InputMethodManager;\n\nimport com.kunfei.bookshelf.MApplication;\n\npublic class SoftInputUtil {\n\n    //隐藏输入法\n    public static void hideIMM(View view) {\n        InputMethodManager imm = (InputMethodManager) MApplication.getInstance().getSystemService(Context.INPUT_METHOD_SERVICE);\n        if (imm != null && view != null) {\n            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);\n        }\n    }\n\n    public static void resetBoxPosition(Activity activity, View prentView, int viewId) {\n\n        final View decorView = (activity).getWindow().getDecorView();\n        decorView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {\n            try {\n                Rect rect = new Rect();\n                decorView.getWindowVisibleDisplayFrame(rect);\n                int screenHeight = getScreenHeight(activity);\n                int heightDifference = screenHeight - rect.bottom;  //计算软键盘占有的高度  = 屏幕高度 - 视图可见高度\n                View view = prentView.findViewById(viewId);\n                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();\n                layoutParams.bottomMargin = heightDifference;   //设置rlContent的marginBottom的值为软键盘占有的高度即可\n                view.setLayoutParams(layoutParams);\n                view.requestLayout();\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        });\n    }\n\n    public static int getScreenHeight(Activity activity) {\n        WindowManager manager = (activity).getWindowManager();\n        DisplayMetrics outMetrics = new DisplayMetrics();\n        manager.getDefaultDisplay().getMetrics(outMetrics);\n        return outMetrics.heightPixels;\n    }\n\n    public static int getScreenWidth(Activity activity) {\n        WindowManager manager = (activity).getWindowManager();\n        DisplayMetrics outMetrics = new DisplayMetrics();\n        manager.getDefaultDisplay().getMetrics(outMetrics);\n        return outMetrics.widthPixels;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/StringExtensions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils\n\nimport android.icu.text.Collator\nimport android.icu.util.ULocale\nimport android.net.Uri\nimport java.io.File\nimport java.util.*\n\nfun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()\n\nfun String?.isContentScheme(): Boolean = this?.startsWith(\"content://\") == true\n\nfun String.parseToUri(): Uri {\n    return if (isContentScheme()) {\n        Uri.parse(this)\n    } else {\n        Uri.fromFile(File(this))\n    }\n}\n\nfun String?.isAbsUrl() =\n    this?.let {\n        it.startsWith(\"http://\", true) || it.startsWith(\"https://\", true)\n    } ?: false\n\nfun String?.isJson(): Boolean =\n    this?.run {\n        val str = this.trim()\n        when {\n            str.startsWith(\"{\") && str.endsWith(\"}\") -> true\n            str.startsWith(\"[\") && str.endsWith(\"]\") -> true\n            else -> false\n        }\n    } ?: false\n\nfun String?.isJsonObject(): Boolean =\n    this?.run {\n        val str = this.trim()\n        str.startsWith(\"{\") && str.endsWith(\"}\")\n    } ?: false\n\nfun String?.isJsonArray(): Boolean =\n    this?.run {\n        val str = this.trim()\n        str.startsWith(\"[\") && str.endsWith(\"]\")\n    } ?: false\n\nfun String.splitNotBlank(vararg delimiter: String): Array<String> = run {\n    this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()\n}\n\nfun String.splitNotBlank(regex: Regex, limit: Int = 0): Array<String> = run {\n    this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()\n}\n\nfun String.cnCompare(other: String): Int {\n    return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {\n        Collator.getInstance(ULocale.SIMPLIFIED_CHINESE).compare(this, other)\n    } else {\n        java.text.Collator.getInstance(Locale.CHINA).compare(this, other)\n    }\n}\n\n/**\n * 将字符串拆分为单个字符,包含emoji\n */\nfun String.toStringArray(): Array<String> {\n    var codePointIndex = 0\n    return try {\n        Array(codePointCount(0, length)) {\n            val start = codePointIndex\n            codePointIndex = offsetByCodePoints(start, 1)\n            substring(start, codePointIndex)\n        }\n    } catch (e: Exception) {\n        split(\"\").toTypedArray()\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/StringJoiner.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.Objects;\n\npublic class StringJoiner {\n    private String emptyValue;\n    // 前缀\n    private final String prefix;\n    // 分隔符\n    private final String delimiter;\n    // 后缀\n    private final String suffix;\n    // 值\n    private StringBuilder value;\n\n    /**\n     * 构造器\n     */\n    public StringJoiner(CharSequence delimiter) {\n        this(delimiter, \"\", \"\");\n    }\n\n    public StringJoiner(CharSequence delimiter,\n                        CharSequence prefix,\n                        CharSequence suffix) {\n        Objects.requireNonNull(prefix, \"The prefix must not be null\");\n        Objects.requireNonNull(delimiter, \"The delimiter must not be null\");\n        Objects.requireNonNull(suffix, \"The suffix must not be null\");\n        // make defensive copies of arguments\n        this.prefix = prefix.toString();\n        this.delimiter = delimiter.toString();\n        this.suffix = suffix.toString();\n        this.emptyValue = this.prefix + this.suffix;\n    }\n\n    // 拼接\n    public StringJoiner add(CharSequence newElement) {\n        prepareBuilder().append(newElement);\n        return this;\n    }\n\n    // 预拼接value\n    private StringBuilder prepareBuilder() {\n        // value已加前缀\n        if (value != null) {\n            // 此时添加分隔符\n            value.append(delimiter);\n        } else {\n            // value未加前缀时需要先添加前缀\n            value = new StringBuilder().append(prefix);\n        }\n        return value;\n    }\n\n    //重写了toString 方法\n    @NonNull\n    @Override\n    public String toString() {\n        if (value == null) {\n            // value未进行任何字符拼接时反悔emptyValue\n            return emptyValue;\n        } else {\n            // 后缀为\"\"字符时，直接返回value\n            if (suffix.equals(\"\")) {\n                return value.toString();\n            } else {\n                // 获取value未拼接后缀的长度\n                int initialLength = value.length();\n                String result = value.append(suffix).toString();\n                // reset value to pre-append initialLength\n                // 此处是为了保证value.toString()为未拼接后缀前的字符串\n                value.setLength(initialLength);\n                return result;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/StringUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.annotation.SuppressLint;\nimport android.text.TextUtils;\nimport android.util.Base64;\nimport android.util.Log;\n\nimport androidx.annotation.StringRes;\n\nimport com.kunfei.bookshelf.MApplication;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.charset.StandardCharsets;\nimport java.text.DateFormat;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.HashMap;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.zip.DataFormatException;\nimport java.util.zip.Deflater;\nimport java.util.zip.Inflater;\n\nimport static android.text.TextUtils.isEmpty;\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class StringUtils {\n    private static final String TAG = \"StringUtils\";\n    private static final int HOUR_OF_DAY = 24;\n    private static final int DAY_OF_YESTERDAY = 2;\n    private static final int TIME_UNIT = 60;\n    private final static HashMap<Character, Integer> ChnMap = getChnMap();\n\n    //将时间转换成日期\n    public static String dateConvert(long time, String pattern) {\n        Date date = new Date(time);\n        @SuppressLint(\"SimpleDateFormat\") SimpleDateFormat format = new SimpleDateFormat(pattern);\n        return format.format(date);\n    }\n\n    //将日期转换成昨天、今天、明天\n    public static String dateConvert(String source, String pattern) {\n        @SuppressLint(\"SimpleDateFormat\") DateFormat format = new SimpleDateFormat(pattern);\n        Calendar calendar = Calendar.getInstance();\n        try {\n            Date date = format.parse(source);\n            long curTime = calendar.getTimeInMillis();\n            calendar.setTime(date);\n            //将MISC 转换成 sec\n            long difSec = Math.abs((curTime - date.getTime()) / 1000);\n            long difMin = difSec / 60;\n            long difHour = difMin / 60;\n            long difDate = difHour / 60;\n            int oldHour = calendar.get(Calendar.HOUR);\n            //如果没有时间\n            if (oldHour == 0) {\n                //比日期:昨天今天和明天\n                if (difDate == 0) {\n                    return \"今天\";\n                } else if (difDate < DAY_OF_YESTERDAY) {\n                    return \"昨天\";\n                } else {\n                    @SuppressLint(\"SimpleDateFormat\") DateFormat convertFormat = new SimpleDateFormat(\"yyyy-MM-dd\");\n                    return convertFormat.format(date);\n                }\n            }\n\n            if (difSec < TIME_UNIT) {\n                return difSec + \"秒前\";\n            } else if (difMin < TIME_UNIT) {\n                return difMin + \"分钟前\";\n            } else if (difHour < HOUR_OF_DAY) {\n                return difHour + \"小时前\";\n            } else if (difDate < DAY_OF_YESTERDAY) {\n                return \"昨天\";\n            } else {\n                @SuppressLint(\"SimpleDateFormat\") DateFormat convertFormat = new SimpleDateFormat(\"yyyy-MM-dd\");\n                return convertFormat.format(date);\n            }\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n        return \"\";\n    }\n\n    public static String toFirstCapital(String str) {\n        return str.substring(0, 1).toUpperCase() + str.substring(1);\n    }\n\n    public static String getString(@StringRes int id) {\n        return MApplication.getInstance().getResources().getString(id);\n    }\n\n    public static String getString(@StringRes int id, Object... formatArgs) {\n        return MApplication.getInstance().getString(id, formatArgs);\n    }\n\n    /**\n     * 将文本中的半角字符，转换成全角字符\n     */\n    public static String halfToFull(String input) {\n        char[] c = input.toCharArray();\n        for (int i = 0; i < c.length; i++) {\n            if (c[i] == 32) //半角空格\n            {\n                c[i] = (char) 12288;\n                continue;\n            }\n            //根据实际情况，过滤不需要转换的符号\n            //if (c[i] == 46) //半角点号，不转换\n            // continue;\n\n            if (c[i] > 32 && c[i] < 127)    //其他符号都转换为全角\n                c[i] = (char) (c[i] + 65248);\n        }\n        return new String(c);\n    }\n\n    //功能：字符串全角转换为半角\n    public static String fullToHalf(String input) {\n        char[] c = input.toCharArray();\n        for (int i = 0; i < c.length; i++) {\n            if (c[i] == 12288) //全角空格\n            {\n                c[i] = (char) 32;\n                continue;\n            }\n\n            if (c[i] > 65280 && c[i] < 65375)\n                c[i] = (char) (c[i] - 65248);\n        }\n        return new String(c);\n    }\n\n    private static HashMap<Character, Integer> getChnMap() {\n        HashMap<Character, Integer> map = new HashMap<>();\n        String cnStr = \"零一二三四五六七八九十\";\n        char[] c = cnStr.toCharArray();\n        for (int i = 0; i <= 10; i++) {\n            map.put(c[i], i);\n        }\n        cnStr = \"〇壹贰叁肆伍陆柒捌玖拾\";\n        c = cnStr.toCharArray();\n        for (int i = 0; i <= 10; i++) {\n            map.put(c[i], i);\n        }\n        map.put('两', 2);\n        map.put('百', 100);\n        map.put('佰', 100);\n        map.put('千', 1000);\n        map.put('仟', 1000);\n        map.put('万', 10000);\n        map.put('亿', 100000000);\n        return map;\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    public static int chineseNumToInt(String chNum) {\n        int result = 0;\n        int tmp = 0;\n        int billion = 0;\n        char[] cn = chNum.toCharArray();\n\n        // \"一零二五\" 形式\n        if (cn.length > 1 && chNum.matches(\"^[〇零一二三四五六七八九壹贰叁肆伍陆柒捌玖]$\")) {\n            for (int i = 0; i < cn.length; i++) {\n                cn[i] = (char) (48 + ChnMap.get(cn[i]));\n            }\n            return Integer.parseInt(new String(cn));\n        }\n\n        // \"一千零二十五\", \"一千二\" 形式\n        try {\n            for (int i = 0; i < cn.length; i++) {\n                int tmpNum = ChnMap.get(cn[i]);\n                if (tmpNum == 100000000) {\n                    result += tmp;\n                    result *= tmpNum;\n                    billion = billion * 100000000 + result;\n                    result = 0;\n                    tmp = 0;\n                } else if (tmpNum == 10000) {\n                    result += tmp;\n                    result *= tmpNum;\n                    tmp = 0;\n                } else if (tmpNum >= 10) {\n                    if (tmp == 0)\n                        tmp = 1;\n                    result += tmpNum * tmp;\n                    tmp = 0;\n                } else {\n                    if (i >= 2 && i == cn.length - 1 && ChnMap.get(cn[i - 1]) > 10)\n                        tmp = tmpNum * ChnMap.get(cn[i - 1]) / 10;\n                    else\n                        tmp = tmp * 10 + tmpNum;\n                }\n            }\n            result += tmp + billion;\n            return result;\n        } catch (Exception e) {\n            return -1;\n        }\n    }\n\n    public static int stringToInt(String str) {\n        if (str != null) {\n            String num = fullToHalf(str).replaceAll(\"\\\\s\", \"\");\n            try {\n                return Integer.parseInt(num);\n            } catch (Exception e) {\n                return chineseNumToInt(num);\n            }\n        }\n        return -1;\n    }\n\n    public static String base64Decode(String str) {\n        byte[] bytes = Base64.decode(str, Base64.DEFAULT);\n        try {\n            return new String(bytes, StandardCharsets.UTF_8);\n        } catch (Exception e) {\n            return new String(bytes);\n        }\n    }\n\n    public static String escape(String src) {\n        int i;\n        char j;\n        StringBuilder tmp = new StringBuilder();\n        tmp.ensureCapacity(src.length() * 6);\n        for (i = 0; i < src.length(); i++) {\n            j = src.charAt(i);\n            if (Character.isDigit(j) || Character.isLowerCase(j)\n                    || Character.isUpperCase(j))\n                tmp.append(j);\n            else if (j < 256) {\n                tmp.append(\"%\");\n                if (j < 16)\n                    tmp.append(\"0\");\n                tmp.append(Integer.toString(j, 16));\n            } else {\n                tmp.append(\"%u\");\n                tmp.append(Integer.toString(j, 16));\n            }\n        }\n        return tmp.toString();\n    }\n\n    public static boolean isJsonType(String str) {\n        boolean result = false;\n        if (!TextUtils.isEmpty(str)) {\n            str = str.trim();\n            if (str.startsWith(\"{\") && str.endsWith(\"}\")) {\n                result = true;\n            } else if (str.startsWith(\"[\") && str.endsWith(\"]\")) {\n                result = true;\n            }\n        }\n        return result;\n    }\n\n    public static boolean isCompressJsonType(String str) {\n        if (!TextUtils.isEmpty(str)) {\n            if (str.replaceAll(\"(\\\\s|\\n)*\", \"\").matches(\"^\\\\{.*[^}]$\")) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public static String unCompressJson(String str) {\n        if (TextUtils.isEmpty(str))\n            return \"\";\n        // 如果是未压缩的json\n        if (str.replaceAll(\"(\\\\s|\\n)*\", \"\").matches(\"^\\\\{.*\\\\}$\"))\n            return str;\n//        if (str.replaceAll(\"(\\\\s|\\n)*\",\"\").matches(\"^\\\\{.*[^}]$\"))\n        String string = null;\n        str = str.trim();\n        try {\n            if (str.charAt(0) == '{')\n                string = unzipString(str.substring(1));\n            else\n                string = unzipString(str);\n            if (string.charAt(string.length() - 1) == '}')\n                return \"{\" + string;\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return str;\n    }\n\n    public static boolean isJsonObject(String text) {\n        boolean result = false;\n        if (!TextUtils.isEmpty(text)) {\n            text = text.trim();\n            if (text.startsWith(\"{\") && text.endsWith(\"}\")) {\n                result = true;\n            }\n        }\n        return result;\n    }\n\n    public static boolean isJsonArray(String text) {\n        boolean result = false;\n        if (!TextUtils.isEmpty(text)) {\n            text = text.trim();\n            if (text.startsWith(\"[\") && text.endsWith(\"]\")) {\n                result = true;\n            }\n        }\n        return result;\n    }\n\n    public static boolean isTrimEmpty(String text) {\n        if (text == null) return true;\n        if (text.length() == 0) return true;\n        return text.trim().length() == 0;\n    }\n\n    public static boolean startWithIgnoreCase(String src, String obj) {\n        if (src == null || obj == null) return false;\n        if (obj.length() > src.length()) return false;\n        return src.substring(0, obj.length()).equalsIgnoreCase(obj);\n    }\n\n    public static boolean endWithIgnoreCase(String src, String obj) {\n        if (src == null || obj == null) return false;\n        if (obj.length() > src.length()) return false;\n        return src.substring(src.length() - obj.length()).equalsIgnoreCase(obj);\n    }\n\n    public static boolean isContainNumber(String company) {\n        Pattern p = Pattern.compile(\"[0-9]\");\n        Matcher m = p.matcher(company);\n        return m.find();\n    }\n\n    public static boolean isNumeric(String str) {\n        Pattern pattern = Pattern.compile(\"[0-9]*\");\n        Matcher isNum = pattern.matcher(str);\n        return isNum.matches();\n    }\n\n    public static String getBaseUrl(String url) {\n        if (url == null || !url.startsWith(\"http\")) return null;\n        int index = url.indexOf(\"/\", 9);\n        if (index == -1) {\n            return url;\n        }\n        return url.substring(0, index);\n    }\n\n    // 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格)\n    public static String trim(String s) {\n        if (isEmpty(s)) return \"\";\n        int start = 0, len = s.length();\n        int end = len - 1;\n        while ((start < end) && ((s.charAt(start) <= 0x20) || (s.charAt(start) == '　'))) {\n            ++start;\n        }\n        while ((start < end) && ((s.charAt(end) <= 0x20) || (s.charAt(end) == '　'))) {\n            --end;\n        }\n        if (end < len) ++end;\n        return ((start > 0) || (end < len)) ? s.substring(start, end) : s;\n    }\n\n    public static String repeat(String str, int n) {\n        StringBuilder stringBuilder = new StringBuilder();\n        for (int i = 0; i < n; i++) {\n            stringBuilder.append(str);\n        }\n        return stringBuilder.toString();\n    }\n\n    public static String removeUTFCharacters(String data) {\n        if (data == null) return null;\n        Pattern p = Pattern.compile(\"\\\\\\\\u(\\\\p{XDigit}{4})\");\n        Matcher m = p.matcher(data);\n        StringBuffer buf = new StringBuffer(data.length());\n        while (m.find()) {\n            String ch = String.valueOf((char) Integer.parseInt(m.group(1), 16));\n            m.appendReplacement(buf, Matcher.quoteReplacement(ch));\n        }\n        m.appendTail(buf);\n        return buf.toString();\n    }\n\n    public static String formatHtml(String html) {\n        if (TextUtils.isEmpty(html)) return \"\";\n        return html.replaceAll(\"(?i)<(br[\\\\s/]*|/?p[^>]*|/?div[^>]*)>\", \"\\n\")// 替换特定标签为换行符\n                //.replaceAll(\"<(script[^>]*>)?[^>]*>|&nbsp;\", \"\")// 删除script标签对和空格转义符\n                .replaceAll(\"</?[a-zA-Z][^>]*>\", \"\")// 删除标签对\n                .replaceAll(\"\\\\s*\\\\n+\\\\s*\", \"\\n　　\")// 移除空行,并增加段前缩进2个汉字\n                .replaceAll(\"^[\\\\n\\\\s]+\", \"　　\")//移除开头空行,并增加段前缩进2个汉字\n                .replaceAll(\"[\\\\n\\\\s]+$\", \"\");//移除尾部空行\n    }\n\n    public static String formatHtml2Intor(String html) {\n        if (TextUtils.isEmpty(html)) return \"\";\n        return \"　　\"\n                + html.replaceAll(\"(?i)<(br[\\\\s/]*|/?p[^>]*|/?div[^>]*)>\", \"\\n\")// 替换特定标签为换行符\n                .replaceAll(\"</?[a-zA-Z][^>]*>\", \"\")// 删除标签对\n                .replaceAll(\"\\\\s*\\\\n+\\\\s*\", \"\\n　　\")// 移除空行,并增加段前缩进2个汉字\n                .trim();\n    }\n\n    /**\n     * 压缩\n     */\n    public static String zipString(String unzipString) {\n        /*\n         *     https://www.yiibai.com/javazip/javazip_deflater.html#article-start\n         *     0 ~ 9 压缩等级 低到高\n         *     public static final int BEST_COMPRESSION = 9;            最佳压缩的压缩级别。\n         *     public static final int BEST_SPEED = 1;                  压缩级别最快的压缩。\n         *     public static final int DEFAULT_COMPRESSION = -1;        默认压缩级别。\n         *     public static final int DEFAULT_STRATEGY = 0;            默认压缩策略。\n         *     public static final int DEFLATED = 8;                    压缩算法的压缩方法(目前唯一支持的压缩方法)。\n         *     public static final int FILTERED = 1;                    压缩策略最适用于大部分数值较小且数据分布随机分布的数据。\n         *     public static final int FULL_FLUSH = 3;                  压缩刷新模式，用于清除所有待处理的输出并重置拆卸器。\n         *     public static final int HUFFMAN_ONLY = 2;                仅用于霍夫曼编码的压缩策略。\n         *     public static final int NO_COMPRESSION = 0;              不压缩的压缩级别。\n         *     public static final int NO_FLUSH = 0;                    用于实现最佳压缩结果的压缩刷新模式。\n         *     public static final int SYNC_FLUSH = 2;                  用于清除所有未决输出的压缩刷新模式; 可能会降低某些压缩算法的压缩率。\n         */\n\n        try {\n            //使用指定的压缩级别创建一个新的压缩器。\n            Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);\n            //设置压缩输入数据。\n            deflater.setInput(unzipString.getBytes(StandardCharsets.UTF_8));\n            //当被调用时，表示压缩应该以输入缓冲区的当前内容结束。\n            deflater.finish();\n\n            final byte[] bytes = new byte[512];\n            ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512);\n\n            while (!deflater.finished()) {\n                //压缩输入数据并用压缩数据填充指定的缓冲区。\n                int length = deflater.deflate(bytes);\n                outputStream.write(bytes, 0, length);\n            }\n            //关闭压缩器并丢弃任何未处理的输入。\n            deflater.end();\n            String zipString = new String(Base64.encode(outputStream.toByteArray(), Base64.DEFAULT), StandardCharsets.UTF_8);\n\n            Log.d(\"zipString()压缩比\", \"char:\" + zipString.length() + \"/\" + unzipString.length() + \"=\" + zipString.length() /(float)(unzipString.length())  +\n                    \"\\tbyte:\" + zipString.getBytes(\"utf-8\").length + \"/\" + unzipString.getBytes(\"utf-8\").length\n                    + \"=\" + zipString.getBytes(\"UTF-8\").length /(float) unzipString.getBytes(\"utf-8\").length);\n            return zipString.trim();\n\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return \"\";\n        //处理回车符\n//        return zipString.replaceAll(\"[\\r\\n]\", \"\");\n    }\n\n    /**\n     * 解压缩\n     */\n    public static String unzipString(String zipString) {\n        byte[] decode //= Base64.decodeBase64(zipString);\n                = Base64.decode(zipString, Base64.DEFAULT);\n        //创建一个新的解压缩器  https://www.yiibai.com/javazip/javazip_inflater.html\n\n        Inflater inflater = new Inflater();\n        //设置解压缩的输入数据。\n        inflater.setInput(decode);\n        final byte[] bytes = new byte[512];\n        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(512);\n        try {\n            //finished() 如果已到达压缩数据流的末尾，则返回true。\n            while (!inflater.finished()) {\n                //将字节解压缩到指定的缓冲区中。\n                int length = inflater.inflate(bytes);\n                outputStream.write(bytes, 0, length);\n            }\n        } catch (DataFormatException e) {\n            e.printStackTrace();\n            return null;\n        } finally {\n            //关闭解压缩器并丢弃任何未处理的输入。\n            inflater.end();\n        }\n\n        try {\n            return outputStream.toString(\"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/SystemUtil.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.PowerManager;\nimport android.provider.Settings;\n\nimport static android.content.Context.POWER_SERVICE;\n\npublic class SystemUtil {\n\n    public static int getScreenOffTime(Context context) {\n        int screenOffTime = 0;\n        try {\n            screenOffTime = Settings.System.getInt(context.getContentResolver(),\n                    Settings.System.SCREEN_OFF_TIMEOUT);\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        return screenOffTime;\n    }\n\n    public static void ignoreBatteryOptimization(Activity activity) {\n        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) return;\n\n        PowerManager powerManager = (PowerManager) activity.getSystemService(POWER_SERVICE);\n        boolean hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());\n        //  判断当前APP是否有加入电池优化的白名单，如果没有，弹出加入电池优化的白名单的设置对话框。\n        if (!hasIgnored) {\n            try {\n                @SuppressLint(\"BatteryLife\")\n                Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);\n                intent.setData(Uri.parse(\"package:\" + activity.getPackageName()));\n                activity.startActivity(intent);\n            } catch (Throwable ignored) {\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/TimeUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.constant.TimeConstants;\n\nimport java.text.DateFormat;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.Locale;\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic final class TimeUtils {\n\n    private static final ThreadLocal<SimpleDateFormat> SDF_THREAD_LOCAL = new ThreadLocal<>();\n\n    private static SimpleDateFormat getDefaultFormat() {\n        SimpleDateFormat simpleDateFormat = SDF_THREAD_LOCAL.get();\n        if (simpleDateFormat == null) {\n            simpleDateFormat = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\", Locale.getDefault());\n            SDF_THREAD_LOCAL.set(simpleDateFormat);\n        }\n        return simpleDateFormat;\n    }\n\n    private TimeUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * Milliseconds to the formatted time string.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param millis The milliseconds.\n     * @return the formatted time string\n     */\n    public static String millis2String(final long millis) {\n        return millis2String(millis, getDefaultFormat());\n    }\n\n    /**\n     * Milliseconds to the formatted time string.\n     *\n     * @param millis The milliseconds.\n     * @param format The format.\n     * @return the formatted time string\n     */\n    public static String millis2String(final long millis, @NonNull final DateFormat format) {\n        return format.format(new Date(millis));\n    }\n\n    /**\n     * Formatted time string to the milliseconds.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return the milliseconds\n     */\n    public static long string2Millis(final String time) {\n        return string2Millis(time, getDefaultFormat());\n    }\n\n    /**\n     * Formatted time string to the milliseconds.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return the milliseconds\n     */\n    public static long string2Millis(final String time, @NonNull final DateFormat format) {\n        try {\n            return format.parse(time).getTime();\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n        return -1;\n    }\n\n    /**\n     * Formatted time string to the date.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return the date\n     */\n    public static Date string2Date(final String time) {\n        return string2Date(time, getDefaultFormat());\n    }\n\n    /**\n     * Formatted time string to the date.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return the date\n     */\n    public static Date string2Date(final String time, @NonNull final DateFormat format) {\n        try {\n            return format.parse(time);\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    /**\n     * Date to the formatted time string.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param date The date.\n     * @return the formatted time string\n     */\n    public static String date2String(final Date date) {\n        return date2String(date, getDefaultFormat());\n    }\n\n    /**\n     * Date to the formatted time string.\n     *\n     * @param date   The date.\n     * @param format The format.\n     * @return the formatted time string\n     */\n    public static String date2String(final Date date, @NonNull final DateFormat format) {\n        return format.format(date);\n    }\n\n    /**\n     * Date to the milliseconds.\n     *\n     * @param date The date.\n     * @return the milliseconds\n     */\n    public static long date2Millis(final Date date) {\n        return date.getTime();\n    }\n\n    /**\n     * Milliseconds to the date.\n     *\n     * @param millis The milliseconds.\n     * @return the date\n     */\n    public static Date millis2Date(final long millis) {\n        return new Date(millis);\n    }\n\n    /**\n     * Return the time span, in unit.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time1 The first formatted time string.\n     * @param time2 The second formatted time string.\n     * @param unit  The unit of time span.\n     *              <ul>\n     *              <li>{@link TimeConstants#MSEC}</li>\n     *              <li>{@link TimeConstants#SEC }</li>\n     *              <li>{@link TimeConstants#MIN }</li>\n     *              <li>{@link TimeConstants#HOUR}</li>\n     *              <li>{@link TimeConstants#DAY }</li>\n     *              </ul>\n     * @return the time span, in unit\n     */\n    public static long getTimeSpan(final String time1,\n                                   final String time2,\n                                   @TimeConstants.Unit final int unit) {\n        return getTimeSpan(time1, time2, getDefaultFormat(), unit);\n    }\n\n    /**\n     * Return the time span, in unit.\n     *\n     * @param time1  The first formatted time string.\n     * @param time2  The second formatted time string.\n     * @param format The format.\n     * @param unit   The unit of time span.\n     *               <ul>\n     *               <li>{@link TimeConstants#MSEC}</li>\n     *               <li>{@link TimeConstants#SEC }</li>\n     *               <li>{@link TimeConstants#MIN }</li>\n     *               <li>{@link TimeConstants#HOUR}</li>\n     *               <li>{@link TimeConstants#DAY }</li>\n     *               </ul>\n     * @return the time span, in unit\n     */\n    public static long getTimeSpan(final String time1,\n                                   final String time2,\n                                   @NonNull final DateFormat format,\n                                   @TimeConstants.Unit final int unit) {\n        return millis2TimeSpan(string2Millis(time1, format) - string2Millis(time2, format), unit);\n    }\n\n    /**\n     * Return the time span, in unit.\n     *\n     * @param date1 The first date.\n     * @param date2 The second date.\n     * @param unit  The unit of time span.\n     *              <ul>\n     *              <li>{@link TimeConstants#MSEC}</li>\n     *              <li>{@link TimeConstants#SEC }</li>\n     *              <li>{@link TimeConstants#MIN }</li>\n     *              <li>{@link TimeConstants#HOUR}</li>\n     *              <li>{@link TimeConstants#DAY }</li>\n     *              </ul>\n     * @return the time span, in unit\n     */\n    public static long getTimeSpan(final Date date1,\n                                   final Date date2,\n                                   @TimeConstants.Unit final int unit) {\n        return millis2TimeSpan(date2Millis(date1) - date2Millis(date2), unit);\n    }\n\n    /**\n     * Return the time span, in unit.\n     *\n     * @param millis1 The first milliseconds.\n     * @param millis2 The second milliseconds.\n     * @param unit    The unit of time span.\n     *                <ul>\n     *                <li>{@link TimeConstants#MSEC}</li>\n     *                <li>{@link TimeConstants#SEC }</li>\n     *                <li>{@link TimeConstants#MIN }</li>\n     *                <li>{@link TimeConstants#HOUR}</li>\n     *                <li>{@link TimeConstants#DAY }</li>\n     *                </ul>\n     * @return the time span, in unit\n     */\n    public static long getTimeSpan(final long millis1,\n                                   final long millis2,\n                                   @TimeConstants.Unit final int unit) {\n        return millis2TimeSpan(millis1 - millis2, unit);\n    }\n\n    /**\n     * Return the fit time span.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time1     The first formatted time string.\n     * @param time2     The second formatted time string.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0, return null</li>\n     *                  <li>precision = 1, return 天</li>\n     *                  <li>precision = 2, return 天, 小时</li>\n     *                  <li>precision = 3, return 天, 小时, 分钟</li>\n     *                  <li>precision = 4, return 天, 小时, 分钟, 秒</li>\n     *                  <li>precision &gt;= 5，return 天, 小时, 分钟, 秒, 毫秒</li>\n     *                  </ul>\n     * @return the fit time span\n     */\n    public static String getFitTimeSpan(final String time1,\n                                        final String time2,\n                                        final int precision) {\n        long delta = string2Millis(time1, getDefaultFormat()) - string2Millis(time2, getDefaultFormat());\n        return millis2FitTimeSpan(delta, precision);\n    }\n\n    /**\n     * Return the fit time span.\n     *\n     * @param time1     The first formatted time string.\n     * @param time2     The second formatted time string.\n     * @param format    The format.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0, return null</li>\n     *                  <li>precision = 1, return 天</li>\n     *                  <li>precision = 2, return 天, 小时</li>\n     *                  <li>precision = 3, return 天, 小时, 分钟</li>\n     *                  <li>precision = 4, return 天, 小时, 分钟, 秒</li>\n     *                  <li>precision &gt;= 5，return 天, 小时, 分钟, 秒, 毫秒</li>\n     *                  </ul>\n     * @return the fit time span\n     */\n    public static String getFitTimeSpan(final String time1,\n                                        final String time2,\n                                        @NonNull final DateFormat format,\n                                        final int precision) {\n        long delta = string2Millis(time1, format) - string2Millis(time2, format);\n        return millis2FitTimeSpan(delta, precision);\n    }\n\n    /**\n     * Return the fit time span.\n     *\n     * @param date1     The first date.\n     * @param date2     The second date.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0, return null</li>\n     *                  <li>precision = 1, return 天</li>\n     *                  <li>precision = 2, return 天, 小时</li>\n     *                  <li>precision = 3, return 天, 小时, 分钟</li>\n     *                  <li>precision = 4, return 天, 小时, 分钟, 秒</li>\n     *                  <li>precision &gt;= 5，return 天, 小时, 分钟, 秒, 毫秒</li>\n     *                  </ul>\n     * @return the fit time span\n     */\n    public static String getFitTimeSpan(final Date date1, final Date date2, final int precision) {\n        return millis2FitTimeSpan(date2Millis(date1) - date2Millis(date2), precision);\n    }\n\n    /**\n     * Return the fit time span.\n     *\n     * @param millis1   The first milliseconds.\n     * @param millis2   The second milliseconds.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0, return null</li>\n     *                  <li>precision = 1, return 天</li>\n     *                  <li>precision = 2, return 天, 小时</li>\n     *                  <li>precision = 3, return 天, 小时, 分钟</li>\n     *                  <li>precision = 4, return 天, 小时, 分钟, 秒</li>\n     *                  <li>precision &gt;= 5，return 天, 小时, 分钟, 秒, 毫秒</li>\n     *                  </ul>\n     * @return the fit time span\n     */\n    public static String getFitTimeSpan(final long millis1,\n                                        final long millis2,\n                                        final int precision) {\n        return millis2FitTimeSpan(millis1 - millis2, precision);\n    }\n\n    /**\n     * Return the current time in milliseconds.\n     *\n     * @return the current time in milliseconds\n     */\n    public static long getNowMills() {\n        return System.currentTimeMillis();\n    }\n\n    /**\n     * Return the current formatted time string.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @return the current formatted time string\n     */\n    public static String getNowString() {\n        return millis2String(System.currentTimeMillis(), getDefaultFormat());\n    }\n\n    /**\n     * Return the current formatted time string.\n     *\n     * @param format The format.\n     * @return the current formatted time string\n     */\n    public static String getNowString(@NonNull final DateFormat format) {\n        return millis2String(System.currentTimeMillis(), format);\n    }\n\n    /**\n     * Return the current date.\n     *\n     * @return the current date\n     */\n    public static Date getNowDate() {\n        return new Date();\n    }\n\n    /**\n     * Return the time span by now, in unit.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @param unit The unit of time span.\n     *             <ul>\n     *             <li>{@link TimeConstants#MSEC}</li>\n     *             <li>{@link TimeConstants#SEC }</li>\n     *             <li>{@link TimeConstants#MIN }</li>\n     *             <li>{@link TimeConstants#HOUR}</li>\n     *             <li>{@link TimeConstants#DAY }</li>\n     *             </ul>\n     * @return the time span by now, in unit\n     */\n    public static long getTimeSpanByNow(final String time, @TimeConstants.Unit final int unit) {\n        return getTimeSpan(time, getNowString(), getDefaultFormat(), unit);\n    }\n\n    /**\n     * Return the time span by now, in unit.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @param unit   The unit of time span.\n     *               <ul>\n     *               <li>{@link TimeConstants#MSEC}</li>\n     *               <li>{@link TimeConstants#SEC }</li>\n     *               <li>{@link TimeConstants#MIN }</li>\n     *               <li>{@link TimeConstants#HOUR}</li>\n     *               <li>{@link TimeConstants#DAY }</li>\n     *               </ul>\n     * @return the time span by now, in unit\n     */\n    public static long getTimeSpanByNow(final String time,\n                                        @NonNull final DateFormat format,\n                                        @TimeConstants.Unit final int unit) {\n        return getTimeSpan(time, getNowString(format), format, unit);\n    }\n\n    /**\n     * Return the time span by now, in unit.\n     *\n     * @param date The date.\n     * @param unit The unit of time span.\n     *             <ul>\n     *             <li>{@link TimeConstants#MSEC}</li>\n     *             <li>{@link TimeConstants#SEC }</li>\n     *             <li>{@link TimeConstants#MIN }</li>\n     *             <li>{@link TimeConstants#HOUR}</li>\n     *             <li>{@link TimeConstants#DAY }</li>\n     *             </ul>\n     * @return the time span by now, in unit\n     */\n    public static long getTimeSpanByNow(final Date date, @TimeConstants.Unit final int unit) {\n        return getTimeSpan(date, new Date(), unit);\n    }\n\n    /**\n     * Return the time span by now, in unit.\n     *\n     * @param millis The milliseconds.\n     * @param unit   The unit of time span.\n     *               <ul>\n     *               <li>{@link TimeConstants#MSEC}</li>\n     *               <li>{@link TimeConstants#SEC }</li>\n     *               <li>{@link TimeConstants#MIN }</li>\n     *               <li>{@link TimeConstants#HOUR}</li>\n     *               <li>{@link TimeConstants#DAY }</li>\n     *               </ul>\n     * @return the time span by now, in unit\n     */\n    public static long getTimeSpanByNow(final long millis, @TimeConstants.Unit final int unit) {\n        return getTimeSpan(millis, System.currentTimeMillis(), unit);\n    }\n\n    /**\n     * Return the fit time span by now.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time      The formatted time string.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0，返回 null</li>\n     *                  <li>precision = 1，返回天</li>\n     *                  <li>precision = 2，返回天和小时</li>\n     *                  <li>precision = 3，返回天、小时和分钟</li>\n     *                  <li>precision = 4，返回天、小时、分钟和秒</li>\n     *                  <li>precision &gt;= 5，返回天、小时、分钟、秒和毫秒</li>\n     *                  </ul>\n     * @return the fit time span by now\n     */\n    public static String getFitTimeSpanByNow(final String time, final int precision) {\n        return getFitTimeSpan(time, getNowString(), getDefaultFormat(), precision);\n    }\n\n    /**\n     * Return the fit time span by now.\n     *\n     * @param time      The formatted time string.\n     * @param format    The format.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0，返回 null</li>\n     *                  <li>precision = 1，返回天</li>\n     *                  <li>precision = 2，返回天和小时</li>\n     *                  <li>precision = 3，返回天、小时和分钟</li>\n     *                  <li>precision = 4，返回天、小时、分钟和秒</li>\n     *                  <li>precision &gt;= 5，返回天、小时、分钟、秒和毫秒</li>\n     *                  </ul>\n     * @return the fit time span by now\n     */\n    public static String getFitTimeSpanByNow(final String time,\n                                             @NonNull final DateFormat format,\n                                             final int precision) {\n        return getFitTimeSpan(time, getNowString(format), format, precision);\n    }\n\n    /**\n     * Return the fit time span by now.\n     *\n     * @param date      The date.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0，返回 null</li>\n     *                  <li>precision = 1，返回天</li>\n     *                  <li>precision = 2，返回天和小时</li>\n     *                  <li>precision = 3，返回天、小时和分钟</li>\n     *                  <li>precision = 4，返回天、小时、分钟和秒</li>\n     *                  <li>precision &gt;= 5，返回天、小时、分钟、秒和毫秒</li>\n     *                  </ul>\n     * @return the fit time span by now\n     */\n    public static String getFitTimeSpanByNow(final Date date, final int precision) {\n        return getFitTimeSpan(date, getNowDate(), precision);\n    }\n\n    /**\n     * Return the fit time span by now.\n     *\n     * @param millis    The milliseconds.\n     * @param precision The precision of time span.\n     *                  <ul>\n     *                  <li>precision = 0，返回 null</li>\n     *                  <li>precision = 1，返回天</li>\n     *                  <li>precision = 2，返回天和小时</li>\n     *                  <li>precision = 3，返回天、小时和分钟</li>\n     *                  <li>precision = 4，返回天、小时、分钟和秒</li>\n     *                  <li>precision &gt;= 5，返回天、小时、分钟、秒和毫秒</li>\n     *                  </ul>\n     * @return the fit time span by now\n     */\n    public static String getFitTimeSpanByNow(final long millis, final int precision) {\n        return getFitTimeSpan(millis, System.currentTimeMillis(), precision);\n    }\n\n    /**\n     * Return the friendly time span by now.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return the friendly time span by now\n     * <ul>\n     * <li>如果小于 1 秒钟内，显示刚刚</li>\n     * <li>如果在 1 分钟内，显示 XXX秒前</li>\n     * <li>如果在 1 小时内，显示 XXX分钟前</li>\n     * <li>如果在 1 小时外的今天内，显示今天15:32</li>\n     * <li>如果是昨天的，显示昨天15:32</li>\n     * <li>其余显示，2016-10-15</li>\n     * <li>时间不合法的情况全部日期和时间信息，如星期六 十月 27 14:21:20 CST 2007</li>\n     * </ul>\n     */\n    public static String getFriendlyTimeSpanByNow(final String time) {\n        return getFriendlyTimeSpanByNow(time, getDefaultFormat());\n    }\n\n    /**\n     * Return the friendly time span by now.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return the friendly time span by now\n     * <ul>\n     * <li>如果小于 1 秒钟内，显示刚刚</li>\n     * <li>如果在 1 分钟内，显示 XXX秒前</li>\n     * <li>如果在 1 小时内，显示 XXX分钟前</li>\n     * <li>如果在 1 小时外的今天内，显示今天15:32</li>\n     * <li>如果是昨天的，显示昨天15:32</li>\n     * <li>其余显示，2016-10-15</li>\n     * <li>时间不合法的情况全部日期和时间信息，如星期六 十月 27 14:21:20 CST 2007</li>\n     * </ul>\n     */\n    public static String getFriendlyTimeSpanByNow(final String time,\n                                                  @NonNull final DateFormat format) {\n        return getFriendlyTimeSpanByNow(string2Millis(time, format));\n    }\n\n    /**\n     * Return the friendly time span by now.\n     *\n     * @param date The date.\n     * @return the friendly time span by now\n     * <ul>\n     * <li>如果小于 1 秒钟内，显示刚刚</li>\n     * <li>如果在 1 分钟内，显示 XXX秒前</li>\n     * <li>如果在 1 小时内，显示 XXX分钟前</li>\n     * <li>如果在 1 小时外的今天内，显示今天15:32</li>\n     * <li>如果是昨天的，显示昨天15:32</li>\n     * <li>其余显示，2016-10-15</li>\n     * <li>时间不合法的情况全部日期和时间信息，如星期六 十月 27 14:21:20 CST 2007</li>\n     * </ul>\n     */\n    public static String getFriendlyTimeSpanByNow(final Date date) {\n        return getFriendlyTimeSpanByNow(date.getTime());\n    }\n\n    /**\n     * Return the friendly time span by now.\n     *\n     * @param millis The milliseconds.\n     * @return the friendly time span by now\n     * <ul>\n     * <li>如果小于 1 秒钟内，显示刚刚</li>\n     * <li>如果在 1 分钟内，显示 XXX秒前</li>\n     * <li>如果在 1 小时内，显示 XXX分钟前</li>\n     * <li>如果在 1 小时外的今天内，显示今天15:32</li>\n     * <li>如果是昨天的，显示昨天15:32</li>\n     * <li>其余显示，2016-10-15</li>\n     * <li>时间不合法的情况全部日期和时间信息，如星期六 十月 27 14:21:20 CST 2007</li>\n     * </ul>\n     */\n    public static String getFriendlyTimeSpanByNow(final long millis) {\n        long now = System.currentTimeMillis();\n        long span = now - millis;\n        if (span < 0)\n            // U can read http://www.apihome.cn/api/java/Formatter.html to understand it.\n            return String.format(\"%tc\", millis);\n        if (span < 1000) {\n            return \"刚刚\";\n        } else if (span < TimeConstants.MIN) {\n            return String.format(Locale.getDefault(), \"%d秒前\", span / TimeConstants.SEC);\n        } else if (span < TimeConstants.HOUR) {\n            return String.format(Locale.getDefault(), \"%d分钟前\", span / TimeConstants.MIN);\n        }\n        // 获取当天 00:00\n        long wee = getWeeOfToday();\n        if (millis >= wee) {\n            return String.format(\"今天%tR\", millis);\n        } else if (millis >= wee - TimeConstants.DAY) {\n            return String.format(\"昨天%tR\", millis);\n        } else {\n            return String.format(\"%tF\", millis);\n        }\n    }\n\n    private static long getWeeOfToday() {\n        Calendar cal = Calendar.getInstance();\n        cal.set(Calendar.HOUR_OF_DAY, 0);\n        cal.set(Calendar.SECOND, 0);\n        cal.set(Calendar.MINUTE, 0);\n        cal.set(Calendar.MILLISECOND, 0);\n        return cal.getTimeInMillis();\n    }\n\n    /**\n     * Return the milliseconds differ time span.\n     *\n     * @param millis   The milliseconds.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the milliseconds differ time span\n     */\n    public static long getMillis(final long millis,\n                                 final long timeSpan,\n                                 @TimeConstants.Unit final int unit) {\n        return millis + timeSpan2Millis(timeSpan, unit);\n    }\n\n    /**\n     * Return the milliseconds differ time span.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time     The formatted time string.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the milliseconds differ time span\n     */\n    public static long getMillis(final String time,\n                                 final long timeSpan,\n                                 @TimeConstants.Unit final int unit) {\n        return getMillis(time, getDefaultFormat(), timeSpan, unit);\n    }\n\n    /**\n     * Return the milliseconds differ time span.\n     *\n     * @param time     The formatted time string.\n     * @param format   The format.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the milliseconds differ time span.\n     */\n    public static long getMillis(final String time,\n                                 @NonNull final DateFormat format,\n                                 final long timeSpan,\n                                 @TimeConstants.Unit final int unit) {\n        return string2Millis(time, format) + timeSpan2Millis(timeSpan, unit);\n    }\n\n    /**\n     * Return the milliseconds differ time span.\n     *\n     * @param date     The date.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the milliseconds differ time span.\n     */\n    public static long getMillis(final Date date,\n                                 final long timeSpan,\n                                 @TimeConstants.Unit final int unit) {\n        return date2Millis(date) + timeSpan2Millis(timeSpan, unit);\n    }\n\n    /**\n     * Return the formatted time string differ time span.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param millis   The milliseconds.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span\n     */\n    public static String getString(final long millis,\n                                   final long timeSpan,\n                                   @TimeConstants.Unit final int unit) {\n        return getString(millis, getDefaultFormat(), timeSpan, unit);\n    }\n\n    /**\n     * Return the formatted time string differ time span.\n     *\n     * @param millis   The milliseconds.\n     * @param format   The format.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span\n     */\n    public static String getString(final long millis,\n                                   @NonNull final DateFormat format,\n                                   final long timeSpan,\n                                   @TimeConstants.Unit final int unit) {\n        return millis2String(millis + timeSpan2Millis(timeSpan, unit), format);\n    }\n\n    /**\n     * Return the formatted time string differ time span.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time     The formatted time string.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span\n     */\n    public static String getString(final String time,\n                                   final long timeSpan,\n                                   @TimeConstants.Unit final int unit) {\n        return getString(time, getDefaultFormat(), timeSpan, unit);\n    }\n\n    /**\n     * Return the formatted time string differ time span.\n     *\n     * @param time     The formatted time string.\n     * @param format   The format.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span\n     */\n    public static String getString(final String time,\n                                   @NonNull final DateFormat format,\n                                   final long timeSpan,\n                                   @TimeConstants.Unit final int unit) {\n        return millis2String(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit), format);\n    }\n\n    /**\n     * Return the formatted time string differ time span.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param date     The date.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span\n     */\n    public static String getString(final Date date,\n                                   final long timeSpan,\n                                   @TimeConstants.Unit final int unit) {\n        return getString(date, getDefaultFormat(), timeSpan, unit);\n    }\n\n    /**\n     * Return the formatted time string differ time span.\n     *\n     * @param date     The date.\n     * @param format   The format.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span\n     */\n    public static String getString(final Date date,\n                                   @NonNull final DateFormat format,\n                                   final long timeSpan,\n                                   @TimeConstants.Unit final int unit) {\n        return millis2String(date2Millis(date) + timeSpan2Millis(timeSpan, unit), format);\n    }\n\n    /**\n     * Return the date differ time span.\n     *\n     * @param millis   The milliseconds.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the date differ time span\n     */\n    public static Date getDate(final long millis,\n                               final long timeSpan,\n                               @TimeConstants.Unit final int unit) {\n        return millis2Date(millis + timeSpan2Millis(timeSpan, unit));\n    }\n\n    /**\n     * Return the date differ time span.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time     The formatted time string.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the date differ time span\n     */\n    public static Date getDate(final String time,\n                               final long timeSpan,\n                               @TimeConstants.Unit final int unit) {\n        return getDate(time, getDefaultFormat(), timeSpan, unit);\n    }\n\n    /**\n     * Return the date differ time span.\n     *\n     * @param time     The formatted time string.\n     * @param format   The format.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the date differ time span\n     */\n    public static Date getDate(final String time,\n                               @NonNull final DateFormat format,\n                               final long timeSpan,\n                               @TimeConstants.Unit final int unit) {\n        return millis2Date(string2Millis(time, format) + timeSpan2Millis(timeSpan, unit));\n    }\n\n    /**\n     * Return the date differ time span.\n     *\n     * @param date     The date.\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the date differ time span\n     */\n    public static Date getDate(final Date date,\n                               final long timeSpan,\n                               @TimeConstants.Unit final int unit) {\n        return millis2Date(date2Millis(date) + timeSpan2Millis(timeSpan, unit));\n    }\n\n    /**\n     * Return the milliseconds differ time span by now.\n     *\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the milliseconds differ time span by now\n     */\n    public static long getMillisByNow(final long timeSpan, @TimeConstants.Unit final int unit) {\n        return getMillis(getNowMills(), timeSpan, unit);\n    }\n\n    /**\n     * Return the formatted time string differ time span by now.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span by now\n     */\n    public static String getStringByNow(final long timeSpan, @TimeConstants.Unit final int unit) {\n        return getStringByNow(timeSpan, getDefaultFormat(), unit);\n    }\n\n    /**\n     * Return the formatted time string differ time span by now.\n     *\n     * @param timeSpan The time span.\n     * @param format   The format.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the formatted time string differ time span by now\n     */\n    public static String getStringByNow(final long timeSpan,\n                                        @NonNull final DateFormat format,\n                                        @TimeConstants.Unit final int unit) {\n        return getString(getNowMills(), format, timeSpan, unit);\n    }\n\n    /**\n     * Return the date differ time span by now.\n     *\n     * @param timeSpan The time span.\n     * @param unit     The unit of time span.\n     *                 <ul>\n     *                 <li>{@link TimeConstants#MSEC}</li>\n     *                 <li>{@link TimeConstants#SEC }</li>\n     *                 <li>{@link TimeConstants#MIN }</li>\n     *                 <li>{@link TimeConstants#HOUR}</li>\n     *                 <li>{@link TimeConstants#DAY }</li>\n     *                 </ul>\n     * @return the date differ time span by now\n     */\n    public static Date getDateByNow(final long timeSpan, @TimeConstants.Unit final int unit) {\n        return getDate(getNowMills(), timeSpan, unit);\n    }\n\n    /**\n     * Return whether it is today.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isToday(final String time) {\n        return isToday(string2Millis(time, getDefaultFormat()));\n    }\n\n    /**\n     * Return whether it is today.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isToday(final String time, @NonNull final DateFormat format) {\n        return isToday(string2Millis(time, format));\n    }\n\n    /**\n     * Return whether it is today.\n     *\n     * @param date The date.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isToday(final Date date) {\n        return isToday(date.getTime());\n    }\n\n    /**\n     * Return whether it is today.\n     *\n     * @param millis The milliseconds.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isToday(final long millis) {\n        long wee = getWeeOfToday();\n        return millis >= wee && millis < wee + TimeConstants.DAY;\n    }\n\n    /**\n     * Return whether it is leap year.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isLeapYear(final String time) {\n        return isLeapYear(string2Date(time, getDefaultFormat()));\n    }\n\n    /**\n     * Return whether it is leap year.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isLeapYear(final String time, @NonNull final DateFormat format) {\n        return isLeapYear(string2Date(time, format));\n    }\n\n    /**\n     * Return whether it is leap year.\n     *\n     * @param date The date.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isLeapYear(final Date date) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        int year = cal.get(Calendar.YEAR);\n        return isLeapYear(year);\n    }\n\n    /**\n     * Return whether it is leap year.\n     *\n     * @param millis The milliseconds.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isLeapYear(final long millis) {\n        return isLeapYear(millis2Date(millis));\n    }\n\n    /**\n     * Return whether it is leap year.\n     *\n     * @param year The year.\n     * @return {@code true}: yes<br>{@code false}: no\n     */\n    public static boolean isLeapYear(final int year) {\n        return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;\n    }\n\n    /**\n     * Return the day of week in Chinese.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return the day of week in Chinese\n     */\n    public static String getChineseWeek(final String time) {\n        return getChineseWeek(string2Date(time, getDefaultFormat()));\n    }\n\n    /**\n     * Return the day of week in Chinese.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return the day of week in Chinese\n     */\n    public static String getChineseWeek(final String time, @NonNull final DateFormat format) {\n        return getChineseWeek(string2Date(time, format));\n    }\n\n    /**\n     * Return the day of week in Chinese.\n     *\n     * @param date The date.\n     * @return the day of week in Chinese\n     */\n    public static String getChineseWeek(final Date date) {\n        return new SimpleDateFormat(\"E\", Locale.CHINA).format(date);\n    }\n\n    /**\n     * Return the day of week in Chinese.\n     *\n     * @param millis The milliseconds.\n     * @return the day of week in Chinese\n     */\n    public static String getChineseWeek(final long millis) {\n        return getChineseWeek(new Date(millis));\n    }\n\n    /**\n     * Return the day of week in US.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return the day of week in US\n     */\n    public static String getUSWeek(final String time) {\n        return getUSWeek(string2Date(time, getDefaultFormat()));\n    }\n\n    /**\n     * Return the day of week in US.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return the day of week in US\n     */\n    public static String getUSWeek(final String time, @NonNull final DateFormat format) {\n        return getUSWeek(string2Date(time, format));\n    }\n\n    /**\n     * Return the day of week in US.\n     *\n     * @param date The date.\n     * @return the day of week in US\n     */\n    public static String getUSWeek(final Date date) {\n        return new SimpleDateFormat(\"EEEE\", Locale.US).format(date);\n    }\n\n    /**\n     * Return the day of week in US.\n     *\n     * @param millis The milliseconds.\n     * @return the day of week in US\n     */\n    public static String getUSWeek(final long millis) {\n        return getUSWeek(new Date(millis));\n    }\n\n    /**\n     * Returns the value of the given calendar field.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time  The formatted time string.\n     * @param field The given calendar field.\n     *              <ul>\n     *              <li>{@link Calendar#ERA}</li>\n     *              <li>{@link Calendar#YEAR}</li>\n     *              <li>{@link Calendar#MONTH}</li>\n     *              <li>...</li>\n     *              <li>{@link Calendar#DST_OFFSET}</li>\n     *              </ul>\n     * @return the value of the given calendar field\n     */\n    public static int getValueByCalendarField(final String time, final int field) {\n        return getValueByCalendarField(string2Date(time, getDefaultFormat()), field);\n    }\n\n    /**\n     * Returns the value of the given calendar field.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @param field  The given calendar field.\n     *               <ul>\n     *               <li>{@link Calendar#ERA}</li>\n     *               <li>{@link Calendar#YEAR}</li>\n     *               <li>{@link Calendar#MONTH}</li>\n     *               <li>...</li>\n     *               <li>{@link Calendar#DST_OFFSET}</li>\n     *               </ul>\n     * @return the value of the given calendar field\n     */\n    public static int getValueByCalendarField(final String time,\n                                              @NonNull final DateFormat format,\n                                              final int field) {\n        return getValueByCalendarField(string2Date(time, format), field);\n    }\n\n    /**\n     * Returns the value of the given calendar field.\n     *\n     * @param date  The date.\n     * @param field The given calendar field.\n     *              <ul>\n     *              <li>{@link Calendar#ERA}</li>\n     *              <li>{@link Calendar#YEAR}</li>\n     *              <li>{@link Calendar#MONTH}</li>\n     *              <li>...</li>\n     *              <li>{@link Calendar#DST_OFFSET}</li>\n     *              </ul>\n     * @return the value of the given calendar field\n     */\n    public static int getValueByCalendarField(final Date date, final int field) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        return cal.get(field);\n    }\n\n    /**\n     * Returns the value of the given calendar field.\n     *\n     * @param millis The milliseconds.\n     * @param field  The given calendar field.\n     *               <ul>\n     *               <li>{@link Calendar#ERA}</li>\n     *               <li>{@link Calendar#YEAR}</li>\n     *               <li>{@link Calendar#MONTH}</li>\n     *               <li>...</li>\n     *               <li>{@link Calendar#DST_OFFSET}</li>\n     *               </ul>\n     * @return the value of the given calendar field\n     */\n    public static int getValueByCalendarField(final long millis, final int field) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTimeInMillis(millis);\n        return cal.get(field);\n    }\n\n    private static final String[] CHINESE_ZODIAC =\n            {\"猴\", \"鸡\", \"狗\", \"猪\", \"鼠\", \"牛\", \"虎\", \"兔\", \"龙\", \"蛇\", \"马\", \"羊\"};\n\n    /**\n     * Return the Chinese zodiac.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return the Chinese zodiac\n     */\n    public static String getChineseZodiac(final String time) {\n        return getChineseZodiac(string2Date(time, getDefaultFormat()));\n    }\n\n    /**\n     * Return the Chinese zodiac.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return the Chinese zodiac\n     */\n    public static String getChineseZodiac(final String time, @NonNull final DateFormat format) {\n        return getChineseZodiac(string2Date(time, format));\n    }\n\n    /**\n     * Return the Chinese zodiac.\n     *\n     * @param date The date.\n     * @return the Chinese zodiac\n     */\n    public static String getChineseZodiac(final Date date) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        return CHINESE_ZODIAC[cal.get(Calendar.YEAR) % 12];\n    }\n\n    /**\n     * Return the Chinese zodiac.\n     *\n     * @param millis The milliseconds.\n     * @return the Chinese zodiac\n     */\n    public static String getChineseZodiac(final long millis) {\n        return getChineseZodiac(millis2Date(millis));\n    }\n\n    /**\n     * Return the Chinese zodiac.\n     *\n     * @param year The year.\n     * @return the Chinese zodiac\n     */\n    public static String getChineseZodiac(final int year) {\n        return CHINESE_ZODIAC[year % 12];\n    }\n\n    private static final int[] ZODIAC_FLAGS = {20, 19, 21, 21, 21, 22, 23, 23, 23, 24, 23, 22};\n    private static final String[] ZODIAC = {\n            \"水瓶座\", \"双鱼座\", \"白羊座\", \"金牛座\", \"双子座\", \"巨蟹座\",\n            \"狮子座\", \"处女座\", \"天秤座\", \"天蝎座\", \"射手座\", \"魔羯座\"\n    };\n\n    /**\n     * Return the zodiac.\n     * <p>The pattern is {@code yyyy-MM-dd HH:mm:ss}.</p>\n     *\n     * @param time The formatted time string.\n     * @return the zodiac\n     */\n    public static String getZodiac(final String time) {\n        return getZodiac(string2Date(time, getDefaultFormat()));\n    }\n\n    /**\n     * Return the zodiac.\n     *\n     * @param time   The formatted time string.\n     * @param format The format.\n     * @return the zodiac\n     */\n    public static String getZodiac(final String time, @NonNull final DateFormat format) {\n        return getZodiac(string2Date(time, format));\n    }\n\n    /**\n     * Return the zodiac.\n     *\n     * @param date The date.\n     * @return the zodiac\n     */\n    public static String getZodiac(final Date date) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTime(date);\n        int month = cal.get(Calendar.MONTH) + 1;\n        int day = cal.get(Calendar.DAY_OF_MONTH);\n        return getZodiac(month, day);\n    }\n\n    /**\n     * Return the zodiac.\n     *\n     * @param millis The milliseconds.\n     * @return the zodiac\n     */\n    public static String getZodiac(final long millis) {\n        return getZodiac(millis2Date(millis));\n    }\n\n    /**\n     * Return the zodiac.\n     *\n     * @param month The month.\n     * @param day   The day.\n     * @return the zodiac\n     */\n    public static String getZodiac(final int month, final int day) {\n        return ZODIAC[day >= ZODIAC_FLAGS[month - 1]\n                ? month - 1\n                : (month + 10) % 12];\n    }\n\n    private static long timeSpan2Millis(final long timeSpan, @TimeConstants.Unit final int unit) {\n        return timeSpan * unit;\n    }\n\n    private static long millis2TimeSpan(final long millis, @TimeConstants.Unit final int unit) {\n        return millis / unit;\n    }\n\n    private static String millis2FitTimeSpan(long millis, int precision) {\n        if (precision <= 0) return null;\n        precision = Math.min(precision, 5);\n        String[] units = {\"天\", \"小时\", \"分钟\", \"秒\", \"毫秒\"};\n        if (millis == 0) return 0 + units[precision - 1];\n        StringBuilder sb = new StringBuilder();\n        if (millis < 0) {\n            sb.append(\"-\");\n            millis = -millis;\n        }\n        int[] unitLen = {86400000, 3600000, 60000, 1000, 1};\n        for (int i = 0; i < precision; i++) {\n            if (millis >= unitLen[i]) {\n                long mode = millis / unitLen[i];\n                millis -= mode * unitLen[i];\n                sb.append(mode).append(units[i]);\n            }\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/Toasts.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils\n\nimport android.content.Context\nimport android.widget.Toast\nimport androidx.fragment.app.Fragment\n\n\nprivate var toast: Toast? = null\n\nfun Context.toastOnUi(message: Int) {\n    runOnUI {\n        if (toast == null) {\n            toast = Toast.makeText(this, message, Toast.LENGTH_SHORT)\n        } else {\n            toast?.setText(message)\n            toast?.duration = Toast.LENGTH_SHORT\n        }\n        toast?.show()\n    }\n}\n\nfun Context.toastOnUi(message: CharSequence?) {\n    runOnUI {\n        if (toast == null) {\n            toast = Toast.makeText(this, message, Toast.LENGTH_SHORT)\n        } else {\n            toast?.setText(message)\n            toast?.duration = Toast.LENGTH_SHORT\n        }\n        toast?.show()\n    }\n}\n\nfun Context.longToastOnUi(message: Int) {\n    runOnUI {\n        if (toast == null) {\n            toast = Toast.makeText(this, message, Toast.LENGTH_LONG)\n        } else {\n            toast?.setText(message)\n            toast?.duration = Toast.LENGTH_LONG\n        }\n        toast?.show()\n    }\n}\n\nfun Context.longToastOnUi(message: CharSequence?) {\n    runOnUI {\n        if (toast == null) {\n            toast = Toast.makeText(this, message, Toast.LENGTH_LONG)\n        } else {\n            toast?.setText(message)\n            toast?.duration = Toast.LENGTH_LONG\n        }\n        toast?.show()\n    }\n}\n\n/**\n * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration.\n *\n * @param message the message text resource.\n */\nfun Fragment.toastOnUi(message: Int) = requireActivity().toastOnUi(message)\n\n/**\n * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration.\n *\n * @param message the message text.\n */\nfun Fragment.toastOnUi(message: CharSequence) = requireActivity().toastOnUi(message)\n\n/**\n * Display the simple Toast message with the [Toast.LENGTH_LONG] duration.\n *\n * @param message the message text resource.\n */\nfun Fragment.longToast(message: Int) = requireContext().longToastOnUi(message)\n\n/**\n * Display the simple Toast message with the [Toast.LENGTH_LONG] duration.\n *\n * @param message the message text.\n */\nfun Fragment.longToast(message: CharSequence) = requireContext().longToastOnUi(message)\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/UriExtensions.kt",
    "content": "package com.kunfei.bookshelf.utils\n\nimport android.content.Context\nimport android.net.Uri\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.documentfile.provider.DocumentFile\nimport androidx.fragment.app.Fragment\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.help.permission.Permissions\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat\nimport timber.log.Timber\nimport java.io.File\nimport java.io.IOException\n\nfun Uri.isContentScheme() = this.scheme == \"content\"\n\n/**\n * 读取URI\n */\nfun AppCompatActivity.readUri(uri: Uri?, success: (name: String, bytes: ByteArray) -> Unit) {\n    uri ?: return\n    try {\n        if (uri.isContentScheme()) {\n            val doc = DocumentFile.fromSingleUri(this, uri)\n            doc ?: throw IOException(\"未获取到文件\")\n            val name = doc.name ?: throw IOException(\"未获取到文件名\")\n            val fileBytes = DocumentUtils.readBytes(this, doc.uri)\n            success.invoke(name, fileBytes)\n        } else {\n            PermissionsCompat.Builder(this)\n                .addPermissions(\n                    Permissions.READ_EXTERNAL_STORAGE,\n                    Permissions.WRITE_EXTERNAL_STORAGE\n                )\n                .rationale(R.string.bg_image_per)\n                .onGranted {\n                    RealPathUtil.getPath(this, uri)?.let { path ->\n                        val imgFile = File(path)\n                        success.invoke(imgFile.name, imgFile.readBytes())\n                    }\n                }\n                .request()\n        }\n    } catch (e: Exception) {\n        Timber.e(e)\n        toastOnUi(e.localizedMessage ?: \"read uri error\")\n    }\n}\n\n/**\n * 读取URI\n */\nfun Fragment.readUri(uri: Uri?, success: (name: String, bytes: ByteArray) -> Unit) {\n    uri ?: return\n    try {\n        if (uri.isContentScheme()) {\n            val doc = DocumentFile.fromSingleUri(requireContext(), uri)\n            doc ?: throw IOException(\"未获取到文件\")\n            val name = doc.name ?: throw IOException(\"未获取到文件名\")\n            val fileBytes = DocumentUtils.readBytes(requireContext(), doc.uri)\n            success.invoke(name, fileBytes)\n        } else {\n            PermissionsCompat.Builder(this)\n                .addPermissions(\n                    Permissions.READ_EXTERNAL_STORAGE,\n                    Permissions.WRITE_EXTERNAL_STORAGE\n                )\n                .rationale(R.string.bg_image_per)\n                .onGranted {\n                    RealPathUtil.getPath(requireContext(), uri)?.let { path ->\n                        val imgFile = File(path)\n                        success.invoke(imgFile.name, imgFile.readBytes())\n                    }\n                }\n                .request()\n        }\n    } catch (e: Exception) {\n        Timber.e(e)\n        toastOnUi(e.localizedMessage ?: \"read uri error\")\n    }\n}\n\n@Throws(Exception::class)\nfun Uri.readBytes(context: Context): ByteArray {\n    return if (this.isContentScheme()) {\n        DocumentUtils.readBytes(context, this)\n    } else {\n        val path = RealPathUtil.getPath(context, this)\n        if (path?.isNotEmpty() == true) {\n            File(path).readBytes()\n        } else {\n            throw IOException(\"获取文件真实地址失败\\n${this.path}\")\n        }\n    }\n}\n\n@Throws(Exception::class)\nfun Uri.readText(context: Context): String {\n    readBytes(context).let {\n        return String(it)\n    }\n}\n\n@Throws(Exception::class)\nfun Uri.writeBytes(\n    context: Context,\n    byteArray: ByteArray\n): Boolean {\n    if (this.isContentScheme()) {\n        return DocumentUtils.writeBytes(context, byteArray, this)\n    } else {\n        val path = RealPathUtil.getPath(context, this)\n        if (path?.isNotEmpty() == true) {\n            File(path).writeBytes(byteArray)\n            return true\n        }\n    }\n    return false\n}\n\n@Throws(Exception::class)\nfun Uri.writeText(context: Context, text: String): Boolean {\n    return writeBytes(context, text.toByteArray())\n}\n\nfun Uri.writeBytes(\n    context: Context,\n    fileName: String,\n    byteArray: ByteArray\n): Boolean {\n    if (this.isContentScheme()) {\n        DocumentFile.fromTreeUri(context, this)?.let { pDoc ->\n            DocumentUtils.createFileIfNotExist(pDoc, fileName)?.let {\n                return it.uri.writeBytes(context, byteArray)\n            }\n        }\n    } else {\n        FileUtils.createFileWithReplace(path + File.separatorChar + fileName)\n            .writeBytes(byteArray)\n        return true\n    }\n    return false\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/UrlEncoderUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport java.util.BitSet;\n\n/**\n * 这里会有误差,比如输入一个字符串 123+456,它到底是原文就是123+456还是123 456做了urlEncode后的内容呢？<br>\n * 其实问题是一样的，比如遇到123%2B456,它到底是原文即使如此，还是123+456 urlEncode后的呢？ <br>\n * 在这里，我认为只要符合urlEncode规范的，就当作已经urlEncode过了<br>\n * 毕竟这个方法的初衷就是判断string是否urlEncode过<br>\n */\npublic class UrlEncoderUtils {\n    private static BitSet dontNeedEncoding;\n\n    static {\n        dontNeedEncoding = new BitSet(256);\n        int i;\n        for (i = 'a'; i <= 'z'; i++) {\n            dontNeedEncoding.set(i);\n        }\n        for (i = 'A'; i <= 'Z'; i++) {\n            dontNeedEncoding.set(i);\n        }\n        for (i = '0'; i <= '9'; i++) {\n            dontNeedEncoding.set(i);\n        }\n        dontNeedEncoding.set('+');\n        dontNeedEncoding.set('-');\n        dontNeedEncoding.set('_');\n        dontNeedEncoding.set('.');\n        dontNeedEncoding.set('$');\n        dontNeedEncoding.set(':');\n        dontNeedEncoding.set('(');\n        dontNeedEncoding.set(')');\n        dontNeedEncoding.set('!');\n        dontNeedEncoding.set('*');\n        dontNeedEncoding.set('@');\n        dontNeedEncoding.set('&');\n        dontNeedEncoding.set('#');\n        dontNeedEncoding.set(',');\n        dontNeedEncoding.set('[');\n        dontNeedEncoding.set(']');\n    }\n\n    /**\n     * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+'\n     * 0-9a-zA-Z保留 <br>\n     * ! * ' ( ) ; : @ & = + $ , / ? # [ ] 保留\n     * 其他字符转成%XX的格式，X是16进制的大写字符，范围是[0-9A-F]\n     */\n    public static boolean hasUrlEncoded(String str) {\n        boolean needEncode = false;\n        for (int i = 0; i < str.length(); i++) {\n            char c = str.charAt(i);\n            if (dontNeedEncoding.get((int) c)) {\n                continue;\n            }\n            if (c == '%' && (i + 2) < str.length()) {\n                // 判断是否符合urlEncode规范\n                char c1 = str.charAt(++i);\n                char c2 = str.charAt(++i);\n                if (isDigit16Char(c1) && isDigit16Char(c2)) {\n                    continue;\n                }\n            }\n            // 其他字符，肯定需要urlEncode\n            needEncode = true;\n            break;\n        }\n\n        return !needEncode;\n    }\n\n    /**\n     * 判断c是否是16进制的字符\n     */\n    private static boolean isDigit16Char(char c) {\n        return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F');\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/XmlUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n/*\n * Copyright (C) 2006 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport android.graphics.Bitmap;\nimport android.graphics.Bitmap.CompressFormat;\nimport android.graphics.BitmapFactory;\nimport android.net.Uri;\nimport android.util.Base64;\nimport android.util.Xml;\n\nimport org.xmlpull.v1.XmlPullParser;\nimport org.xmlpull.v1.XmlPullParserException;\nimport org.xmlpull.v1.XmlSerializer;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.ProtocolException;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic class XmlUtils {\n\n    public static void skipCurrentTag(XmlPullParser parser)\n            throws XmlPullParserException, IOException {\n        int outerDepth = parser.getDepth();\n        int type;\n        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT\n                && (type != XmlPullParser.END_TAG\n                || parser.getDepth() > outerDepth)) {\n        }\n    }\n\n    public static int convertValueToList(CharSequence value, String[] options, int defaultValue) {\n        if (null != value) {\n            for (int i = 0; i < options.length; i++) {\n                if (value.equals(options[i]))\n                    return i;\n            }\n        }\n\n        return defaultValue;\n    }\n\n    public static boolean convertValueToBoolean(CharSequence value, boolean defaultValue) {\n        boolean result = false;\n\n        if (null == value)\n            return defaultValue;\n\n        if (value.equals(\"1\")\n                || value.equals(\"true\")\n                || value.equals(\"TRUE\"))\n            result = true;\n\n        return result;\n    }\n\n    public static int convertValueToInt(CharSequence charSeq, int defaultValue) {\n        if (null == charSeq)\n            return defaultValue;\n\n        String nm = charSeq.toString();\n\n        // XXX This code is copied from Integer.decode() so we don't\n        // have to instantiate an Integer!\n\n        int value;\n        int sign = 1;\n        int index = 0;\n        int len = nm.length();\n        int base = 10;\n\n        if ('-' == nm.charAt(0)) {\n            sign = -1;\n            index++;\n        }\n\n        if ('0' == nm.charAt(index)) {\n            //  Quick check for a zero by itself\n            if (index == (len - 1))\n                return 0;\n\n            char c = nm.charAt(index + 1);\n\n            if ('x' == c || 'X' == c) {\n                index += 2;\n                base = 16;\n            } else {\n                index++;\n                base = 8;\n            }\n        } else if ('#' == nm.charAt(index)) {\n            index++;\n            base = 16;\n        }\n\n        return Integer.parseInt(nm.substring(index), base) * sign;\n    }\n\n    public static int convertValueToUnsignedInt(String value, int defaultValue) {\n        if (null == value) {\n            return defaultValue;\n        }\n\n        return parseUnsignedIntAttribute(value);\n    }\n\n    public static int parseUnsignedIntAttribute(CharSequence charSeq) {\n        String value = charSeq.toString();\n\n        long bits;\n        int index = 0;\n        int len = value.length();\n        int base = 10;\n\n        if ('0' == value.charAt(index)) {\n            //  Quick check for zero by itself\n            if (index == (len - 1))\n                return 0;\n\n            char c = value.charAt(index + 1);\n\n            if ('x' == c || 'X' == c) {     //  check for hex\n                index += 2;\n                base = 16;\n            } else {                        //  check for octal\n                index++;\n                base = 8;\n            }\n        } else if ('#' == value.charAt(index)) {\n            index++;\n            base = 16;\n        }\n\n        return (int) Long.parseLong(value.substring(index), base);\n    }\n\n    /**\n     * Flatten a Map into an output stream as XML.  The map can later be\n     * read back with readMapXml().\n     *\n     * @param val The map to be flattened.\n     * @param out Where to write the XML data.\n     * @see #writeMapXml(Map, String, XmlSerializer)\n     * @see #writeListXml\n     * @see #writeValueXml\n     * @see #readMapXml\n     */\n    public static void writeMapXml(Map val, OutputStream out)\n            throws XmlPullParserException, java.io.IOException {\n        XmlSerializer serializer = new FastXmlSerializer();\n        serializer.setOutput(out, \"utf-8\");\n        serializer.startDocument(null, true);\n        serializer.setFeature(\"http://xmlpull.org/v1/doc/features.html#indent-output\", true);\n        writeMapXml(val, null, serializer);\n        serializer.endDocument();\n    }\n\n    /**\n     * Flatten a List into an output stream as XML.  The list can later be\n     * read back with readListXml().\n     *\n     * @param val The list to be flattened.\n     * @param out Where to write the XML data.\n     * @see #writeListXml(List, String, XmlSerializer)\n     * @see #writeMapXml\n     * @see #writeValueXml\n     * @see #readListXml\n     */\n    public static void writeListXml(List val, OutputStream out)\n            throws XmlPullParserException, java.io.IOException {\n        XmlSerializer serializer = Xml.newSerializer();\n        serializer.setOutput(out, \"utf-8\");\n        serializer.startDocument(null, true);\n        serializer.setFeature(\"http://xmlpull.org/v1/doc/features.html#indent-output\", true);\n        writeListXml(val, null, serializer);\n        serializer.endDocument();\n    }\n\n    /**\n     * Flatten a Map into an XmlSerializer.  The map can later be read back\n     * with readThisMapXml().\n     *\n     * @param val  The map to be flattened.\n     * @param name Name attribute to include with this list's tag, or null for\n     *             none.\n     * @param out  XmlSerializer to write the map into.\n     * @see #writeMapXml(Map, OutputStream)\n     * @see #writeListXml\n     * @see #writeValueXml\n     * @see #readMapXml\n     */\n    public static void writeMapXml(Map val, String name, XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n        writeMapXml(val, name, out, null);\n    }\n\n    /**\n     * Flatten a Map into an XmlSerializer.  The map can later be read back\n     * with readThisMapXml().\n     *\n     * @param val      The map to be flattened.\n     * @param name     Name attribute to include with this list's tag, or null for\n     *                 none.\n     * @param out      XmlSerializer to write the map into.\n     * @param callback Method to call when an Object type is not recognized.\n     * @hide\n     * @see #writeMapXml(Map, OutputStream)\n     * @see #writeListXml\n     * @see #writeValueXml\n     * @see #readMapXml\n     */\n    public static void writeMapXml(Map val, String name, XmlSerializer out,\n                                   WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {\n\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"map\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        writeMapXml(val, out, callback);\n\n        out.endTag(null, \"map\");\n    }\n\n    /**\n     * Flatten a Map into an XmlSerializer.  The map can later be read back\n     * with readThisMapXml(). This method presumes that the start tag and\n     * name attribute have already been written and does not write an end tag.\n     *\n     * @param val The map to be flattened.\n     * @param out XmlSerializer to write the map into.\n     * @hide\n     * @see #writeMapXml(Map, OutputStream)\n     * @see #writeListXml\n     * @see #writeValueXml\n     * @see #readMapXml\n     */\n    public static void writeMapXml(Map val, XmlSerializer out,\n                                   WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {\n        if (val == null) {\n            return;\n        }\n\n        Set s = val.entrySet();\n\n        for (Object o : s) {\n            Map.Entry e = (Map.Entry) o;\n            writeValueXml(e.getValue(), (String) e.getKey(), out, callback);\n        }\n    }\n\n    /**\n     * Flatten a List into an XmlSerializer.  The list can later be read back\n     * with readThisListXml().\n     *\n     * @param val  The list to be flattened.\n     * @param name Name attribute to include with this list's tag, or null for\n     *             none.\n     * @param out  XmlSerializer to write the list into.\n     * @see #writeListXml(List, OutputStream)\n     * @see #writeMapXml\n     * @see #writeValueXml\n     * @see #readListXml\n     */\n    public static void writeListXml(List val, String name, XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"list\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        int N = val.size();\n        int i = 0;\n        while (i < N) {\n            writeValueXml(val.get(i), null, out);\n            i++;\n        }\n\n        out.endTag(null, \"list\");\n    }\n\n    public static void writeSetXml(Set val, String name, XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"set\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        for (Object v : val) {\n            writeValueXml(v, null, out);\n        }\n\n        out.endTag(null, \"set\");\n    }\n\n    /**\n     * Flatten a byte[] into an XmlSerializer.  The list can later be read back\n     * with readThisByteArrayXml().\n     *\n     * @param val  The byte array to be flattened.\n     * @param name Name attribute to include with this array's tag, or null for\n     *             none.\n     * @param out  XmlSerializer to write the array into.\n     * @see #writeMapXml\n     * @see #writeValueXml\n     */\n    public static void writeByteArrayXml(byte[] val, String name,\n                                         XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"byte-array\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        final int N = val.length;\n        out.attribute(null, \"num\", Integer.toString(N));\n\n        StringBuilder sb = new StringBuilder(val.length * 2);\n        for (int b : val) {\n            int h = b >> 4;\n            sb.append(h >= 10 ? ('a' + h - 10) : ('0' + h));\n            h = b & 0xff;\n            sb.append(h >= 10 ? ('a' + h - 10) : ('0' + h));\n        }\n\n        out.text(sb.toString());\n\n        out.endTag(null, \"byte-array\");\n    }\n\n    /**\n     * Flatten an int[] into an XmlSerializer.  The list can later be read back\n     * with readThisIntArrayXml().\n     *\n     * @param val  The int array to be flattened.\n     * @param name Name attribute to include with this array's tag, or null for\n     *             none.\n     * @param out  XmlSerializer to write the array into.\n     * @see #writeMapXml\n     * @see #writeValueXml\n     * @see #readThisIntArrayXml\n     */\n    public static void writeIntArrayXml(int[] val, String name,\n                                        XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"int-array\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        final int N = val.length;\n        out.attribute(null, \"num\", Integer.toString(N));\n\n        for (int i1 : val) {\n            out.startTag(null, \"item\");\n            out.attribute(null, \"value\", Integer.toString(i1));\n            out.endTag(null, \"item\");\n        }\n\n        out.endTag(null, \"int-array\");\n    }\n\n    /**\n     * Flatten a long[] into an XmlSerializer.  The list can later be read back\n     * with readThisLongArrayXml().\n     *\n     * @param val  The long array to be flattened.\n     * @param name Name attribute to include with this array's tag, or null for\n     *             none.\n     * @param out  XmlSerializer to write the array into.\n     * @see #writeMapXml\n     * @see #writeValueXml\n     * @see #readThisIntArrayXml\n     */\n    public static void writeLongArrayXml(long[] val, String name, XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"long-array\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        final int N = val.length;\n        out.attribute(null, \"num\", Integer.toString(N));\n\n        for (long l : val) {\n            out.startTag(null, \"item\");\n            out.attribute(null, \"value\", Long.toString(l));\n            out.endTag(null, \"item\");\n        }\n\n        out.endTag(null, \"long-array\");\n    }\n\n    /**\n     * Flatten a double[] into an XmlSerializer.  The list can later be read back\n     * with readThisDoubleArrayXml().\n     *\n     * @param val  The double array to be flattened.\n     * @param name Name attribute to include with this array's tag, or null for\n     *             none.\n     * @param out  XmlSerializer to write the array into.\n     * @see #writeMapXml\n     * @see #writeValueXml\n     * @see #readThisIntArrayXml\n     */\n    public static void writeDoubleArrayXml(double[] val, String name, XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"double-array\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        final int N = val.length;\n        out.attribute(null, \"num\", Integer.toString(N));\n\n        for (double v : val) {\n            out.startTag(null, \"item\");\n            out.attribute(null, \"value\", Double.toString(v));\n            out.endTag(null, \"item\");\n        }\n\n        out.endTag(null, \"double-array\");\n    }\n\n    /**\n     * Flatten a String[] into an XmlSerializer.  The list can later be read back\n     * with readThisStringArrayXml().\n     *\n     * @param val  The long array to be flattened.\n     * @param name Name attribute to include with this array's tag, or null for\n     *             none.\n     * @param out  XmlSerializer to write the array into.\n     * @see #writeMapXml\n     * @see #writeValueXml\n     * @see #readThisIntArrayXml\n     */\n    public static void writeStringArrayXml(String[] val, String name, XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n\n        if (val == null) {\n            out.startTag(null, \"null\");\n            out.endTag(null, \"null\");\n            return;\n        }\n\n        out.startTag(null, \"string-array\");\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n\n        final int N = val.length;\n        out.attribute(null, \"num\", Integer.toString(N));\n\n        for (String s : val) {\n            out.startTag(null, \"item\");\n            out.attribute(null, \"value\", s);\n            out.endTag(null, \"item\");\n        }\n\n        out.endTag(null, \"string-array\");\n    }\n\n    /**\n     * Flatten an object's value into an XmlSerializer.  The value can later\n     * be read back with readThisValueXml().\n     * <p>\n     * Currently supported value types are: null, String, Integer, Long,\n     * Float, Double Boolean, Map, List.\n     *\n     * @param v    The object to be flattened.\n     * @param name Name attribute to include with this value's tag, or null\n     *             for none.\n     * @param out  XmlSerializer to write the object into.\n     * @see #writeMapXml\n     * @see #writeListXml\n     * @see #readValueXml\n     */\n    public static void writeValueXml(Object v, String name, XmlSerializer out)\n            throws XmlPullParserException, java.io.IOException {\n        writeValueXml(v, name, out, null);\n    }\n\n    /**\n     * Flatten an object's value into an XmlSerializer.  The value can later\n     * be read back with readThisValueXml().\n     * <p>\n     * Currently supported value types are: null, String, Integer, Long,\n     * Float, Double Boolean, Map, List.\n     *\n     * @param v        The object to be flattened.\n     * @param name     Name attribute to include with this value's tag, or null\n     *                 for none.\n     * @param out      XmlSerializer to write the object into.\n     * @param callback Handler for Object types not recognized.\n     * @see #writeMapXml\n     * @see #writeListXml\n     * @see #readValueXml\n     */\n    private static void writeValueXml(Object v, String name, XmlSerializer out,\n                                      WriteMapCallback callback) throws XmlPullParserException, java.io.IOException {\n        String typeStr;\n        if (v == null) {\n            out.startTag(null, \"null\");\n            if (name != null) {\n                out.attribute(null, \"name\", name);\n            }\n            out.endTag(null, \"null\");\n            return;\n        } else if (v instanceof String) {\n            out.startTag(null, \"string\");\n            if (name != null) {\n                out.attribute(null, \"name\", name);\n            }\n            out.text(v.toString());\n            out.endTag(null, \"string\");\n            return;\n        } else if (v instanceof Integer) {\n            typeStr = \"int\";\n        } else if (v instanceof Long) {\n            typeStr = \"long\";\n        } else if (v instanceof Float) {\n            typeStr = \"float\";\n        } else if (v instanceof Double) {\n            typeStr = \"double\";\n        } else if (v instanceof Boolean) {\n            typeStr = \"boolean\";\n        } else if (v instanceof byte[]) {\n            writeByteArrayXml((byte[]) v, name, out);\n            return;\n        } else if (v instanceof int[]) {\n            writeIntArrayXml((int[]) v, name, out);\n            return;\n        } else if (v instanceof long[]) {\n            writeLongArrayXml((long[]) v, name, out);\n            return;\n        } else if (v instanceof double[]) {\n            writeDoubleArrayXml((double[]) v, name, out);\n            return;\n        } else if (v instanceof String[]) {\n            writeStringArrayXml((String[]) v, name, out);\n            return;\n        } else if (v instanceof Map) {\n            writeMapXml((Map) v, name, out);\n            return;\n        } else if (v instanceof List) {\n            writeListXml((List) v, name, out);\n            return;\n        } else if (v instanceof Set) {\n            writeSetXml((Set) v, name, out);\n            return;\n        } else if (v instanceof CharSequence) {\n            // XXX This is to allow us to at least write something if\n            // we encounter styled text...  but it means we will drop all\n            // of the styling information. :(\n            out.startTag(null, \"string\");\n            if (name != null) {\n                out.attribute(null, \"name\", name);\n            }\n            out.text(v.toString());\n            out.endTag(null, \"string\");\n            return;\n        } else if (callback != null) {\n            callback.writeUnknownObject(v, name, out);\n            return;\n        } else {\n            throw new RuntimeException(\"writeValueXml: unable to write value \" + v);\n        }\n\n        out.startTag(null, typeStr);\n        if (name != null) {\n            out.attribute(null, \"name\", name);\n        }\n        out.attribute(null, \"value\", v.toString());\n        out.endTag(null, typeStr);\n    }\n\n    /**\n     * Read a HashMap from an InputStream containing XML.  The stream can\n     * previously have been written by writeMapXml().\n     *\n     * @param in The InputStream from which to read.\n     * @return HashMap The resulting map.\n     * @see #readListXml\n     * @see #readValueXml\n     * @see #readThisMapXml\n     * #see #writeMapXml\n     */\n    @SuppressWarnings(\"unchecked\")\n    public static HashMap<String, ?> readMapXml(InputStream in)\n            throws XmlPullParserException, java.io.IOException {\n        XmlPullParser parser = Xml.newPullParser();\n        parser.setInput(in, null);\n        return (HashMap<String, ?>) readValueXml(parser, new String[1]);\n    }\n\n    /**\n     * Read an ArrayList from an InputStream containing XML.  The stream can\n     * previously have been written by writeListXml().\n     *\n     * @param in The InputStream from which to read.\n     * @return ArrayList The resulting list.\n     * @see #readMapXml\n     * @see #readValueXml\n     * @see #readThisListXml\n     * @see #writeListXml\n     */\n    public static ArrayList readListXml(InputStream in)\n            throws XmlPullParserException, java.io.IOException {\n        XmlPullParser parser = Xml.newPullParser();\n        parser.setInput(in, null);\n        return (ArrayList) readValueXml(parser, new String[1]);\n    }\n\n\n    /**\n     * Read a HashSet from an InputStream containing XML. The stream can\n     * previously have been written by writeSetXml().\n     *\n     * @param in The InputStream from which to read.\n     * @return HashSet The resulting set.\n     * @throws XmlPullParserException\n     * @throws java.io.IOException\n     * @see #readValueXml\n     * @see #readThisSetXml\n     * @see #writeSetXml\n     */\n    public static HashSet readSetXml(InputStream in)\n            throws XmlPullParserException, java.io.IOException {\n        XmlPullParser parser = Xml.newPullParser();\n        parser.setInput(in, null);\n        return (HashSet) readValueXml(parser, new String[1]);\n    }\n\n    /**\n     * Read a HashMap object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeMapXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the map.\n     *\n     * @param parser The XmlPullParser from which to read the map data.\n     * @param endTag Name of the tag that will end the map, usually \"map\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the map's tag.\n     * @return HashMap The newly generated map.\n     * @see #readMapXml\n     */\n    public static HashMap<String, ?> readThisMapXml(XmlPullParser parser, String endTag,\n                                                    String[] name) throws XmlPullParserException, java.io.IOException {\n        return readThisMapXml(parser, endTag, name, null);\n    }\n\n    /**\n     * Read a HashMap object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeMapXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the map.\n     *\n     * @param parser The XmlPullParser from which to read the map data.\n     * @param endTag Name of the tag that will end the map, usually \"map\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the map's tag.\n     * @return HashMap The newly generated map.\n     * @hide\n     * @see #readMapXml\n     */\n    public static HashMap<String, ?> readThisMapXml(XmlPullParser parser, String endTag,\n                                                    String[] name, ReadMapCallback callback)\n            throws XmlPullParserException, java.io.IOException {\n        HashMap<String, Object> map = new HashMap<>();\n\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                Object val = readThisValueXml(parser, name, callback);\n                map.put(name[0], val);\n            } else if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(endTag)) {\n                    return map;\n                }\n                throw new XmlPullParserException(\n                        \"Expected \" + endTag + \" end tag at: \" + parser.getName());\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\n                \"Document ended before \" + endTag + \" end tag\");\n    }\n\n    /**\n     * Read an ArrayList object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeListXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the list.\n     *\n     * @param parser The XmlPullParser from which to read the list data.\n     * @param endTag Name of the tag that will end the list, usually \"list\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the list's tag.\n     * @return HashMap The newly generated list.\n     * @see #readListXml\n     */\n    public static ArrayList readThisListXml(XmlPullParser parser, String endTag,\n                                            String[] name) throws XmlPullParserException, java.io.IOException {\n        return readThisListXml(parser, endTag, name, null);\n    }\n\n    /**\n     * Read an ArrayList object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeListXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the list.\n     *\n     * @param parser The XmlPullParser from which to read the list data.\n     * @param endTag Name of the tag that will end the list, usually \"list\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the list's tag.\n     * @return HashMap The newly generated list.\n     * @see #readListXml\n     */\n    private static ArrayList<Object> readThisListXml(XmlPullParser parser, String endTag,\n                                                     String[] name, ReadMapCallback callback)\n            throws XmlPullParserException, java.io.IOException {\n        ArrayList<Object> list = new ArrayList<>();\n\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                Object val = readThisValueXml(parser, name, callback);\n                list.add(val);\n                //System.out.println(\"Adding to list: \" + val);\n            } else if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(endTag)) {\n                    return list;\n                }\n                throw new XmlPullParserException(\n                        \"Expected \" + endTag + \" end tag at: \" + parser.getName());\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\n                \"Document ended before \" + endTag + \" end tag\");\n    }\n\n    /**\n     * Read a HashSet object from an XmlPullParser. The XML data could previously\n     * have been generated by writeSetXml(). The XmlPullParser must be positioned\n     * <em>after</em> the tag that begins the set.\n     *\n     * @param parser The XmlPullParser from which to read the set data.\n     * @param endTag Name of the tag that will end the set, usually \"set\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the set's tag.\n     * @return HashSet The newly generated set.\n     * @throws XmlPullParserException\n     * @throws java.io.IOException\n     * @see #readSetXml\n     */\n    public static HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name)\n            throws XmlPullParserException, java.io.IOException {\n        return readThisSetXml(parser, endTag, name, null);\n    }\n\n    /**\n     * Read a HashSet object from an XmlPullParser. The XML data could previously\n     * have been generated by writeSetXml(). The XmlPullParser must be positioned\n     * <em>after</em> the tag that begins the set.\n     *\n     * @param parser The XmlPullParser from which to read the set data.\n     * @param endTag Name of the tag that will end the set, usually \"set\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the set's tag.\n     * @return HashSet The newly generated set.\n     * @throws XmlPullParserException\n     * @throws java.io.IOException\n     * @hide\n     * @see #readSetXml\n     */\n    private static HashSet<Object> readThisSetXml(XmlPullParser parser, String endTag, String[] name,\n                                                  ReadMapCallback callback) throws XmlPullParserException, java.io.IOException {\n        HashSet<Object> set = new HashSet<>();\n\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                Object val = readThisValueXml(parser, name, callback);\n                set.add(val);\n                //System.out.println(\"Adding to set: \" + val);\n            } else if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(endTag)) {\n                    return set;\n                }\n                throw new XmlPullParserException(\n                        \"Expected \" + endTag + \" end tag at: \" + parser.getName());\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\n                \"Document ended before \" + endTag + \" end tag\");\n    }\n\n    /**\n     * Read an int[] object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeIntArrayXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the list.\n     *\n     * @param parser The XmlPullParser from which to read the list data.\n     * @param endTag Name of the tag that will end the list, usually \"list\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the list's tag.\n     * @return Returns a newly generated int[].\n     * @see #readListXml\n     */\n    public static int[] readThisIntArrayXml(XmlPullParser parser,\n                                            String endTag, String[] name)\n            throws XmlPullParserException, java.io.IOException {\n\n        int num;\n        try {\n            num = Integer.parseInt(parser.getAttributeValue(null, \"num\"));\n        } catch (NullPointerException e) {\n            throw new XmlPullParserException(\n                    \"Need num attribute in byte-array\");\n        } catch (NumberFormatException e) {\n            throw new XmlPullParserException(\n                    \"Not a number in num attribute in byte-array\");\n        }\n        parser.next();\n\n        int[] array = new int[num];\n        int i = 0;\n\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                if (parser.getName().equals(\"item\")) {\n                    try {\n                        array[i] = Integer.parseInt(\n                                parser.getAttributeValue(null, \"value\"));\n                    } catch (NullPointerException e) {\n                        throw new XmlPullParserException(\n                                \"Need value attribute in item\");\n                    } catch (NumberFormatException e) {\n                        throw new XmlPullParserException(\n                                \"Not a number in value attribute in item\");\n                    }\n                } else {\n                    throw new XmlPullParserException(\n                            \"Expected item tag at: \" + parser.getName());\n                }\n            } else if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(endTag)) {\n                    return array;\n                } else if (parser.getName().equals(\"item\")) {\n                    i++;\n                } else {\n                    throw new XmlPullParserException(\n                            \"Expected \" + endTag + \" end tag at: \"\n                                    + parser.getName());\n                }\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\n                \"Document ended before \" + endTag + \" end tag\");\n    }\n\n    /**\n     * Read a long[] object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeLongArrayXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the list.\n     *\n     * @param parser The XmlPullParser from which to read the list data.\n     * @param endTag Name of the tag that will end the list, usually \"list\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the list's tag.\n     * @return Returns a newly generated long[].\n     * @see #readListXml\n     */\n    public static long[] readThisLongArrayXml(XmlPullParser parser,\n                                              String endTag, String[] name)\n            throws XmlPullParserException, java.io.IOException {\n\n        int num;\n        try {\n            num = Integer.parseInt(parser.getAttributeValue(null, \"num\"));\n        } catch (NullPointerException e) {\n            throw new XmlPullParserException(\"Need num attribute in long-array\");\n        } catch (NumberFormatException e) {\n            throw new XmlPullParserException(\"Not a number in num attribute in long-array\");\n        }\n        parser.next();\n\n        long[] array = new long[num];\n        int i = 0;\n\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                if (parser.getName().equals(\"item\")) {\n                    try {\n                        array[i] = Long.parseLong(parser.getAttributeValue(null, \"value\"));\n                    } catch (NullPointerException e) {\n                        throw new XmlPullParserException(\"Need value attribute in item\");\n                    } catch (NumberFormatException e) {\n                        throw new XmlPullParserException(\"Not a number in value attribute in item\");\n                    }\n                } else {\n                    throw new XmlPullParserException(\"Expected item tag at: \" + parser.getName());\n                }\n            } else if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(endTag)) {\n                    return array;\n                } else if (parser.getName().equals(\"item\")) {\n                    i++;\n                } else {\n                    throw new XmlPullParserException(\"Expected \" + endTag + \" end tag at: \" +\n                            parser.getName());\n                }\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\"Document ended before \" + endTag + \" end tag\");\n    }\n\n    /**\n     * Read a double[] object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeDoubleArrayXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the list.\n     *\n     * @param parser The XmlPullParser from which to read the list data.\n     * @param endTag Name of the tag that will end the list, usually \"double-array\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the list's tag.\n     * @return Returns a newly generated double[].\n     * @see #readListXml\n     */\n    public static double[] readThisDoubleArrayXml(XmlPullParser parser, String endTag,\n                                                  String[] name) throws XmlPullParserException, java.io.IOException {\n\n        int num;\n        try {\n            num = Integer.parseInt(parser.getAttributeValue(null, \"num\"));\n        } catch (NullPointerException e) {\n            throw new XmlPullParserException(\"Need num attribute in double-array\");\n        } catch (NumberFormatException e) {\n            throw new XmlPullParserException(\"Not a number in num attribute in double-array\");\n        }\n        parser.next();\n\n        double[] array = new double[num];\n        int i = 0;\n\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                if (parser.getName().equals(\"item\")) {\n                    try {\n                        array[i] = Double.parseDouble(parser.getAttributeValue(null, \"value\"));\n                    } catch (NullPointerException e) {\n                        throw new XmlPullParserException(\"Need value attribute in item\");\n                    } catch (NumberFormatException e) {\n                        throw new XmlPullParserException(\"Not a number in value attribute in item\");\n                    }\n                } else {\n                    throw new XmlPullParserException(\"Expected item tag at: \" + parser.getName());\n                }\n            } else if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(endTag)) {\n                    return array;\n                } else if (parser.getName().equals(\"item\")) {\n                    i++;\n                } else {\n                    throw new XmlPullParserException(\"Expected \" + endTag + \" end tag at: \" +\n                            parser.getName());\n                }\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\"Document ended before \" + endTag + \" end tag\");\n    }\n\n    /**\n     * Read a String[] object from an XmlPullParser.  The XML data could\n     * previously have been generated by writeStringArrayXml().  The XmlPullParser\n     * must be positioned <em>after</em> the tag that begins the list.\n     *\n     * @param parser The XmlPullParser from which to read the list data.\n     * @param endTag Name of the tag that will end the list, usually \"string-array\".\n     * @param name   An array of one string, used to return the name attribute\n     *               of the list's tag.\n     * @return Returns a newly generated String[].\n     * @see #readListXml\n     */\n    public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag,\n                                                  String[] name) throws XmlPullParserException, java.io.IOException {\n\n        int num;\n        try {\n            num = Integer.parseInt(parser.getAttributeValue(null, \"num\"));\n        } catch (NullPointerException e) {\n            throw new XmlPullParserException(\"Need num attribute in string-array\");\n        } catch (NumberFormatException e) {\n            throw new XmlPullParserException(\"Not a number in num attribute in string-array\");\n        }\n        parser.next();\n\n        String[] array = new String[num];\n        int i = 0;\n\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                if (parser.getName().equals(\"item\")) {\n                    try {\n                        array[i] = parser.getAttributeValue(null, \"value\");\n                    } catch (NullPointerException e) {\n                        throw new XmlPullParserException(\"Need value attribute in item\");\n                    } catch (NumberFormatException e) {\n                        throw new XmlPullParserException(\"Not a number in value attribute in item\");\n                    }\n                } else {\n                    throw new XmlPullParserException(\"Expected item tag at: \" + parser.getName());\n                }\n            } else if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(endTag)) {\n                    return array;\n                } else if (parser.getName().equals(\"item\")) {\n                    i++;\n                } else {\n                    throw new XmlPullParserException(\"Expected \" + endTag + \" end tag at: \" +\n                            parser.getName());\n                }\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\"Document ended before \" + endTag + \" end tag\");\n    }\n\n    /**\n     * Read a flattened object from an XmlPullParser.  The XML data could\n     * previously have been written with writeMapXml(), writeListXml(), or\n     * writeValueXml().  The XmlPullParser must be positioned <em>at</em> the\n     * tag that defines the value.\n     *\n     * @param parser The XmlPullParser from which to read the object.\n     * @param name   An array of one string, used to return the name attribute\n     *               of the value's tag.\n     * @return Object The newly generated value object.\n     * @see #readMapXml\n     * @see #readListXml\n     * @see #writeValueXml\n     */\n    public static Object readValueXml(XmlPullParser parser, String[] name)\n            throws XmlPullParserException, java.io.IOException {\n        int eventType = parser.getEventType();\n        do {\n            if (eventType == parser.START_TAG) {\n                return readThisValueXml(parser, name, null);\n            } else if (eventType == parser.END_TAG) {\n                throw new XmlPullParserException(\n                        \"Unexpected end tag at: \" + parser.getName());\n            } else if (eventType == parser.TEXT) {\n                throw new XmlPullParserException(\n                        \"Unexpected text: \" + parser.getText());\n            }\n            eventType = parser.next();\n        } while (eventType != parser.END_DOCUMENT);\n\n        throw new XmlPullParserException(\n                \"Unexpected end of document\");\n    }\n\n    private static Object readThisValueXml(XmlPullParser parser, String[] name,\n                                           ReadMapCallback callback) throws XmlPullParserException, java.io.IOException {\n        final String valueName = parser.getAttributeValue(null, \"name\");\n        final String tagName = parser.getName();\n\n        //System.out.println(\"Reading this value tag: \" + tagName + \", name=\" + valueName);\n\n        Object res;\n\n        if (tagName.equals(\"null\")) {\n            res = null;\n        } else if (tagName.equals(\"string\")) {\n            String value = \"\";\n            int eventType;\n            while ((eventType = parser.next()) != parser.END_DOCUMENT) {\n                if (eventType == parser.END_TAG) {\n                    if (parser.getName().equals(\"string\")) {\n                        name[0] = valueName;\n                        //System.out.println(\"Returning value for \" + valueName + \": \" + value);\n                        return value;\n                    }\n                    throw new XmlPullParserException(\n                            \"Unexpected end tag in <string>: \" + parser.getName());\n                } else if (eventType == parser.TEXT) {\n                    value += parser.getText();\n                } else if (eventType == parser.START_TAG) {\n                    throw new XmlPullParserException(\n                            \"Unexpected start tag in <string>: \" + parser.getName());\n                }\n            }\n            throw new XmlPullParserException(\n                    \"Unexpected end of document in <string>\");\n        } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {\n            // all work already done by readThisPrimitiveValueXml\n        } else if (tagName.equals(\"int-array\")) {\n            res = readThisIntArrayXml(parser, \"int-array\", name);\n            name[0] = valueName;\n            //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n            return res;\n        } else if (tagName.equals(\"long-array\")) {\n            res = readThisLongArrayXml(parser, \"long-array\", name);\n            name[0] = valueName;\n            //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n            return res;\n        } else if (tagName.equals(\"double-array\")) {\n            res = readThisDoubleArrayXml(parser, \"double-array\", name);\n            name[0] = valueName;\n            //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n            return res;\n        } else if (tagName.equals(\"string-array\")) {\n            res = readThisStringArrayXml(parser, \"string-array\", name);\n            name[0] = valueName;\n            //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n            return res;\n        } else if (tagName.equals(\"map\")) {\n            parser.next();\n            res = readThisMapXml(parser, \"map\", name);\n            name[0] = valueName;\n            //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n            return res;\n        } else if (tagName.equals(\"list\")) {\n            parser.next();\n            res = readThisListXml(parser, \"list\", name);\n            name[0] = valueName;\n            //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n            return res;\n        } else if (tagName.equals(\"set\")) {\n            parser.next();\n            res = readThisSetXml(parser, \"set\", name);\n            name[0] = valueName;\n            //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n            return res;\n        } else if (callback != null) {\n            res = callback.readThisUnknownObjectXml(parser, tagName);\n            name[0] = valueName;\n            return res;\n        } else {\n            throw new XmlPullParserException(\"Unknown tag: \" + tagName);\n        }\n\n        // Skip through to end tag.\n        int eventType;\n        while ((eventType = parser.next()) != parser.END_DOCUMENT) {\n            if (eventType == parser.END_TAG) {\n                if (parser.getName().equals(tagName)) {\n                    name[0] = valueName;\n                    //System.out.println(\"Returning value for \" + valueName + \": \" + res);\n                    return res;\n                }\n                throw new XmlPullParserException(\n                        \"Unexpected end tag in <\" + tagName + \">: \" + parser.getName());\n            } else if (eventType == parser.TEXT) {\n                throw new XmlPullParserException(\n                        \"Unexpected text in <\" + tagName + \">: \" + parser.getName());\n            } else if (eventType == parser.START_TAG) {\n                throw new XmlPullParserException(\n                        \"Unexpected start tag in <\" + tagName + \">: \" + parser.getName());\n            }\n        }\n        throw new XmlPullParserException(\n                \"Unexpected end of document in <\" + tagName + \">\");\n    }\n\n    private static Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)\n            throws XmlPullParserException, java.io.IOException {\n        try {\n            switch (tagName) {\n                case \"int\":\n                    return Integer.parseInt(parser.getAttributeValue(null, \"value\"));\n                case \"long\":\n                    return Long.valueOf(parser.getAttributeValue(null, \"value\"));\n                case \"float\":\n                    return Float.valueOf(parser.getAttributeValue(null, \"value\"));\n                case \"double\":\n                    return Double.valueOf(parser.getAttributeValue(null, \"value\"));\n                case \"boolean\":\n                    return Boolean.valueOf(parser.getAttributeValue(null, \"value\"));\n                default:\n                    return null;\n            }\n        } catch (NullPointerException e) {\n            throw new XmlPullParserException(\"Need value attribute in <\" + tagName + \">\");\n        } catch (NumberFormatException e) {\n            throw new XmlPullParserException(\n                    \"Not a number in value attribute in <\" + tagName + \">\");\n        }\n    }\n\n    public static void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException {\n        int type;\n        while ((type = parser.next()) != parser.START_TAG\n                && type != parser.END_DOCUMENT) {\n        }\n\n        if (type != parser.START_TAG) {\n            throw new XmlPullParserException(\"No start tag found\");\n        }\n\n        if (!parser.getName().equals(firstElementName)) {\n            throw new XmlPullParserException(\"Unexpected start tag: found \" + parser.getName() +\n                    \", expected \" + firstElementName);\n        }\n    }\n\n    public static void nextElement(XmlPullParser parser) throws XmlPullParserException, IOException {\n        int type;\n        while ((type = parser.next()) != parser.START_TAG\n                && type != parser.END_DOCUMENT) {\n        }\n    }\n\n    public static boolean nextElementWithin(XmlPullParser parser, int outerDepth)\n            throws IOException, XmlPullParserException {\n        for (; ; ) {\n            int type = parser.next();\n            if (type == XmlPullParser.END_DOCUMENT\n                    || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) {\n                return false;\n            }\n            if (type == XmlPullParser.START_TAG\n                    && parser.getDepth() == outerDepth + 1) {\n                return true;\n            }\n        }\n    }\n\n    public static int readIntAttribute(XmlPullParser in, String name, int defaultValue) {\n        final String value = in.getAttributeValue(null, name);\n        try {\n            return Integer.parseInt(value);\n        } catch (NumberFormatException e) {\n            return defaultValue;\n        }\n    }\n\n    public static int readIntAttribute(XmlPullParser in, String name) throws IOException {\n        final String value = in.getAttributeValue(null, name);\n        try {\n            return Integer.parseInt(value);\n        } catch (NumberFormatException e) {\n            throw new ProtocolException(\"problem parsing \" + name + \"=\" + value + \" as int\");\n        }\n    }\n\n    public static void writeIntAttribute(XmlSerializer out, String name, int value)\n            throws IOException {\n        out.attribute(null, name, Integer.toString(value));\n    }\n\n    public static long readLongAttribute(XmlPullParser in, String name, long defaultValue) {\n        final String value = in.getAttributeValue(null, name);\n        try {\n            return Long.parseLong(value);\n        } catch (NumberFormatException e) {\n            return defaultValue;\n        }\n    }\n\n    public static long readLongAttribute(XmlPullParser in, String name) throws IOException {\n        final String value = in.getAttributeValue(null, name);\n        try {\n            return Long.parseLong(value);\n        } catch (NumberFormatException e) {\n            throw new ProtocolException(\"problem parsing \" + name + \"=\" + value + \" as long\");\n        }\n    }\n\n    public static void writeLongAttribute(XmlSerializer out, String name, long value)\n            throws IOException {\n        out.attribute(null, name, Long.toString(value));\n    }\n\n    public static float readFloatAttribute(XmlPullParser in, String name) throws IOException {\n        final String value = in.getAttributeValue(null, name);\n        try {\n            return Float.parseFloat(value);\n        } catch (NumberFormatException e) {\n            throw new ProtocolException(\"problem parsing \" + name + \"=\" + value + \" as long\");\n        }\n    }\n\n    public static void writeFloatAttribute(XmlSerializer out, String name, float value)\n            throws IOException {\n        out.attribute(null, name, Float.toString(value));\n    }\n\n    public static boolean readBooleanAttribute(XmlPullParser in, String name) {\n        final String value = in.getAttributeValue(null, name);\n        return Boolean.parseBoolean(value);\n    }\n\n    public static boolean readBooleanAttribute(XmlPullParser in, String name,\n                                               boolean defaultValue) {\n        final String value = in.getAttributeValue(null, name);\n        if (value == null) {\n            return defaultValue;\n        } else {\n            return Boolean.parseBoolean(value);\n        }\n    }\n\n    public static void writeBooleanAttribute(XmlSerializer out, String name, boolean value)\n            throws IOException {\n        out.attribute(null, name, Boolean.toString(value));\n    }\n\n    public static Uri readUriAttribute(XmlPullParser in, String name) {\n        final String value = in.getAttributeValue(null, name);\n        return (value != null) ? Uri.parse(value) : null;\n    }\n\n    public static void writeUriAttribute(XmlSerializer out, String name, Uri value)\n            throws IOException {\n        if (value != null) {\n            out.attribute(null, name, value.toString());\n        }\n    }\n\n    public static String readStringAttribute(XmlPullParser in, String name) {\n        return in.getAttributeValue(null, name);\n    }\n\n    public static void writeStringAttribute(XmlSerializer out, String name, String value)\n            throws IOException {\n        if (value != null) {\n            out.attribute(null, name, value);\n        }\n    }\n\n    public static byte[] readByteArrayAttribute(XmlPullParser in, String name) {\n        final String value = in.getAttributeValue(null, name);\n        if (value != null) {\n            return Base64.decode(value, Base64.DEFAULT);\n        } else {\n            return null;\n        }\n    }\n\n    public static void writeByteArrayAttribute(XmlSerializer out, String name, byte[] value)\n            throws IOException {\n        if (value != null) {\n            out.attribute(null, name, Base64.encodeToString(value, Base64.DEFAULT));\n        }\n    }\n\n    public static Bitmap readBitmapAttribute(XmlPullParser in, String name) {\n        final byte[] value = readByteArrayAttribute(in, name);\n        if (value != null) {\n            return BitmapFactory.decodeByteArray(value, 0, value.length);\n        } else {\n            return null;\n        }\n    }\n\n    @Deprecated\n    public static void writeBitmapAttribute(XmlSerializer out, String name, Bitmap value)\n            throws IOException {\n        if (value != null) {\n            final ByteArrayOutputStream os = new ByteArrayOutputStream();\n            value.compress(CompressFormat.PNG, 90, os);\n            writeByteArrayAttribute(out, name, os.toByteArray());\n        }\n    }\n\n    /**\n     * @hide\n     */\n    public interface WriteMapCallback {\n        /**\n         * Called from writeMapXml when an Object type is not recognized. The implementer\n         * must write out the entire element including start and end tags.\n         *\n         * @param v    The object to be written out\n         * @param name The mapping key for v. Must be written into the \"name\" attribute of the\n         *             start tag.\n         * @param out  The XML output stream.\n         * @throws XmlPullParserException on unrecognized Object type.\n         * @throws IOException            on XmlSerializer serialization errors.\n         * @hide\n         */\n        void writeUnknownObject(Object v, String name, XmlSerializer out)\n                throws XmlPullParserException, IOException;\n    }\n\n    /**\n     * @hide\n     */\n    public interface ReadMapCallback {\n        /**\n         * Called from readThisMapXml when a START_TAG is not recognized. The input stream\n         * is positioned within the start tag so that attributes can be read using in.getAttribute.\n         *\n         * @param in  the XML input stream\n         * @param tag the START_TAG that was not recognized.\n         * @return the Object parsed from the stream which will be put into the map.\n         * @throws XmlPullParserException if the START_TAG is not recognized.\n         * @throws IOException            on XmlPullParser serialization errors.\n         * @hide\n         */\n        Object readThisUnknownObjectXml(XmlPullParser in, String tag)\n                throws XmlPullParserException, IOException;\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/ZipUtils.java",
    "content": "package com.kunfei.bookshelf.utils;\n\nimport android.util.Log;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\nimport java.util.zip.ZipOutputStream;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     time  : 2016/08/27\n *     desc  : utils about zip\n * </pre>\n */\n@SuppressWarnings({\"unused\", \"WeakerAccess\"})\npublic final class ZipUtils {\n\n    private static final int BUFFER_LEN = 8192;\n\n    private ZipUtils() {\n        throw new UnsupportedOperationException(\"u can't instantiate me...\");\n    }\n\n    /**\n     * Zip the files.\n     *\n     * @param srcFiles    The source of files.\n     * @param zipFilePath The path of ZIP file.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFiles(final Collection<String> srcFiles,\n                                   final String zipFilePath)\n            throws IOException {\n        return zipFiles(srcFiles, zipFilePath, null);\n    }\n\n    /**\n     * Zip the files.\n     *\n     * @param srcFilePaths The paths of source files.\n     * @param zipFilePath  The path of ZIP file.\n     * @param comment      The comment.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFiles(final Collection<String> srcFilePaths,\n                                   final String zipFilePath,\n                                   final String comment)\n            throws IOException {\n        if (srcFilePaths == null || zipFilePath == null) return false;\n        ZipOutputStream zos = null;\n        try {\n            zos = new ZipOutputStream(new FileOutputStream(zipFilePath));\n            for (String srcFile : srcFilePaths) {\n                if (!zipFile(getFileByPath(srcFile), \"\", zos, comment)) return false;\n            }\n            return true;\n        } finally {\n            if (zos != null) {\n                zos.finish();\n                zos.close();\n            }\n        }\n    }\n\n    /**\n     * Zip the files.\n     *\n     * @param srcFiles The source of files.\n     * @param zipFile  The ZIP file.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFiles(final Collection<File> srcFiles, final File zipFile)\n            throws IOException {\n        return zipFiles(srcFiles, zipFile, null);\n    }\n\n    /**\n     * Zip the files.\n     *\n     * @param srcFiles The source of files.\n     * @param zipFile  The ZIP file.\n     * @param comment  The comment.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFiles(final Collection<File> srcFiles,\n                                   final File zipFile,\n                                   final String comment)\n            throws IOException {\n        if (srcFiles == null || zipFile == null) return false;\n        ZipOutputStream zos = null;\n        try {\n            zos = new ZipOutputStream(new FileOutputStream(zipFile));\n            for (File srcFile : srcFiles) {\n                if (!zipFile(srcFile, \"\", zos, comment)) return false;\n            }\n            return true;\n        } finally {\n            if (zos != null) {\n                zos.finish();\n                zos.close();\n            }\n        }\n    }\n\n    /**\n     * Zip the file.\n     *\n     * @param srcFilePath The path of source file.\n     * @param zipFilePath The path of ZIP file.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFile(final String srcFilePath,\n                                  final String zipFilePath)\n            throws IOException {\n        return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), null);\n    }\n\n    /**\n     * Zip the file.\n     *\n     * @param srcFilePath The path of source file.\n     * @param zipFilePath The path of ZIP file.\n     * @param comment     The comment.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFile(final String srcFilePath,\n                                  final String zipFilePath,\n                                  final String comment)\n            throws IOException {\n        return zipFile(getFileByPath(srcFilePath), getFileByPath(zipFilePath), comment);\n    }\n\n    /**\n     * Zip the file.\n     *\n     * @param srcFile The source of file.\n     * @param zipFile The ZIP file.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFile(final File srcFile,\n                                  final File zipFile)\n            throws IOException {\n        return zipFile(srcFile, zipFile, null);\n    }\n\n    /**\n     * Zip the file.\n     *\n     * @param srcFile The source of file.\n     * @param zipFile The ZIP file.\n     * @param comment The comment.\n     * @return {@code true}: success<br>{@code false}: fail\n     * @throws IOException if an I/O error has occurred\n     */\n    public static boolean zipFile(final File srcFile,\n                                  final File zipFile,\n                                  final String comment)\n            throws IOException {\n        if (srcFile == null || zipFile == null) return false;\n        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {\n            return zipFile(srcFile, \"\", zos, comment);\n        }\n    }\n\n    private static boolean zipFile(final File srcFile,\n                                   String rootPath,\n                                   final ZipOutputStream zos,\n                                   final String comment) throws IOException {\n        if (!srcFile.exists()) return true;\n        rootPath = rootPath + (isSpace(rootPath) ? \"\" : File.separator) + srcFile.getName();\n        if (srcFile.isDirectory()) {\n            File[] fileList = srcFile.listFiles();\n            if (fileList == null || fileList.length <= 0) {\n                ZipEntry entry = new ZipEntry(rootPath + '/');\n                entry.setComment(comment);\n                zos.putNextEntry(entry);\n                zos.closeEntry();\n            } else {\n                for (File file : fileList) {\n                    if (!zipFile(file, rootPath, zos, comment)) return false;\n                }\n            }\n        } else {\n            try (InputStream is = new BufferedInputStream(new FileInputStream(srcFile))) {\n                ZipEntry entry = new ZipEntry(rootPath);\n                entry.setComment(comment);\n                zos.putNextEntry(entry);\n                byte buffer[] = new byte[BUFFER_LEN];\n                int len;\n                while ((len = is.read(buffer, 0, BUFFER_LEN)) != -1) {\n                    zos.write(buffer, 0, len);\n                }\n                zos.closeEntry();\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Unzip the file.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @param destDirPath The path of destination directory.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    public static List<File> unzipFile(final String zipFilePath, final String destDirPath) throws IOException {\n        return unzipFileByKeyword(zipFilePath, destDirPath, null);\n    }\n\n    /**\n     * Unzip the file.\n     *\n     * @param zipFile The ZIP file.\n     * @param destDir The destination directory.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    public static List<File> unzipFile(final File zipFile,\n                                       final File destDir)\n            throws IOException {\n        return unzipFileByKeyword(zipFile, destDir, null);\n    }\n\n    /**\n     * Unzip the file by keyword.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @param destDirPath The path of destination directory.\n     * @param keyword     The keyboard.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    public static List<File> unzipFileByKeyword(final String zipFilePath,\n                                                final String destDirPath,\n                                                final String keyword) throws IOException {\n        return unzipFileByKeyword(getFileByPath(zipFilePath), getFileByPath(destDirPath), keyword);\n    }\n\n    /**\n     * Unzip the file by keyword.\n     *\n     * @param zipFile The ZIP file.\n     * @param destDir The destination directory.\n     * @param keyword The keyboard.\n     * @return the unzipped files\n     * @throws IOException if unzip unsuccessfully\n     */\n    public static List<File> unzipFileByKeyword(final File zipFile,\n                                                final File destDir,\n                                                final String keyword) throws IOException {\n        if (zipFile == null || destDir == null) return null;\n        List<File> files = new ArrayList<>();\n        ZipFile zip = new ZipFile(zipFile);\n        Enumeration<?> entries = zip.entries();\n        try {\n            if (isSpace(keyword)) {\n                while (entries.hasMoreElements()) {\n                    ZipEntry entry = ((ZipEntry) entries.nextElement());\n                    String entryName = entry.getName();\n                    if (entryName.contains(\"../\")) {\n                        Log.e(\"ZipUtils\", \"entryName: \" + entryName + \" is dangerous!\");\n                        continue;\n                    }\n                    if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files;\n                }\n            } else {\n                while (entries.hasMoreElements()) {\n                    ZipEntry entry = ((ZipEntry) entries.nextElement());\n                    String entryName = entry.getName();\n                    if (entryName.contains(\"../\")) {\n                        Log.e(\"ZipUtils\", \"entryName: \" + entryName + \" is dangerous!\");\n                        continue;\n                    }\n                    if (entryName.contains(keyword)) {\n                        if (!unzipChildFile(destDir, files, zip, entry, entryName)) return files;\n                    }\n                }\n            }\n        } finally {\n            zip.close();\n        }\n        return files;\n    }\n\n    private static boolean unzipChildFile(final File destDir,\n                                          final List<File> files,\n                                          final ZipFile zip,\n                                          final ZipEntry entry,\n                                          final String name) throws IOException {\n        File file = new File(destDir, name);\n        files.add(file);\n        if (entry.isDirectory()) {\n            return createOrExistsDir(file);\n        } else {\n            if (!createOrExistsFile(file)) return false;\n            try (InputStream in = new BufferedInputStream(zip.getInputStream(entry)); OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {\n                byte buffer[] = new byte[BUFFER_LEN];\n                int len;\n                while ((len = in.read(buffer)) != -1) {\n                    out.write(buffer, 0, len);\n                }\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Return the files' path in ZIP file.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @return the files' path in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    public static List<String> getFilesPath(final String zipFilePath)\n            throws IOException {\n        return getFilesPath(getFileByPath(zipFilePath));\n    }\n\n    /**\n     * Return the files' path in ZIP file.\n     *\n     * @param zipFile The ZIP file.\n     * @return the files' path in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    public static List<String> getFilesPath(final File zipFile)\n            throws IOException {\n        if (zipFile == null) return null;\n        List<String> paths = new ArrayList<>();\n        ZipFile zip = new ZipFile(zipFile);\n        Enumeration<?> entries = zip.entries();\n        while (entries.hasMoreElements()) {\n            String entryName = ((ZipEntry) entries.nextElement()).getName();\n            if (entryName.contains(\"../\")) {\n                Log.e(\"ZipUtils\", \"entryName: \" + entryName + \" is dangerous!\");\n                paths.add(entryName);\n            } else {\n                paths.add(entryName);\n            }\n        }\n        zip.close();\n        return paths;\n    }\n\n    /**\n     * Return the files' comment in ZIP file.\n     *\n     * @param zipFilePath The path of ZIP file.\n     * @return the files' comment in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    public static List<String> getComments(final String zipFilePath)\n            throws IOException {\n        return getComments(getFileByPath(zipFilePath));\n    }\n\n    /**\n     * Return the files' comment in ZIP file.\n     *\n     * @param zipFile The ZIP file.\n     * @return the files' comment in ZIP file\n     * @throws IOException if an I/O error has occurred\n     */\n    public static List<String> getComments(final File zipFile)\n            throws IOException {\n        if (zipFile == null) return null;\n        List<String> comments = new ArrayList<>();\n        ZipFile zip = new ZipFile(zipFile);\n        Enumeration<?> entries = zip.entries();\n        while (entries.hasMoreElements()) {\n            ZipEntry entry = ((ZipEntry) entries.nextElement());\n            comments.add(entry.getComment());\n        }\n        zip.close();\n        return comments;\n    }\n\n    private static boolean createOrExistsDir(final File file) {\n        return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());\n    }\n\n    private static boolean createOrExistsFile(final File file) {\n        if (file == null) return false;\n        if (file.exists()) return file.isFile();\n        if (!createOrExistsDir(file.getParentFile())) return false;\n        try {\n            return file.createNewFile();\n        } catch (IOException e) {\n            e.printStackTrace();\n            return false;\n        }\n    }\n\n    private static File getFileByPath(final String filePath) {\n        return isSpace(filePath) ? null : new File(filePath);\n    }\n\n    private static boolean isSpace(final String s) {\n        if (s == null) return true;\n        for (int i = 0, len = s.length(); i < len; ++i) {\n            if (!Character.isWhitespace(s.charAt(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AlertBuilder.kt",
    "content": "@file:Suppress(\"NOTHING_TO_INLINE\", \"unused\")\n\npackage com.kunfei.bookshelf.utils.dialogs\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.graphics.drawable.Drawable\nimport android.view.KeyEvent\nimport android.view.View\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport com.kunfei.bookshelf.R\n\n@SuppressLint(\"SupportAnnotationUsage\")\ninterface AlertBuilder<out D : DialogInterface> {\n    val ctx: Context\n\n    fun setTitle(title: CharSequence)\n\n    fun setTitle(titleResource: Int)\n\n    fun setMessage(message: CharSequence)\n\n    fun setMessage(messageResource: Int)\n\n    fun setIcon(icon: Drawable)\n\n    fun setIcon(@DrawableRes iconResource: Int)\n\n    fun setCustomTitle(customTitle: View)\n\n    fun setCustomView(customView: View)\n\n    fun setCancelable(isCancelable: Boolean)\n\n    fun positiveButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null)\n    fun positiveButton(\n        @StringRes buttonTextResource: Int,\n        onClicked: ((dialog: DialogInterface) -> Unit)? = null\n    )\n\n    fun negativeButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null)\n    fun negativeButton(\n        @StringRes buttonTextResource: Int,\n        onClicked: ((dialog: DialogInterface) -> Unit)? = null\n    )\n\n    fun neutralButton(buttonText: String, onClicked: ((dialog: DialogInterface) -> Unit)? = null)\n    fun neutralButton(\n        @StringRes buttonTextResource: Int,\n        onClicked: ((dialog: DialogInterface) -> Unit)? = null\n    )\n\n    fun onCancelled(handler: (dialog: DialogInterface) -> Unit)\n\n    fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean)\n\n    fun onDismiss(handler: (dialog: DialogInterface) -> Unit)\n\n    fun items(\n        items: List<CharSequence>,\n        onItemSelected: (dialog: DialogInterface, index: Int) -> Unit\n    )\n\n    fun <T> items(\n        items: List<T>,\n        onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit\n    )\n\n    fun multiChoiceItems(\n        items: Array<String>,\n        checkedItems: BooleanArray,\n        onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit\n    )\n\n    fun singleChoiceItems(\n        items: Array<String>,\n        checkedItem: Int = 0,\n        onClick: ((dialog: DialogInterface, which: Int) -> Unit)? = null\n    )\n\n    fun build(): D\n    fun show(): D\n\n\n    fun customTitle(view: () -> View) {\n        setCustomTitle(view())\n    }\n\n    fun customView(view: () -> View) {\n        setCustomView(view())\n    }\n\n    fun okButton(handler: ((dialog: DialogInterface) -> Unit)? = null) =\n        positiveButton(android.R.string.ok, handler)\n\n    fun cancelButton(handler: ((dialog: DialogInterface) -> Unit)? = null) =\n        negativeButton(android.R.string.cancel, handler)\n\n    fun yesButton(handler: ((dialog: DialogInterface) -> Unit)? = null) =\n        positiveButton(R.string.yes, handler)\n\n    fun noButton(handler: ((dialog: DialogInterface) -> Unit)? = null) =\n        negativeButton(R.string.no, handler)\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AndroidAlertBuilder.kt",
    "content": "package com.kunfei.bookshelf.utils.dialogs\n\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.graphics.drawable.Drawable\nimport android.view.KeyEvent\nimport android.view.View\nimport androidx.appcompat.app.AlertDialog\nimport com.kunfei.bookshelf.utils.applyTint\n\ninternal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<AlertDialog> {\n    private val builder = AlertDialog.Builder(ctx)\n\n    override fun setTitle(title: CharSequence) {\n        builder.setTitle(title)\n    }\n\n    override fun setTitle(titleResource: Int) {\n        builder.setTitle(titleResource)\n    }\n\n    override fun setMessage(message: CharSequence) {\n        builder.setMessage(message)\n    }\n\n    override fun setMessage(messageResource: Int) {\n        builder.setMessage(messageResource)\n    }\n\n    override fun setIcon(icon: Drawable) {\n        builder.setIcon(icon)\n    }\n\n    override fun setIcon(iconResource: Int) {\n        builder.setIcon(iconResource)\n    }\n\n    override fun setCustomTitle(customTitle: View) {\n        builder.setCustomTitle(customTitle)\n    }\n\n    override fun setCustomView(customView: View) {\n        builder.setView(customView)\n    }\n\n    override fun setCancelable(isCancelable: Boolean) {\n        builder.setCancelable(isCancelable)\n    }\n\n    override fun onCancelled(handler: (DialogInterface) -> Unit) {\n        builder.setOnCancelListener(handler)\n    }\n\n    override fun onKeyPressed(handler: (dialog: DialogInterface, keyCode: Int, e: KeyEvent) -> Boolean) {\n        builder.setOnKeyListener(handler)\n    }\n\n    override fun positiveButton(\n        buttonText: String,\n        onClicked: ((dialog: DialogInterface) -> Unit)?\n    ) {\n        builder.setPositiveButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) }\n    }\n\n    override fun positiveButton(\n        buttonTextResource: Int,\n        onClicked: ((dialog: DialogInterface) -> Unit)?\n    ) {\n        builder.setPositiveButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) }\n    }\n\n    override fun negativeButton(\n        buttonText: String,\n        onClicked: ((dialog: DialogInterface) -> Unit)?\n    ) {\n        builder.setNegativeButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) }\n    }\n\n    override fun negativeButton(\n        buttonTextResource: Int,\n        onClicked: ((dialog: DialogInterface) -> Unit)?\n    ) {\n        builder.setNegativeButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) }\n    }\n\n    override fun neutralButton(\n        buttonText: String,\n        onClicked: ((dialog: DialogInterface) -> Unit)?\n    ) {\n        builder.setNeutralButton(buttonText) { dialog, _ -> onClicked?.invoke(dialog) }\n    }\n\n    override fun neutralButton(\n        buttonTextResource: Int,\n        onClicked: ((dialog: DialogInterface) -> Unit)?\n    ) {\n        builder.setNeutralButton(buttonTextResource) { dialog, _ -> onClicked?.invoke(dialog) }\n    }\n\n    override fun onDismiss(handler: (dialog: DialogInterface) -> Unit) {\n        builder.setOnDismissListener(handler)\n    }\n\n    override fun items(\n        items: List<CharSequence>,\n        onItemSelected: (dialog: DialogInterface, index: Int) -> Unit\n    ) {\n        builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which ->\n            onItemSelected(dialog, which)\n        }\n    }\n\n    override fun <T> items(\n        items: List<T>,\n        onItemSelected: (dialog: DialogInterface, item: T, index: Int) -> Unit\n    ) {\n        builder.setItems(Array(items.size) { i -> items[i].toString() }) { dialog, which ->\n            onItemSelected(dialog, items[which], which)\n        }\n    }\n\n    override fun multiChoiceItems(\n        items: Array<String>,\n        checkedItems: BooleanArray,\n        onClick: (dialog: DialogInterface, which: Int, isChecked: Boolean) -> Unit\n    ) {\n        builder.setMultiChoiceItems(items, checkedItems) { dialog, which, isChecked ->\n            onClick(dialog, which, isChecked)\n        }\n    }\n\n    override fun singleChoiceItems(\n        items: Array<String>,\n        checkedItem: Int,\n        onClick: ((dialog: DialogInterface, which: Int) -> Unit)?\n    ) {\n        builder.setSingleChoiceItems(items, checkedItem) { dialog, which ->\n            onClick?.invoke(dialog, which)\n        }\n    }\n\n    override fun build(): AlertDialog = builder.create()\n\n    override fun show(): AlertDialog = builder.show().applyTint()\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AndroidDialogs.kt",
    "content": "@file:Suppress(\"NOTHING_TO_INLINE\", \"unused\", \"DEPRECATION\")\n\npackage io.legado.app.lib.dialogs\n\nimport android.app.ProgressDialog\nimport android.content.Context\nimport android.content.DialogInterface\nimport androidx.appcompat.app.AlertDialog\nimport androidx.fragment.app.Fragment\nimport com.kunfei.bookshelf.utils.dialogs.AlertBuilder\nimport com.kunfei.bookshelf.utils.dialogs.AndroidAlertBuilder\n\nfun Context.alert(\n    title: CharSequence? = null,\n    message: CharSequence? = null,\n    init: (AlertBuilder<DialogInterface>.() -> Unit)? = null\n): AlertDialog {\n    return AndroidAlertBuilder(this).apply {\n        if (title != null) {\n            this.setTitle(title)\n        }\n        if (message != null) {\n            this.setMessage(message)\n        }\n        if (init != null) init()\n    }.show()\n}\n\ninline fun Fragment.alert(\n    title: CharSequence? = null,\n    message: CharSequence? = null,\n    noinline init: (AlertBuilder<DialogInterface>.() -> Unit)? = null\n) = requireActivity().alert(title, message, init)\n\nfun Context.alert(\n    titleResource: Int? = null,\n    messageResource: Int? = null,\n    init: (AlertBuilder<DialogInterface>.() -> Unit)? = null\n): AlertDialog {\n    return AndroidAlertBuilder(this).apply {\n        if (titleResource != null) {\n            this.setTitle(titleResource)\n        }\n        if (messageResource != null) {\n            this.setMessage(messageResource)\n        }\n        if (init != null) init()\n    }.show()\n}\n\ninline fun Fragment.alert(\n    titleResource: Int? = null,\n    message: Int? = null,\n    noinline init: (AlertBuilder<DialogInterface>.() -> Unit)? = null\n) = requireActivity().alert(titleResource, message, init)\n\nfun Context.alert(init: AlertBuilder<AlertDialog>.() -> Unit): AlertDialog =\n    AndroidAlertBuilder(this).apply {\n        init()\n    }.show()\n\ninline fun Fragment.alert(noinline init: AlertBuilder<DialogInterface>.() -> Unit) =\n    requireContext().alert(init)\n\ninline fun Fragment.progressDialog(\n    title: Int? = null,\n    message: Int? = null,\n    noinline init: (ProgressDialog.() -> Unit)? = null\n) = requireActivity().progressDialog(title, message, init)\n\nfun Context.progressDialog(\n    title: Int? = null,\n    message: Int? = null,\n    init: (ProgressDialog.() -> Unit)? = null\n) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, false, init)\n\n\ninline fun Fragment.indeterminateProgressDialog(\n    title: Int? = null,\n    message: Int? = null,\n    noinline init: (ProgressDialog.() -> Unit)? = null\n) = requireActivity().indeterminateProgressDialog(title, message, init)\n\nfun Context.indeterminateProgressDialog(\n    title: Int? = null,\n    message: Int? = null,\n    init: (ProgressDialog.() -> Unit)? = null\n) = progressDialog(title?.let { getString(it) }, message?.let { getString(it) }, true, init)\n\ninline fun Fragment.progressDialog(\n    title: CharSequence? = null,\n    message: CharSequence? = null,\n    noinline init: (ProgressDialog.() -> Unit)? = null\n) = requireActivity().progressDialog(title, message, init)\n\nfun Context.progressDialog(\n    title: CharSequence? = null,\n    message: CharSequence? = null,\n    init: (ProgressDialog.() -> Unit)? = null\n) = progressDialog(title, message, false, init)\n\n\ninline fun Fragment.indeterminateProgressDialog(\n    title: CharSequence? = null,\n    message: CharSequence? = null,\n    noinline init: (ProgressDialog.() -> Unit)? = null\n) = requireActivity().indeterminateProgressDialog(title, message, init)\n\nfun Context.indeterminateProgressDialog(\n    title: CharSequence? = null,\n    message: CharSequence? = null,\n    init: (ProgressDialog.() -> Unit)? = null\n) = progressDialog(title, message, true, init)\n\n\nprivate fun Context.progressDialog(\n    title: CharSequence? = null,\n    message: CharSequence? = null,\n    indeterminate: Boolean,\n    init: (ProgressDialog.() -> Unit)? = null\n) = ProgressDialog(this).apply {\n    isIndeterminate = indeterminate\n    if (!indeterminate) setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)\n    if (message != null) setMessage(message)\n    if (title != null) setTitle(title)\n    if (init != null) init()\n    show()\n}\n\ntypealias AlertBuilderFactory<D> = (Context) -> AlertBuilder<D>\n\ninline fun <D : DialogInterface> Fragment.alert(\n    noinline factory: AlertBuilderFactory<D>,\n    title: String? = null,\n    message: String? = null,\n    noinline init: (AlertBuilder<D>.() -> Unit)? = null\n) = activity?.alert(factory, title, message, init)\n\nfun <D : DialogInterface> Context.alert(\n    factory: AlertBuilderFactory<D>,\n    title: String? = null,\n    message: String? = null,\n    init: (AlertBuilder<D>.() -> Unit)? = null\n): AlertBuilder<D> {\n    return factory(this).apply {\n        if (title != null) {\n            this.setTitle(title)\n        }\n        if (message != null) {\n            this.setMessage(message)\n        }\n        if (init != null) init()\n    }\n}\n\ninline fun <D : DialogInterface> Fragment.alert(\n    noinline factory: AlertBuilderFactory<D>,\n    titleResource: Int? = null,\n    messageResource: Int? = null,\n    noinline init: (AlertBuilder<D>.() -> Unit)? = null\n) = requireActivity().alert(factory, titleResource, messageResource, init)\n\nfun <D : DialogInterface> Context.alert(\n    factory: AlertBuilderFactory<D>,\n    titleResource: Int? = null,\n    messageResource: Int? = null,\n    init: (AlertBuilder<D>.() -> Unit)? = null\n): AlertBuilder<D> {\n    return factory(this).apply {\n        if (titleResource != null) {\n            this.setTitle(titleResource)\n        }\n        if (messageResource != null) {\n            this.setMessage(messageResource)\n        }\n        if (init != null) init()\n    }\n}\n\ninline fun <D : DialogInterface> Fragment.alert(\n    noinline factory: AlertBuilderFactory<D>,\n    noinline init: AlertBuilder<D>.() -> Unit\n) = requireActivity().alert(factory, init)\n\nfun <D : DialogInterface> Context.alert(\n    factory: AlertBuilderFactory<D>,\n    init: AlertBuilder<D>.() -> Unit\n): AlertBuilder<D> = factory(this).apply { init() }\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/dialogs/AndroidSelectors.kt",
    "content": "/*\n * Copyright 2016 JetBrains s.r.o.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n@file:Suppress(\"NOTHING_TO_INLINE\", \"unused\")\n\npackage io.legado.app.lib.dialogs\n\nimport android.content.Context\nimport android.content.DialogInterface\nimport com.kunfei.bookshelf.utils.dialogs.AndroidAlertBuilder\n\nfun Context.selector(\n    items: List<CharSequence>,\n    onClick: (DialogInterface, Int) -> Unit\n) {\n    with(AndroidAlertBuilder(this)) {\n        items(items, onClick)\n        show()\n    }\n}\n\nfun <T> Context.selector(\n    items: List<T>,\n    onClick: (DialogInterface, T, Int) -> Unit\n) {\n    with(AndroidAlertBuilder(this)) {\n        items(items, onClick)\n        show()\n    }\n}\n\nfun Context.selector(\n    title: CharSequence,\n    items: List<CharSequence>,\n    onClick: (DialogInterface, Int) -> Unit\n) {\n    with(AndroidAlertBuilder(this)) {\n        this.setTitle(title)\n        items(items, onClick)\n        show()\n    }\n}\n\nfun <T> Context.selector(\n    title: CharSequence,\n    items: List<T>,\n    onClick: (DialogInterface, T, Int) -> Unit\n) {\n    with(AndroidAlertBuilder(this)) {\n        this.setTitle(title)\n        items(items, onClick)\n        show()\n    }\n}\n\nfun Context.selector(\n    titleSource: Int,\n    items: List<CharSequence>,\n    onClick: (DialogInterface, Int) -> Unit\n) {\n    with(AndroidAlertBuilder(this)) {\n        this.setTitle(titleSource)\n        items(items, onClick)\n        show()\n    }\n}\n\nfun <T> Context.selector(\n    titleSource: Int,\n    items: List<T>,\n    onClick: (DialogInterface, T, Int) -> Unit\n) {\n    with(AndroidAlertBuilder(this)) {\n        this.setTitle(titleSource)\n        items(items, onClick)\n        show()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/dialogs/SelectItem.kt",
    "content": "package com.kunfei.bookshelf.utils.dialogs\n\n@Suppress(\"unused\")\ndata class SelectItem<T>(\n    val title: String,\n    val value: T\n) {\n\n    override fun toString(): String {\n        return title\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/download/DownloadUtils.java",
    "content": "package com.kunfei.bookshelf.utils.download;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\nimport java.io.FileNotFoundException;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.concurrent.TimeUnit;\n\nimport io.reactivex.Observer;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\nimport okhttp3.OkHttpClient;\nimport okhttp3.ResponseBody;\nimport retrofit2.Retrofit;\nimport retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;\n\npublic class DownloadUtils {\n    private static final String TAG = \"DownloadUtils\";\n    private static final int DEFAULT_TIMEOUT = 15;\n    private Retrofit retrofit;\n    private JsDownloadListener listener;\n\n    public DownloadUtils(String baseUrl, JsDownloadListener listener) {\n        this.listener = listener;\n        JsDownloadInterceptor mInterceptor = new JsDownloadInterceptor(listener);\n        OkHttpClient httpClient = new OkHttpClient.Builder()\n                .addInterceptor(mInterceptor)\n                .retryOnConnectionFailure(true)\n                .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)\n                .build();\n\n        retrofit = new Retrofit.Builder()\n                .baseUrl(baseUrl)\n                .client(httpClient)\n                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())\n                .build();\n    }\n\n    /**\n     * 开始下载\n     */\n    public void download(@NonNull String url, final File file, Observer<InputStream> subscriber) {\n        retrofit.create(Service.class)\n                .download(url)\n                .subscribeOn(Schedulers.io())\n                .unsubscribeOn(Schedulers.io())\n                .map(ResponseBody::byteStream)\n                .observeOn(Schedulers.computation()) // 用于计算任务\n                .doOnNext(inputStream -> writeFile(inputStream, file))\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(subscriber);\n    }\n\n    /**\n     * 将输入流写入文件\n     */\n    private void writeFile(InputStream inputString, File file) {\n        if (file.exists()) {\n            //noinspection ResultOfMethodCallIgnored\n            file.delete();\n        }\n\n        FileOutputStream fos = null;\n        try {\n            fos = new FileOutputStream(file);\n\n            byte[] b = new byte[1024];\n\n            int len;\n            while ((len = inputString.read(b)) != -1) {\n                fos.write(b, 0, len);\n            }\n            inputString.close();\n            fos.close();\n\n        } catch (FileNotFoundException e) {\n            listener.onFail(\"FileNotFoundException\");\n        } catch (IOException e) {\n            listener.onFail(\"IOException\");\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/download/JsDownloadInterceptor.java",
    "content": "package com.kunfei.bookshelf.utils.download;\n\nimport java.io.IOException;\n\nimport okhttp3.Interceptor;\nimport okhttp3.Response;\n\npublic class JsDownloadInterceptor implements Interceptor {\n    private JsDownloadListener downloadListener;\n\n    public JsDownloadInterceptor(JsDownloadListener downloadListener) {\n        this.downloadListener = downloadListener;\n    }\n\n    @Override\n    public Response intercept(Chain chain) throws IOException {\n        Response response = chain.proceed(chain.request());\n        return response.newBuilder().body(\n                new JsResponseBody(response.body(), downloadListener)).build();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/download/JsDownloadListener.java",
    "content": "package com.kunfei.bookshelf.utils.download;\n\npublic interface JsDownloadListener {\n    void onStartDownload(long length);\n\n    void onProgress(int progress);\n\n    void onFail(String errorInfo);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/download/JsResponseBody.java",
    "content": "package com.kunfei.bookshelf.utils.download;\n\nimport java.io.IOException;\n\nimport okhttp3.MediaType;\nimport okhttp3.ResponseBody;\nimport okio.Buffer;\nimport okio.BufferedSource;\nimport okio.ForwardingSource;\nimport okio.Okio;\nimport okio.Source;\n\npublic class JsResponseBody extends ResponseBody {\n    private ResponseBody responseBody;\n    private JsDownloadListener downloadListener;\n    // BufferedSource 是okio库中的输入流，这里就当作inputStream来使用。\n    private BufferedSource bufferedSource;\n\n    public JsResponseBody(ResponseBody responseBody, JsDownloadListener downloadListener) {\n        this.responseBody = responseBody;\n        this.downloadListener = downloadListener;\n        downloadListener.onStartDownload(responseBody.contentLength());\n    }\n\n    @Override\n    public MediaType contentType() {\n        return responseBody.contentType();\n    }\n\n    @Override\n    public long contentLength() {\n        return responseBody.contentLength();\n    }\n\n    @Override\n    public BufferedSource source() {\n        if (bufferedSource == null) {\n            bufferedSource = Okio.buffer(source(responseBody.source()));\n        }\n        return bufferedSource;\n    }\n\n    private Source source(Source source) {\n        return new ForwardingSource(source) {\n            long totalBytesRead = 0L;\n\n            @Override\n            public long read(Buffer sink, long byteCount) throws IOException {\n                long bytesRead = super.read(sink, byteCount);\n                totalBytesRead += bytesRead != -1 ? bytesRead : 0;\n                if (null != downloadListener) {\n                    if (bytesRead != -1) {\n                        downloadListener.onProgress((int) (totalBytesRead * 100 / responseBody.contentLength()));\n                    }\n                }\n                return bytesRead;\n            }\n        };\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/download/Service.java",
    "content": "package com.kunfei.bookshelf.utils.download;\n\nimport io.reactivex.Observable;\nimport okhttp3.ResponseBody;\nimport retrofit2.http.GET;\nimport retrofit2.http.Streaming;\nimport retrofit2.http.Url;\n\npublic interface Service {\n    @Streaming\n    @GET\n    Observable<ResponseBody> download(@Url String url);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/ATH.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\nimport static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.app.ActivityManager;\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.os.Build;\nimport android.view.View;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AlertDialog;\n\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.Selector;\n\n/**\n * @author Karim Abou Zeid (kabouzeid)\n */\npublic final class ATH {\n\n    @SuppressLint(\"CommitPrefEdits\")\n    public static boolean didThemeValuesChange(@NonNull Context context, long since) {\n        return ThemeStore.isConfigured(context) && ThemeStore.prefs(context).getLong(ThemeStore.VALUES_CHANGED, -1) > since;\n    }\n\n    public static void setStatusbarColorAuto(Activity activity) {\n        setStatusbarColor(activity, ThemeStore.statusBarColor(activity));\n    }\n\n    public static void setStatusbarColor(Activity activity, int color) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            activity.getWindow().setStatusBarColor(color);\n            setLightStatusbarAuto(activity, color);\n        }\n    }\n\n    public static void setLightStatusbarAuto(Activity activity, int bgColor) {\n        setLightStatusbar(activity, ColorUtils.isColorLight(bgColor));\n    }\n\n    public static void setLightStatusbar(Activity activity, boolean enabled) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n            final View decorView = activity.getWindow().getDecorView();\n            final int systemUiVisibility = decorView.getSystemUiVisibility();\n            if (enabled) {\n                decorView.setSystemUiVisibility(systemUiVisibility | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n            } else {\n                decorView.setSystemUiVisibility(systemUiVisibility & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n            }\n        }\n    }\n\n    public static void setLightNavigationbar(Activity activity, boolean enabled) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            final View decorView = activity.getWindow().getDecorView();\n            int systemUiVisibility = decorView.getSystemUiVisibility();\n            if (enabled) {\n                systemUiVisibility |= SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;\n            } else {\n                systemUiVisibility &= ~SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;\n            }\n            decorView.setSystemUiVisibility(systemUiVisibility);\n        }\n    }\n\n    public static void setLightNavigationbarAuto(Activity activity, int bgColor) {\n        setLightNavigationbar(activity, ColorUtils.isColorLight(bgColor));\n    }\n\n    public static void setNavigationbarColorAuto(Activity activity) {\n        setNavigationbarColor(activity, ThemeStore.navigationBarColor(activity));\n    }\n\n    public static void setNavigationbarColor(Activity activity, int color) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            activity.getWindow().setNavigationBarColor(color);\n            setLightNavigationbarAuto(activity, color);\n        }\n    }\n\n    public static void setTaskDescriptionColorAuto(@NonNull Activity activity) {\n        setTaskDescriptionColor(activity, ThemeStore.primaryColor(activity));\n    }\n\n    public static void setTaskDescriptionColor(@NonNull Activity activity, @ColorInt int color) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            // Task description requires fully opaque color\n            color = ColorUtils.stripAlpha(color);\n            // Sets color of entry in the system recents page\n            activity.setTaskDescription(new ActivityManager.TaskDescription((String) activity.getTitle(), null, color));\n        }\n    }\n\n    public static void setTint(@NonNull View view, @ColorInt int color) {\n        TintHelper.setTintAuto(view, color, false);\n    }\n\n    public static void setBackgroundTint(@NonNull View view, @ColorInt int color) {\n        TintHelper.setTintAuto(view, color, true);\n    }\n\n    public static AlertDialog setAlertDialogTint(@NonNull AlertDialog dialog) {\n        ColorStateList colorStateList = Selector.colorBuild()\n                .setDefaultColor(ThemeStore.accentColor(dialog.getContext()))\n                .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(dialog.getContext())))\n                .create();\n        if (dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE) != null) {\n            dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE).setTextColor(colorStateList);\n        }\n        if (dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE) != null) {\n            dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setTextColor(colorStateList);\n        }\n        return dialog;\n    }\n\n    private ATH() {\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/ATHUtil.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\n\nimport androidx.annotation.AttrRes;\n\nimport com.kunfei.bookshelf.utils.ColorUtils;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic final class ATHUtil {\n\n    public static boolean isWindowBackgroundDark(Context context) {\n        return !ColorUtils.isColorLight(ATHUtil.resolveColor(context, android.R.attr.windowBackground));\n    }\n\n    public static int resolveColor(Context context, @AttrRes int attr) {\n        return resolveColor(context, attr, 0);\n    }\n\n    public static int resolveColor(Context context, @AttrRes int attr, int fallback) {\n        TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});\n        try {\n            return a.getColor(0, fallback);\n        } finally {\n            a.recycle();\n        }\n    }\n\n    private ATHUtil() {\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/MaterialValueHelper.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n\nimport androidx.annotation.ColorInt;\nimport androidx.core.content.ContextCompat;\n\nimport com.kunfei.bookshelf.R;\n\n/**\n * @author Karim Abou Zeid (kabouzeid)\n */\npublic final class MaterialValueHelper {\n\n    @SuppressLint(\"PrivateResource\")\n    @ColorInt\n    public static int getPrimaryTextColor(final Context context, boolean dark) {\n        if (dark) {\n            return ContextCompat.getColor(context, R.color.primary_text_default_material_light);\n        }\n        return ContextCompat.getColor(context, R.color.primary_text_default_material_dark);\n    }\n\n    @SuppressLint(\"PrivateResource\")\n    @ColorInt\n    public static int getSecondaryTextColor(final Context context, boolean dark) {\n        if (dark) {\n            return ContextCompat.getColor(context, R.color.secondary_text_default_material_light);\n        }\n        return ContextCompat.getColor(context, R.color.secondary_text_default_material_dark);\n    }\n\n    @SuppressLint(\"PrivateResource\")\n    @ColorInt\n    public static int getPrimaryDisabledTextColor(final Context context, boolean dark) {\n        if (dark) {\n            return ContextCompat.getColor(context, R.color.primary_text_disabled_material_light);\n        }\n        return ContextCompat.getColor(context, R.color.primary_text_disabled_material_dark);\n    }\n\n    @SuppressLint(\"PrivateResource\")\n    @ColorInt\n    public static int getSecondaryDisabledTextColor(final Context context, boolean dark) {\n        if (dark) {\n            return ContextCompat.getColor(context, R.color.secondary_text_disabled_material_light);\n        }\n        return ContextCompat.getColor(context, R.color.secondary_text_disabled_material_dark);\n    }\n\n    private MaterialValueHelper() {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/MaterialValueHelper.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.kunfei.bookshelf.utils.theme\n\nimport android.content.Context\nimport android.graphics.drawable.GradientDrawable\nimport androidx.annotation.ColorInt\nimport androidx.core.content.ContextCompat\nimport androidx.fragment.app.Fragment\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.utils.ColorUtils\nimport com.kunfei.bookshelf.utils.dp\n\n/**\n * @author Karim Abou Zeid (kabouzeid)\n */\n@ColorInt\nfun Context.getPrimaryTextColor(dark: Boolean): Int {\n    return if (dark) {\n        ContextCompat.getColor(this, R.color.primary_text_default_material_light)\n    } else ContextCompat.getColor(this, R.color.primary_text_default_material_dark)\n}\n\n@ColorInt\nfun Context.getSecondaryTextColor(dark: Boolean): Int {\n    return if (dark) {\n        ContextCompat.getColor(this, R.color.secondary_text_default_material_light)\n    } else {\n        ContextCompat.getColor(this, R.color.secondary_text_default_material_dark)\n    }\n}\n\n@ColorInt\nfun Context.getPrimaryDisabledTextColor(dark: Boolean): Int {\n    return if (dark) {\n        ContextCompat.getColor(this, R.color.primary_text_disabled_material_light)\n    } else {\n        ContextCompat.getColor(this, R.color.primary_text_disabled_material_dark)\n    }\n}\n\n@ColorInt\nfun Context.getSecondaryDisabledTextColor(dark: Boolean): Int {\n    return if (dark) {\n        ContextCompat.getColor(this, R.color.secondary_text_disabled_material_light)\n    } else {\n        ContextCompat.getColor(this, R.color.secondary_text_disabled_material_dark)\n    }\n}\n\nval Context.primaryColor: Int\n    get() = ThemeStore.primaryColor(this)\n\nval Context.primaryColorDark: Int\n    get() = ThemeStore.primaryColorDark(this)\n\nval Context.accentColor: Int\n    get() = ThemeStore.accentColor(this)\n\nval Context.backgroundColor: Int\n    get() = ThemeStore.backgroundColor(this)\n\nval Context.primaryTextColor: Int\n    get() = getPrimaryTextColor(isDarkTheme)\n\nval Context.secondaryTextColor: Int\n    get() = getSecondaryTextColor(isDarkTheme)\n\nval Context.primaryDisabledTextColor: Int\n    get() = getPrimaryDisabledTextColor(isDarkTheme)\n\nval Context.secondaryDisabledTextColor: Int\n    get() = getSecondaryDisabledTextColor(isDarkTheme)\n\nval Fragment.primaryColor: Int\n    get() = ThemeStore.primaryColor(requireContext())\n\nval Fragment.primaryColorDark: Int\n    get() = ThemeStore.primaryColorDark(requireContext())\n\nval Fragment.accentColor: Int\n    get() = ThemeStore.accentColor(requireContext())\n\nval Fragment.backgroundColor: Int\n    get() = ThemeStore.backgroundColor(requireContext())\n\nval Fragment.primaryTextColor: Int\n    get() = requireContext().getPrimaryTextColor(isDarkTheme)\n\nval Fragment.secondaryTextColor: Int\n    get() = requireContext().getSecondaryTextColor(isDarkTheme)\n\nval Fragment.primaryDisabledTextColor: Int\n    get() = requireContext().getPrimaryDisabledTextColor(isDarkTheme)\n\nval Fragment.secondaryDisabledTextColor: Int\n    get() = requireContext().getSecondaryDisabledTextColor(isDarkTheme)\n\nval Context.buttonDisabledColor: Int\n    get() = if (isDarkTheme) {\n        ContextCompat.getColor(this, R.color.md_dark_disabled)\n    } else {\n        ContextCompat.getColor(this, R.color.md_light_disabled)\n    }\n\nval Context.isDarkTheme: Boolean\n    get() = ColorUtils.isColorLight(ThemeStore.primaryColor(this))\n\nval Fragment.isDarkTheme: Boolean\n    get() = requireContext().isDarkTheme\n\nval Context.filletBackground: GradientDrawable\n    get() {\n        val background = GradientDrawable()\n        background.cornerRadius = 3F.dp\n        background.setColor(backgroundColor)\n        return background\n    }"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/NavigationViewUtil.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\nimport android.content.res.ColorStateList;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\n\nimport com.google.android.material.internal.NavigationMenuView;\nimport com.google.android.material.navigation.NavigationView;\n\n/**\n * @author Karim Abou Zeid (kabouzeid)\n */\npublic final class NavigationViewUtil {\n\n    public static void setItemIconColors(@NonNull NavigationView navigationView, @ColorInt int normalColor, @ColorInt int selectedColor) {\n        final ColorStateList iconSl = new ColorStateList(\n                new int[][]{\n                        new int[]{-android.R.attr.state_checked},\n                        new int[]{android.R.attr.state_checked}\n                },\n                new int[]{\n                        normalColor,\n                        selectedColor\n                });\n        navigationView.setItemIconTintList(iconSl);\n    }\n\n    public static void setItemTextColors(@NonNull NavigationView navigationView, @ColorInt int normalColor, @ColorInt int selectedColor) {\n        final ColorStateList textSl = new ColorStateList(\n                new int[][]{\n                        new int[]{-android.R.attr.state_checked},\n                        new int[]{android.R.attr.state_checked}\n                },\n                new int[]{\n                        normalColor,\n                        selectedColor\n                });\n        navigationView.setItemTextColor(textSl);\n    }\n\n    /**\n     * 去掉navigationView的滚动条\n     *\n     * @param navigationView NavigationView\n     */\n    public static void disableScrollbar(NavigationView navigationView) {\n        if (navigationView != null) {\n            NavigationMenuView navigationMenuView = (NavigationMenuView) navigationView.getChildAt(0);\n            if (navigationMenuView != null) {\n                navigationMenuView.setVerticalScrollBarEnabled(false);\n            }\n        }\n    }\n\n    private NavigationViewUtil() {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/ThemeStore.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.graphics.Color;\n\nimport androidx.annotation.AttrRes;\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.ColorUtils;\n\n/**\n * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)\n */\npublic final class ThemeStore implements ThemeStorePrefKeys, ThemeStoreInterface {\n\n    private final Context mContext;\n    private final SharedPreferences.Editor mEditor;\n\n    public static ThemeStore editTheme(@NonNull Context context) {\n        return new ThemeStore(context);\n    }\n\n    @SuppressLint(\"CommitPrefEdits\")\n    private ThemeStore(@NonNull Context context) {\n        mContext = context;\n        mEditor = prefs(context).edit();\n    }\n\n\n    @Override\n    public ThemeStore primaryColor(@ColorInt int color) {\n        mEditor.putInt(KEY_PRIMARY_COLOR, color);\n        if (autoGeneratePrimaryDark(mContext))\n            primaryColorDark(ColorUtils.darkenColor(color));\n        return this;\n    }\n\n    @Override\n    public ThemeStore primaryColorRes(@ColorRes int colorRes) {\n        return primaryColor(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore primaryColorAttr(@AttrRes int colorAttr) {\n        return primaryColor(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore primaryColorDark(@ColorInt int color) {\n        mEditor.putInt(KEY_PRIMARY_COLOR_DARK, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore primaryColorDarkRes(@ColorRes int colorRes) {\n        return primaryColorDark(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore primaryColorDarkAttr(@AttrRes int colorAttr) {\n        return primaryColorDark(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore accentColor(@ColorInt int color) {\n        mEditor.putInt(KEY_ACCENT_COLOR, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore accentColorRes(@ColorRes int colorRes) {\n        return accentColor(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore accentColorAttr(@AttrRes int colorAttr) {\n        return accentColor(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore statusBarColor(@ColorInt int color) {\n        mEditor.putInt(KEY_STATUS_BAR_COLOR, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore statusBarColorRes(@ColorRes int colorRes) {\n        return statusBarColor(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore statusBarColorAttr(@AttrRes int colorAttr) {\n        return statusBarColor(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore navigationBarColor(@ColorInt int color) {\n        mEditor.putInt(KEY_NAVIGATION_BAR_COLOR, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore navigationBarColorRes(@ColorRes int colorRes) {\n        return navigationBarColor(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore navigationBarColorAttr(@AttrRes int colorAttr) {\n        return navigationBarColor(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore textColorPrimary(@ColorInt int color) {\n        mEditor.putInt(KEY_TEXT_COLOR_PRIMARY, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore textColorPrimaryRes(@ColorRes int colorRes) {\n        return textColorPrimary(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore textColorPrimaryAttr(@AttrRes int colorAttr) {\n        return textColorPrimary(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore textColorPrimaryInverse(@ColorInt int color) {\n        mEditor.putInt(KEY_TEXT_COLOR_PRIMARY_INVERSE, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore textColorPrimaryInverseRes(@ColorRes int colorRes) {\n        return textColorPrimaryInverse(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore textColorPrimaryInverseAttr(@AttrRes int colorAttr) {\n        return textColorPrimaryInverse(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore textColorSecondary(@ColorInt int color) {\n        mEditor.putInt(KEY_TEXT_COLOR_SECONDARY, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore textColorSecondaryRes(@ColorRes int colorRes) {\n        return textColorSecondary(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore textColorSecondaryAttr(@AttrRes int colorAttr) {\n        return textColorSecondary(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore textColorSecondaryInverse(@ColorInt int color) {\n        mEditor.putInt(KEY_TEXT_COLOR_SECONDARY_INVERSE, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore textColorSecondaryInverseRes(@ColorRes int colorRes) {\n        return textColorSecondaryInverse(ContextCompat.getColor(mContext, colorRes));\n    }\n\n    @Override\n    public ThemeStore textColorSecondaryInverseAttr(@AttrRes int colorAttr) {\n        return textColorSecondaryInverse(ATHUtil.resolveColor(mContext, colorAttr));\n    }\n\n    @Override\n    public ThemeStore backgroundColor(int color) {\n        mEditor.putInt(KEY_BACKGROUND_COLOR, color);\n        return this;\n    }\n\n    @Override\n    public ThemeStore coloredStatusBar(boolean colored) {\n        mEditor.putBoolean(KEY_APPLY_PRIMARYDARK_STATUSBAR, colored);\n        return this;\n    }\n\n    @Override\n    public ThemeStore coloredNavigationBar(boolean applyToNavBar) {\n        mEditor.putBoolean(KEY_APPLY_PRIMARY_NAVBAR, applyToNavBar);\n        return this;\n    }\n\n    @Override\n    public ThemeStore autoGeneratePrimaryDark(boolean autoGenerate) {\n        mEditor.putBoolean(KEY_AUTO_GENERATE_PRIMARYDARK, autoGenerate);\n        return this;\n    }\n\n    // Commit method\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void apply() {\n        mEditor.putLong(VALUES_CHANGED, System.currentTimeMillis())\n                .putBoolean(IS_CONFIGURED_KEY, true)\n                .apply();\n    }\n\n    // Static getters\n\n    @CheckResult\n    @NonNull\n    protected static SharedPreferences prefs(@NonNull Context context) {\n        return context.getSharedPreferences(CONFIG_PREFS_KEY_DEFAULT, Context.MODE_PRIVATE);\n    }\n\n    public static void markChanged(@NonNull Context context) {\n        new ThemeStore(context).apply();\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int primaryColor(@NonNull Context context) {\n        return prefs(context).getInt(KEY_PRIMARY_COLOR, ATHUtil.resolveColor(context, R.attr.colorPrimary, Color.parseColor(\"#455A64\")));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int primaryColorDark(@NonNull Context context) {\n        return prefs(context).getInt(KEY_PRIMARY_COLOR_DARK, ATHUtil.resolveColor(context, R.attr.colorPrimaryDark, Color.parseColor(\"#37474F\")));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int accentColor(@NonNull Context context) {\n        return prefs(context).getInt(KEY_ACCENT_COLOR, ATHUtil.resolveColor(context, R.attr.colorAccent, Color.parseColor(\"#263238\")));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int statusBarColor(@NonNull Context context) {\n        if (!coloredStatusBar(context)) {\n            return Color.BLACK;\n        }\n        return prefs(context).getInt(KEY_STATUS_BAR_COLOR, primaryColorDark(context));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int navigationBarColor(@NonNull Context context) {\n        if (!coloredNavigationBar(context)) {\n            return Color.BLACK;\n        }\n        return prefs(context).getInt(KEY_NAVIGATION_BAR_COLOR, primaryColor(context));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int textColorPrimary(@NonNull Context context) {\n        return prefs(context).getInt(KEY_TEXT_COLOR_PRIMARY, ATHUtil.resolveColor(context, android.R.attr.textColorPrimary));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int textColorPrimaryInverse(@NonNull Context context) {\n        return prefs(context).getInt(KEY_TEXT_COLOR_PRIMARY_INVERSE, ATHUtil.resolveColor(context, android.R.attr.textColorPrimaryInverse));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int textColorSecondary(@NonNull Context context) {\n        return prefs(context).getInt(KEY_TEXT_COLOR_SECONDARY, ATHUtil.resolveColor(context, android.R.attr.textColorSecondary));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int textColorSecondaryInverse(@NonNull Context context) {\n        return prefs(context).getInt(KEY_TEXT_COLOR_SECONDARY_INVERSE, ATHUtil.resolveColor(context, android.R.attr.textColorSecondaryInverse));\n    }\n\n    @CheckResult\n    @ColorInt\n    public static int backgroundColor(@NonNull Context context) {\n        return prefs(context).getInt(KEY_BACKGROUND_COLOR, ATHUtil.resolveColor(context, android.R.attr.colorBackground));\n    }\n\n    @CheckResult\n    public static boolean coloredStatusBar(@NonNull Context context) {\n        return prefs(context).getBoolean(KEY_APPLY_PRIMARYDARK_STATUSBAR, true);\n    }\n\n    @CheckResult\n    public static boolean coloredNavigationBar(@NonNull Context context) {\n        return prefs(context).getBoolean(KEY_APPLY_PRIMARY_NAVBAR, false);\n    }\n\n    @CheckResult\n    public static boolean autoGeneratePrimaryDark(@NonNull Context context) {\n        return prefs(context).getBoolean(KEY_AUTO_GENERATE_PRIMARYDARK, true);\n    }\n\n    @CheckResult\n    public static boolean isConfigured(Context context) {\n        return prefs(context).getBoolean(IS_CONFIGURED_KEY, false);\n    }\n\n    @SuppressLint(\"CommitPrefEdits\")\n    public static boolean isConfigured(Context context, @IntRange(from = 0, to = Integer.MAX_VALUE) int version) {\n        final SharedPreferences prefs = prefs(context);\n        final int lastVersion = prefs.getInt(IS_CONFIGURED_VERSION_KEY, -1);\n        if (version > lastVersion) {\n            prefs.edit().putInt(IS_CONFIGURED_VERSION_KEY, version).apply();\n            return false;\n        }\n        return true;\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/ThemeStoreInterface.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\n\nimport androidx.annotation.AttrRes;\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.ColorRes;\n\n/**\n * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)\n */\ninterface ThemeStoreInterface {\n\n    // Primary colors\n\n    ThemeStore primaryColor(@ColorInt int color);\n\n    ThemeStore primaryColorRes(@ColorRes int colorRes);\n\n    ThemeStore primaryColorAttr(@AttrRes int colorAttr);\n\n    ThemeStore autoGeneratePrimaryDark(boolean autoGenerate);\n\n    ThemeStore primaryColorDark(@ColorInt int color);\n\n    ThemeStore primaryColorDarkRes(@ColorRes int colorRes);\n\n    ThemeStore primaryColorDarkAttr(@AttrRes int colorAttr);\n\n    // Accent colors\n\n    ThemeStore accentColor(@ColorInt int color);\n\n    ThemeStore accentColorRes(@ColorRes int colorRes);\n\n    ThemeStore accentColorAttr(@AttrRes int colorAttr);\n\n    // Status bar color\n\n    ThemeStore statusBarColor(@ColorInt int color);\n\n    ThemeStore statusBarColorRes(@ColorRes int colorRes);\n\n    ThemeStore statusBarColorAttr(@AttrRes int colorAttr);\n\n    // Navigation bar color\n\n    ThemeStore navigationBarColor(@ColorInt int color);\n\n    ThemeStore navigationBarColorRes(@ColorRes int colorRes);\n\n    ThemeStore navigationBarColorAttr(@AttrRes int colorAttr);\n\n    // Primary text color\n\n    ThemeStore textColorPrimary(@ColorInt int color);\n\n    ThemeStore textColorPrimaryRes(@ColorRes int colorRes);\n\n    ThemeStore textColorPrimaryAttr(@AttrRes int colorAttr);\n\n    ThemeStore textColorPrimaryInverse(@ColorInt int color);\n\n    ThemeStore textColorPrimaryInverseRes(@ColorRes int colorRes);\n\n    ThemeStore textColorPrimaryInverseAttr(@AttrRes int colorAttr);\n\n    // Secondary text color\n\n    ThemeStore textColorSecondary(@ColorInt int color);\n\n    ThemeStore textColorSecondaryRes(@ColorRes int colorRes);\n\n    ThemeStore textColorSecondaryAttr(@AttrRes int colorAttr);\n\n    ThemeStore textColorSecondaryInverse(@ColorInt int color);\n\n    ThemeStore textColorSecondaryInverseRes(@ColorRes int colorRes);\n\n    ThemeStore textColorSecondaryInverseAttr(@AttrRes int colorAttr);\n\n    ThemeStore backgroundColor(@ColorInt int color);\n\n    // Toggle configurations\n\n    ThemeStore coloredStatusBar(boolean colored);\n\n    ThemeStore coloredNavigationBar(boolean applyToNavBar);\n\n    // Commit/apply\n\n    void apply();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/ThemeStorePrefKeys.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\n/**\n * @author Aidan Follestad (afollestad), Karim Abou Zeid (kabouzeid)\n */\ninterface ThemeStorePrefKeys {\n\n    String CONFIG_PREFS_KEY_DEFAULT = \"[[kabouzeid_app-theme-helper]]\";\n    String IS_CONFIGURED_KEY = \"is_configured\";\n    String IS_CONFIGURED_VERSION_KEY = \"is_configured_version\";\n    String VALUES_CHANGED = \"values_changed\";\n\n    String KEY_PRIMARY_COLOR = \"primary_color\";\n    String KEY_PRIMARY_COLOR_DARK = \"primary_color_dark\";\n    String KEY_ACCENT_COLOR = \"accent_color\";\n    String KEY_STATUS_BAR_COLOR = \"status_bar_color\";\n    String KEY_NAVIGATION_BAR_COLOR = \"navigation_bar_color\";\n\n    String KEY_TEXT_COLOR_PRIMARY = \"text_color_primary\";\n    String KEY_TEXT_COLOR_PRIMARY_INVERSE = \"text_color_primary_inverse\";\n    String KEY_TEXT_COLOR_SECONDARY = \"text_color_secondary\";\n    String KEY_TEXT_COLOR_SECONDARY_INVERSE = \"text_color_secondary_inverse\";\n\n    String KEY_BACKGROUND_COLOR = \"backgroundColor\";\n\n    String KEY_APPLY_PRIMARYDARK_STATUSBAR = \"apply_primarydark_statusbar\";\n    String KEY_APPLY_PRIMARY_NAVBAR = \"apply_primary_navbar\";\n    String KEY_AUTO_GENERATE_PRIMARYDARK = \"auto_generate_primarydark\";\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/TintHelper.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.graphics.PorterDuff;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.RippleDrawable;\nimport android.os.Build;\nimport android.view.View;\nimport android.widget.Button;\nimport android.widget.CheckBox;\nimport android.widget.EditText;\nimport android.widget.ImageView;\nimport android.widget.ProgressBar;\nimport android.widget.RadioButton;\nimport android.widget.SeekBar;\nimport android.widget.Switch;\nimport android.widget.TextView;\n\nimport androidx.annotation.CheckResult;\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.widget.AppCompatEditText;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.appcompat.widget.SwitchCompat;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.graphics.drawable.DrawableCompat;\n\nimport com.google.android.material.floatingactionbutton.FloatingActionButton;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.ColorUtils;\n\nimport java.lang.reflect.Field;\n\n/**\n * @author afollestad, plusCubed\n */\npublic final class TintHelper {\n\n    @SuppressLint(\"PrivateResource\")\n    @ColorInt\n    private static int getDefaultRippleColor(@NonNull Context context, boolean useDarkRipple) {\n        // Light ripple is actually translucent black, and vice versa\n        return ContextCompat.getColor(context, useDarkRipple ?\n                R.color.ripple_material_light : R.color.ripple_material_dark);\n    }\n\n    @NonNull\n    private static ColorStateList getDisabledColorStateList(@ColorInt int normal, @ColorInt int disabled) {\n        return new ColorStateList(new int[][]{\n                new int[]{-android.R.attr.state_enabled},\n                new int[]{android.R.attr.state_enabled}\n        }, new int[]{\n                disabled,\n                normal\n        });\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    public static void setTintSelector(@NonNull View view, @ColorInt final int color, final boolean darker, final boolean useDarkTheme) {\n        final boolean isColorLight = ColorUtils.isColorLight(color);\n        final int disabled = ContextCompat.getColor(view.getContext(), useDarkTheme ? R.color.ate_button_disabled_dark : R.color.ate_button_disabled_light);\n        final int pressed = ColorUtils.shiftColor(color, darker ? 0.9f : 1.1f);\n        final int activated = ColorUtils.shiftColor(color, darker ? 1.1f : 0.9f);\n        final int rippleColor = getDefaultRippleColor(view.getContext(), isColorLight);\n        final int textColor = ContextCompat.getColor(view.getContext(), isColorLight ? R.color.ate_primary_text_light : R.color.ate_primary_text_dark);\n\n        final ColorStateList sl;\n        if (view instanceof Button) {\n            sl = getDisabledColorStateList(color, disabled);\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&\n                    view.getBackground() instanceof RippleDrawable) {\n                RippleDrawable rd = (RippleDrawable) view.getBackground();\n                rd.setColor(ColorStateList.valueOf(rippleColor));\n            }\n\n            // Disabled text color state for buttons, may get overridden later by ATE tags\n            final Button button = (Button) view;\n            button.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(), useDarkTheme ? R.color.ate_button_text_disabled_dark : R.color.ate_button_text_disabled_light)));\n        } else if (view instanceof FloatingActionButton) {\n            // FloatingActionButton doesn't support disabled state?\n            sl = new ColorStateList(new int[][]{\n                    new int[]{-android.R.attr.state_pressed},\n                    new int[]{android.R.attr.state_pressed}\n            }, new int[]{\n                    color,\n                    pressed\n            });\n\n            final FloatingActionButton fab = (FloatingActionButton) view;\n            fab.setRippleColor(rippleColor);\n            fab.setBackgroundTintList(sl);\n            if (fab.getDrawable() != null)\n                fab.setImageDrawable(createTintedDrawable(fab.getDrawable(), textColor));\n            return;\n        } else {\n            sl = new ColorStateList(\n                    new int[][]{\n                            new int[]{-android.R.attr.state_enabled},\n                            new int[]{android.R.attr.state_enabled},\n                            new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed},\n                            new int[]{android.R.attr.state_enabled, android.R.attr.state_activated},\n                            new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}\n                    },\n                    new int[]{\n                            disabled,\n                            color,\n                            pressed,\n                            activated,\n                            activated\n                    }\n            );\n        }\n\n        Drawable drawable = view.getBackground();\n        if (drawable != null) {\n            drawable = createTintedDrawable(drawable, sl);\n            ViewUtil.setBackgroundCompat(view, drawable);\n        }\n\n        if (view instanceof TextView && !(view instanceof Button)) {\n            final TextView tv = (TextView) view;\n            tv.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(), isColorLight ? R.color.ate_text_disabled_light : R.color.ate_text_disabled_dark)));\n        }\n    }\n\n    public static void setTintAuto(final @NonNull View view, final @ColorInt int color,\n                                   boolean background) {\n        setTintAuto(view, color, background, ATHUtil.isWindowBackgroundDark(view.getContext()));\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    public static void setTintAuto(final @NonNull View view, final @ColorInt int color,\n                                   boolean background, final boolean isDark) {\n        if (!background) {\n            if (view instanceof RadioButton)\n                setTint((RadioButton) view, color, isDark);\n            else if (view instanceof SeekBar)\n                setTint((SeekBar) view, color, isDark);\n            else if (view instanceof ProgressBar)\n                setTint((ProgressBar) view, color);\n            else if (view instanceof AppCompatEditText)\n                setTint((AppCompatEditText) view, color, isDark);\n            else if (view instanceof CheckBox)\n                setTint((CheckBox) view, color, isDark);\n            else if (view instanceof ImageView)\n                setTint((ImageView) view, color);\n            else if (view instanceof Switch)\n                setTint((Switch) view, color, isDark);\n            else if (view instanceof SwitchCompat)\n                setTint((SwitchCompat) view, color, isDark);\n            else if (view instanceof SearchView) {\n                int iconIdS[] = new int[]{androidx.appcompat.R.id.search_button, androidx.appcompat.R.id.search_close_btn,};\n                for (int iconId : iconIdS) {\n                    ImageView icon = view.findViewById(iconId);\n                    if (icon != null) {\n                        setTint(icon, color);\n                    }\n                }\n\n            } else {\n                background = true;\n            }\n\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&\n                    !background && view.getBackground() instanceof RippleDrawable) {\n                // Ripples for the above views (e.g. when you tap and hold a switch or checkbox)\n                RippleDrawable rd = (RippleDrawable) view.getBackground();\n                @SuppressLint(\"PrivateResource\") final int unchecked = ContextCompat.getColor(view.getContext(),\n                        isDark ? R.color.ripple_material_dark : R.color.ripple_material_light);\n                final int checked = ColorUtils.adjustAlpha(color, 0.4f);\n                final ColorStateList sl = new ColorStateList(\n                        new int[][]{\n                                new int[]{-android.R.attr.state_activated, -android.R.attr.state_checked},\n                                new int[]{android.R.attr.state_activated},\n                                new int[]{android.R.attr.state_checked}\n                        },\n                        new int[]{\n                                unchecked,\n                                checked,\n                                checked\n                        }\n                );\n                rd.setColor(sl);\n            }\n        }\n        if (background) {\n            // Need to tint the background of a view\n            if (view instanceof FloatingActionButton || view instanceof Button) {\n                setTintSelector(view, color, false, isDark);\n            } else if (view.getBackground() != null) {\n                Drawable drawable = view.getBackground();\n                if (drawable != null) {\n                    drawable = createTintedDrawable(drawable, color);\n                    ViewUtil.setBackgroundCompat(view, drawable);\n                }\n            }\n        }\n    }\n\n    public static void setTint(@NonNull RadioButton radioButton, @ColorInt int color, boolean useDarker) {\n        ColorStateList sl = new ColorStateList(new int[][]{\n                new int[]{-android.R.attr.state_enabled},\n                new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked},\n                new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}\n        }, new int[]{\n                // Radio button includes own alpha for disabled state\n                ColorUtils.stripAlpha(ContextCompat.getColor(radioButton.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light)),\n                ContextCompat.getColor(radioButton.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),\n                color\n        });\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            radioButton.setButtonTintList(sl);\n        } else {\n            Drawable d = createTintedDrawable(ContextCompat.getDrawable(radioButton.getContext(), R.drawable.abc_btn_radio_material), sl);\n            radioButton.setButtonDrawable(d);\n        }\n    }\n\n    public static void setTint(@NonNull SeekBar seekBar, @ColorInt int color, boolean useDarker) {\n        final ColorStateList s1 = getDisabledColorStateList(color,\n                ContextCompat.getColor(seekBar.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light));\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            seekBar.setThumbTintList(s1);\n            seekBar.setProgressTintList(s1);\n        } else {\n            Drawable progressDrawable = createTintedDrawable(seekBar.getProgressDrawable(), s1);\n            seekBar.setProgressDrawable(progressDrawable);\n            Drawable thumbDrawable = createTintedDrawable(seekBar.getThumb(), s1);\n            seekBar.setThumb(thumbDrawable);\n        }\n    }\n\n    public static void setTint(@NonNull ProgressBar progressBar, @ColorInt int color) {\n        setTint(progressBar, color, false);\n    }\n\n    public static void setTint(@NonNull ProgressBar progressBar, @ColorInt int color, boolean skipIndeterminate) {\n        ColorStateList sl = ColorStateList.valueOf(color);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            progressBar.setProgressTintList(sl);\n            progressBar.setSecondaryProgressTintList(sl);\n            if (!skipIndeterminate)\n                progressBar.setIndeterminateTintList(sl);\n        } else {\n            PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN;\n            if (!skipIndeterminate && progressBar.getIndeterminateDrawable() != null)\n                progressBar.getIndeterminateDrawable().setColorFilter(color, mode);\n            if (progressBar.getProgressDrawable() != null)\n                progressBar.getProgressDrawable().setColorFilter(color, mode);\n        }\n    }\n\n\n    @SuppressLint(\"RestrictedApi\")\n    public static void setTint(@NonNull AppCompatEditText editText, @ColorInt int color, boolean useDarker) {\n        final ColorStateList editTextColorStateList = new ColorStateList(new int[][]{\n                new int[]{-android.R.attr.state_enabled},\n                new int[]{android.R.attr.state_enabled, -android.R.attr.state_pressed, -android.R.attr.state_focused},\n                new int[]{}\n        }, new int[]{\n                ContextCompat.getColor(editText.getContext(), useDarker ? R.color.ate_text_disabled_dark : R.color.ate_text_disabled_light),\n                ContextCompat.getColor(editText.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),\n                color\n        });\n        editText.setSupportBackgroundTintList(editTextColorStateList);\n        setCursorTint(editText, color);\n    }\n\n    public static void setTint(@NonNull CheckBox box, @ColorInt int color, boolean useDarker) {\n        ColorStateList sl = new ColorStateList(new int[][]{\n                new int[]{-android.R.attr.state_enabled},\n                new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked},\n                new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}\n        }, new int[]{\n                ContextCompat.getColor(box.getContext(), useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light),\n                ContextCompat.getColor(box.getContext(), useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),\n                color\n        });\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            box.setButtonTintList(sl);\n        } else {\n            Drawable drawable = createTintedDrawable(ContextCompat.getDrawable(box.getContext(), R.drawable.abc_btn_check_material), sl);\n            box.setButtonDrawable(drawable);\n        }\n    }\n\n    public static void setTint(@NonNull ImageView image, @ColorInt int color) {\n        image.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);\n    }\n\n    private static Drawable modifySwitchDrawable(@NonNull Context context, @NonNull Drawable from, @ColorInt int tint, boolean thumb, boolean compatSwitch, boolean useDarker) {\n        if (useDarker) {\n            tint = ColorUtils.shiftColor(tint, 1.1f);\n        }\n        tint = ColorUtils.adjustAlpha(tint, (compatSwitch && !thumb) ? 0.5f : 1.0f);\n        int disabled;\n        int normal;\n        if (thumb) {\n            disabled = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_thumb_disabled_dark : R.color.ate_switch_thumb_disabled_light);\n            normal = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_thumb_normal_dark : R.color.ate_switch_thumb_normal_light);\n        } else {\n            disabled = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_track_disabled_dark : R.color.ate_switch_track_disabled_light);\n            normal = ContextCompat.getColor(context, useDarker ? R.color.ate_switch_track_normal_dark : R.color.ate_switch_track_normal_light);\n        }\n\n        // Stock switch includes its own alpha\n        if (!compatSwitch) {\n            normal = ColorUtils.stripAlpha(normal);\n        }\n\n        final ColorStateList sl = new ColorStateList(\n                new int[][]{\n                        new int[]{-android.R.attr.state_enabled},\n                        new int[]{android.R.attr.state_enabled, -android.R.attr.state_activated, -android.R.attr.state_checked},\n                        new int[]{android.R.attr.state_enabled, android.R.attr.state_activated},\n                        new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}\n                },\n                new int[]{\n                        disabled,\n                        normal,\n                        tint,\n                        tint\n                }\n        );\n        return createTintedDrawable(from, sl);\n    }\n\n    public static void setTint(@NonNull Switch switchView, @ColorInt int color, boolean useDarker) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return;\n        if (switchView.getTrackDrawable() != null) {\n            switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(),\n                    switchView.getTrackDrawable(), color, false, false, useDarker));\n        }\n        if (switchView.getThumbDrawable() != null) {\n            switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(),\n                    switchView.getThumbDrawable(), color, true, false, useDarker));\n        }\n    }\n\n    public static void setTint(@NonNull SwitchCompat switchView, @ColorInt int color, boolean useDarker) {\n        if (switchView.getTrackDrawable() != null) {\n            switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(),\n                    switchView.getTrackDrawable(), color, false, true, useDarker));\n        }\n        if (switchView.getThumbDrawable() != null) {\n            switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(),\n                    switchView.getThumbDrawable(), color, true, true, useDarker));\n        }\n    }\n\n    // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.\n    @CheckResult\n    @Nullable\n    public static Drawable createTintedDrawable(@Nullable Drawable drawable, @ColorInt int color) {\n        if (drawable == null) return null;\n        drawable = DrawableCompat.wrap(drawable.mutate());\n        DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN);\n        DrawableCompat.setTint(drawable, color);\n        return drawable;\n    }\n\n    // This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.\n    @CheckResult\n    @Nullable\n    public static Drawable createTintedDrawable(@Nullable Drawable drawable, @NonNull ColorStateList sl) {\n        if (drawable == null) return null;\n        drawable = DrawableCompat.wrap(drawable.mutate());\n        DrawableCompat.setTintList(drawable, sl);\n        return drawable;\n    }\n\n    public static void setCursorTint(@NonNull EditText editText, @ColorInt int color) {\n        try {\n            Field fCursorDrawableRes = TextView.class.getDeclaredField(\"mCursorDrawableRes\");\n            fCursorDrawableRes.setAccessible(true);\n            int mCursorDrawableRes = fCursorDrawableRes.getInt(editText);\n            Field fEditor = TextView.class.getDeclaredField(\"mEditor\");\n            fEditor.setAccessible(true);\n            Object editor = fEditor.get(editText);\n            Class<?> clazz = editor.getClass();\n            Field fCursorDrawable = clazz.getDeclaredField(\"mCursorDrawable\");\n            fCursorDrawable.setAccessible(true);\n            Drawable[] drawables = new Drawable[2];\n            drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes);\n            drawables[0] = createTintedDrawable(drawables[0], color);\n            drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes);\n            drawables[1] = createTintedDrawable(drawables[1], color);\n            fCursorDrawable.set(editor, drawables);\n        } catch (Exception ignored) {\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/theme/ViewUtil.java",
    "content": "package com.kunfei.bookshelf.utils.theme;\n\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.TransitionDrawable;\nimport android.view.View;\nimport android.view.ViewTreeObserver;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunfei.bookshelf.utils.DrawableUtil;\n\n/**\n * @author Karim Abou Zeid (kabouzeid)\n */\npublic final class ViewUtil {\n\n    @SuppressWarnings(\"deprecation\")\n    public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener) {\n        v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    public static void setBackgroundCompat(@NonNull View view, @Nullable Drawable drawable) {\n        view.setBackground(drawable);\n    }\n\n    public static TransitionDrawable setBackgroundTransition(@NonNull View view, @NonNull Drawable newDrawable) {\n        TransitionDrawable transition = DrawableUtil.createTransitionDrawable(view.getBackground(), newDrawable);\n        setBackgroundCompat(view, transition);\n        return transition;\n    }\n\n    public static TransitionDrawable setBackgroundColorTransition(@NonNull View view, @ColorInt int newColor) {\n        final Drawable oldColor = view.getBackground();\n\n        Drawable start = oldColor != null ? oldColor : new ColorDrawable(view.getSolidColor());\n        Drawable end = new ColorDrawable(newColor);\n\n        TransitionDrawable transition = DrawableUtil.createTransitionDrawable(start, end);\n\n        setBackgroundCompat(view, transition);\n\n        return transition;\n    }\n\n    private ViewUtil() {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/viewbindingdelegate/ActivityViewBindings.kt",
    "content": "@file:Suppress(\"RedundantVisibilityModifier\", \"unused\")\n\npackage com.kunfei.bookshelf.utils.viewbindingdelegate\n\nimport android.view.LayoutInflater\nimport androidx.core.app.ComponentActivity\nimport androidx.viewbinding.ViewBinding\n\n/**\n * Create new [ViewBinding] associated with the [ComponentActivity]\n */\n@JvmName(\"viewBindingActivity\")\ninline fun <T : ViewBinding> ComponentActivity.viewBinding(\n    crossinline bindingInflater: (LayoutInflater) -> T,\n    setContentView: Boolean = false\n) = lazy(LazyThreadSafetyMode.SYNCHRONIZED) {\n    val binding = bindingInflater.invoke(layoutInflater)\n    if (setContentView) {\n        setContentView(binding.root)\n    }\n    binding\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/viewbindingdelegate/FragmentViewBindings.kt",
    "content": "@file:Suppress(\"RedundantVisibilityModifier\", \"unused\")\n@file:JvmName(\"ReflectionFragmentViewBindings\")\n\npackage com.kunfei.bookshelf.utils.viewbindingdelegate\n\nimport android.view.View\nimport androidx.annotation.IdRes\nimport androidx.fragment.app.Fragment\nimport androidx.viewbinding.ViewBinding\n\nprivate class FragmentViewBindingProperty<F : Fragment, T : ViewBinding>(\n    viewBinder: (F) -> T\n) : ViewBindingProperty<F, T>(viewBinder) {\n\n    override fun getLifecycleOwner(thisRef: F) = thisRef.viewLifecycleOwner\n}\n\n/**\n * Create new [ViewBinding] associated with the [Fragment]\n */\n@JvmName(\"viewBindingFragment\")\npublic fun <F : Fragment, T : ViewBinding> Fragment.viewBinding(viewBinder: (F) -> T): ViewBindingProperty<F, T> {\n    return FragmentViewBindingProperty(viewBinder)\n}\n\n/**\n * Create new [ViewBinding] associated with the [Fragment]\n *\n * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used\n * @param viewProvider Provide a [View] from the Fragment. By default call [Fragment.requireView]\n */\n@JvmName(\"viewBindingFragment\")\npublic inline fun <F : Fragment, T : ViewBinding> Fragment.viewBinding(\n    crossinline vbFactory: (View) -> T,\n    crossinline viewProvider: (F) -> View = Fragment::requireView\n): ViewBindingProperty<F, T> {\n    return viewBinding { fragment: F -> vbFactory(viewProvider(fragment)) }\n}\n\n/**\n * Create new [ViewBinding] associated with the [Fragment]\n *\n * @param vbFactory Function that create new instance of [ViewBinding]. `MyViewBinding::bind` can be used\n * @param viewBindingRootId Root view's id that will be used as root for the view binding\n */\n@JvmName(\"viewBindingFragment\")\npublic inline fun <T : ViewBinding> Fragment.viewBinding(\n    crossinline vbFactory: (View) -> T,\n    @IdRes viewBindingRootId: Int\n): ViewBindingProperty<Fragment, T> {\n    return viewBinding(vbFactory) { fragment: Fragment ->\n        fragment.requireView().findViewById(viewBindingRootId)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/viewbindingdelegate/ViewBindingProperty.kt",
    "content": "@file:Suppress(\"RedundantVisibilityModifier\")\n\npackage com.kunfei.bookshelf.utils.viewbindingdelegate\n\nimport android.os.Handler\nimport android.os.Looper\nimport androidx.annotation.MainThread\nimport androidx.lifecycle.DefaultLifecycleObserver\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.viewbinding.ViewBinding\nimport kotlin.properties.ReadOnlyProperty\nimport kotlin.reflect.KProperty\n\npublic abstract class ViewBindingProperty<in R : Any, T : ViewBinding>(\n    private val viewBinder: (R) -> T\n) : ReadOnlyProperty<R, T> {\n\n    private var viewBinding: T? = null\n    private val lifecycleObserver = ClearOnDestroyLifecycleObserver()\n    private var thisRef: R? = null\n\n    protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner\n\n    @MainThread\n    public override fun getValue(thisRef: R, property: KProperty<*>): T {\n        viewBinding?.let { return it }\n\n        this.thisRef = thisRef\n        val lifecycle = getLifecycleOwner(thisRef).lifecycle\n        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {\n            mainHandler.post { viewBinding = null }\n        } else {\n            lifecycle.addObserver(lifecycleObserver)\n        }\n        return viewBinder(thisRef).also { viewBinding = it }\n    }\n\n    @MainThread\n    public fun clear() {\n        val thisRef = thisRef ?: return\n        this.thisRef = null\n        getLifecycleOwner(thisRef).lifecycle.removeObserver(lifecycleObserver)\n        mainHandler.post { viewBinding = null }\n    }\n\n    private inner class ClearOnDestroyLifecycleObserver : DefaultLifecycleObserver {\n\n        @MainThread\n        override fun onDestroy(owner: LifecycleOwner): Unit = clear()\n    }\n\n    private companion object {\n\n        private val mainHandler = Handler(Looper.getMainLooper())\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/webdav/README.md",
    "content": "## 用于网络备份的WebDav"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/webdav/WebDav.kt",
    "content": "package com.kunfei.bookshelf.utils.webdav\n\nimport com.kunfei.bookshelf.base.BaseModelImpl\nimport com.kunfei.bookshelf.utils.webdav.http.Handler\nimport com.kunfei.bookshelf.utils.webdav.http.HttpAuth\nimport okhttp3.*\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.RequestBody.Companion.asRequestBody\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.jsoup.Jsoup\nimport java.io.File\nimport java.io.IOException\nimport java.io.InputStream\nimport java.io.UnsupportedEncodingException\nimport java.net.MalformedURLException\nimport java.net.URL\nimport java.net.URLEncoder\nimport java.util.*\n\nclass WebDav @Throws(MalformedURLException::class)\nconstructor(urlStr: String) {\n    companion object {\n        // 指定返回哪些属性\n        private const val DIR =\n                \"\"\"<?xml version=\"1.0\"?>\n                <a:propfind xmlns:a=\"DAV:\">\n                    <a:prop>\n                        <a:displayname/>\n                        <a:resourcetype/>\n                        <a:getcontentlength/>\n                        <a:creationdate/>\n                        <a:getlastmodified/>\n                        %s\n                    </a:prop>\n                </a:propfind>\"\"\"\n    }\n\n    private val url: URL = URL(null, urlStr, Handler)\n    private val httpUrl: String? by lazy {\n        val raw = url.toString().replace(\"davs://\", \"https://\").replace(\"dav://\", \"http://\")\n        try {\n            return@lazy URLEncoder.encode(raw, \"UTF-8\")\n                    .replace(\"\\\\+\".toRegex(), \"%20\")\n                    .replace(\"%3A\".toRegex(), \":\")\n                    .replace(\"%2F\".toRegex(), \"/\")\n        } catch (e: UnsupportedEncodingException) {\n            e.printStackTrace()\n            return@lazy null\n        }\n    }\n\n    var displayName: String? = null\n    var size: Long = 0\n    var exists = false\n    var parent = \"\"\n    var urlName = \"\"\n        get() {\n            if (field.isEmpty()) {\n                this.urlName = (\n                        if (parent.isEmpty()) url.file\n                        else url.toString().replace(parent, \"\")\n                        ).replace(\"/\", \"\")\n            }\n            return field\n        }\n\n    fun getPath() = url.toString()\n\n    fun getHost() = url.host\n\n    /**\n     * 填充文件信息。实例化WebDAVFile对象时，并没有将远程文件的信息填充到实例中。需要手动填充！\n     *\n     * @return 远程文件是否存在\n     */\n    @Throws(IOException::class)\n    fun indexFileInfo(): Boolean {\n        propFindResponse(ArrayList())?.let { response ->\n            if (!response.isSuccessful) {\n                this.exists = false\n                return false\n            }\n            response.body?.let {\n                if (it.string().isNotEmpty()) {\n                    return true\n                }\n            }\n        }\n        return false\n    }\n\n    /**\n     * 列出当前路径下的文件\n     *\n     * @param propsList 指定列出文件的哪些属性\n     * @return 文件列表\n     */\n    @Throws(IOException::class)\n    @JvmOverloads\n    fun listFiles(propsList: ArrayList<String> = ArrayList()): List<WebDav> {\n        propFindResponse(propsList)?.let { response ->\n            if (response.isSuccessful) {\n                response.body?.let { body ->\n                    return parseDir(body.string())\n                }\n            }\n        }\n        return ArrayList()\n    }\n\n    @Throws(IOException::class)\n    private fun propFindResponse(propsList: ArrayList<String>, depth: Int = 1): Response? {\n        val requestProps = StringBuilder()\n        for (p in propsList) {\n            requestProps.append(\"<a:\").append(p).append(\"/>\\n\")\n        }\n        val requestPropsStr: String\n        requestPropsStr = if (requestProps.toString().isEmpty()) {\n            DIR.replace(\"%s\", \"\")\n        } else {\n            String.format(DIR, requestProps.toString() + \"\\n\")\n        }\n        httpUrl?.let { url ->\n            val request = Request.Builder()\n                .url(url)\n                // 添加RequestBody对象，可以只返回的属性。如果设为null，则会返回全部属性\n                // 注意：尽量手动指定需要返回的属性。若返回全部属性，可能后由于Prop.java里没有该属性名，而崩溃。\n                .method(\"PROPFIND\", requestPropsStr.toRequestBody(\"text/plain\".toMediaType()))\n\n            HttpAuth.auth?.let {\n                request.header(\n                        \"Authorization\",\n                        Credentials.basic(it.user, it.pass)\n                )\n            }\n            request.header(\"Depth\", if (depth < 0) \"infinity\" else depth.toString())\n            return BaseModelImpl.getClient().newCall(request.build()).execute()\n        }\n        return null\n    }\n\n    private fun parseDir(s: String): List<WebDav> {\n        val list = ArrayList<WebDav>()\n        val document = Jsoup.parse(s)\n        val elements = document.getElementsByTag(\"d:response\")\n        httpUrl?.let { url ->\n            val baseUrl = if (url.endsWith(\"/\")) url else \"$url/\"\n            for (element in elements) {\n                val href = element.getElementsByTag(\"d:href\")[0].text()\n                if (!href.endsWith(\"/\")) {\n                    val fileName = href.substring(href.lastIndexOf(\"/\") + 1)\n                    val webDavFile: WebDav\n                    try {\n                        webDavFile = WebDav(baseUrl + fileName)\n                        webDavFile.displayName = fileName\n                        webDavFile.urlName = href\n                        list.add(webDavFile)\n                    } catch (e: MalformedURLException) {\n                        e.printStackTrace()\n                    }\n                }\n            }\n        }\n        return list\n    }\n\n    /**\n     * 根据自己的URL，在远程处创建对应的文件夹\n     *\n     * @return 是否创建成功\n     */\n    @Throws(IOException::class)\n    fun makeAsDir(): Boolean {\n        httpUrl?.let { url ->\n            val request = Request.Builder()\n                    .url(url)\n                    .method(\"MKCOL\", null)\n            return execRequest(request)\n        }\n        return false\n    }\n\n    /**\n     * 下载到本地\n     *\n     * @param savedPath       本地的完整路径，包括最后的文件名\n     * @param replaceExisting 是否替换本地的同名文件\n     * @return 下载是否成功\n     */\n    fun downloadTo(savedPath: String, replaceExisting: Boolean): Boolean {\n        if (File(savedPath).exists()) {\n            if (!replaceExisting) return false\n        }\n        val inputS = getInputStream() ?: return false\n        File(savedPath).writeBytes(inputS.readBytes())\n        return true\n    }\n\n    /**\n     * 上传文件\n     */\n    @Throws(IOException::class)\n    @JvmOverloads\n    fun upload(localPath: String, contentType: String? = null): Boolean {\n        val file = File(localPath)\n        if (!file.exists()) return false\n        val mediaType = contentType?.toMediaType()\n        // 务必注意RequestBody不要嵌套，不然上传时内容可能会被追加多余的文件信息\n        val fileBody = file.asRequestBody(mediaType)\n        httpUrl?.let {\n            val request = Request.Builder()\n                    .url(it)\n                    .put(fileBody)\n            return execRequest(request)\n        }\n        return false\n    }\n\n    /**\n     * 执行请求，获取响应结果\n     * @param requestBuilder 因为还需要追加验证信息，所以此处传递Request.Builder的对象，而不是Request的对象\n     * @return 请求执行的结果\n     */\n    @Throws(IOException::class)\n    private fun execRequest(requestBuilder: Request.Builder): Boolean {\n        HttpAuth.auth?.let {\n            requestBuilder.header(\n                    \"Authorization\",\n                    Credentials.basic(it.user, it.pass)\n            )\n        }\n        val response = BaseModelImpl.getClient().newCall(requestBuilder.build()).execute()\n        return response.isSuccessful\n    }\n\n    private fun getInputStream(): InputStream? {\n        httpUrl?.let { url ->\n            val request = Request.Builder().url(url)\n            HttpAuth.auth?.let {\n                request.header(\"Authorization\", Credentials.basic(it.user, it.pass))\n            }\n            try {\n                return BaseModelImpl.getClient().newCall(request.build())\n                    .execute().body?.byteStream()\n            } catch (e: IOException) {\n                e.printStackTrace()\n            } catch (e: IllegalArgumentException) {\n                e.printStackTrace()\n            }\n        }\n        return null\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/webdav/http/Handler.kt",
    "content": "package com.kunfei.bookshelf.utils.webdav.http\n\nimport java.net.URL\nimport java.net.URLConnection\nimport java.net.URLStreamHandler\n\nobject Handler : URLStreamHandler() {\n\n    override fun getDefaultPort(): Int {\n        return 80\n    }\n\n    public override fun openConnection(u: URL): URLConnection? {\n        return null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/utils/webdav/http/HttpAuth.kt",
    "content": "package com.kunfei.bookshelf.utils.webdav.http\n\nobject HttpAuth {\n\n    var auth: Auth? = null\n\n    class Auth internal constructor(val user: String, val pass: String)\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/AboutActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.KeyEvent;\nimport android.view.MenuItem;\nimport android.widget.PopupMenu;\n\nimport androidx.appcompat.app.ActionBar;\n\nimport com.google.zxing.EncodeHintType;\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.databinding.ActivityAboutBinding;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.widget.modialog.MoDialogHUD;\n\nimport cn.bingoogolapple.qrcode.zxing.QRCodeEncoder;\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\n/**\n * Created by GKF on 2017/12/15.\n * 关于\n */\n\npublic class AboutActivity extends MBaseActivity<IPresenter> {\n\n    private MoDialogHUD moDialogHUD;\n    private String[] allQQ = new String[]{\"(公众号)开源阅读\", \"(QQ群)701903217\", \"(QQ群)805192012\", \"(QQ群)773736122\", \"(QQ群)981838750\"};\n    private ActivityAboutBinding binding;\n\n    public static void startThis(Context context) {\n        Intent intent = new Intent(context, AboutActivity.class);\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        context.startActivity(intent);\n    }\n\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityAboutBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void initData() {\n        moDialogHUD = new MoDialogHUD(this);\n    }\n\n    @Override\n    protected void bindView() {\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        binding.tvVersion.setText(getString(R.string.version_name, MApplication.getVersionName()));\n    }\n\n    @Override\n    protected void bindEvent() {\n        binding.vwDonate.setOnClickListener(view -> DonateActivity.startThis(this));\n        binding.vwScoring.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, \"market://details?id=\" + getPackageName()));\n        binding.vwMail.setOnClickListener(view -> openIntent(Intent.ACTION_SENDTO, \"mailto:kunfei.ge@gmail.com\"));\n        binding.vwGit.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, getString(R.string.this_github_url)));\n        binding.vwDisclaimer.setOnClickListener(view -> moDialogHUD.showAssetMarkdown(\"disclaimer.md\"));\n        binding.vwUpdate.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, getString(R.string.latest_release_url)));\n        binding.vwHomePage.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, getString(R.string.home_page_url)));\n        binding.vwQq.setOnClickListener(view -> {\n            PopupMenu popupMenu = new PopupMenu(AboutActivity.this, view);\n            for (String qq : allQQ) {\n                popupMenu.getMenu().add(qq);\n            }\n            popupMenu.setOnMenuItemClickListener(menuItem -> {\n                joinGroup(menuItem.getTitle().toString());\n                return true;\n            });\n            popupMenu.show();\n        });\n        binding.vwUpdateLog.setOnClickListener(view -> moDialogHUD.showAssetMarkdown(\"updateLog.md\"));\n        binding.vwFaq.setOnClickListener(view -> openIntent(Intent.ACTION_VIEW, \"https://mp.weixin.qq.com/s?__biz=MzU2NjU0NjM1Mg==&mid=100000032&idx=1&sn=53e52168caf1ad9e507ab56381c45f1f&chksm=7cab9bff4bdc12e925e282effc1d4993a8652c248abc6169bd31d6fac133628fad54cf516043&mpshare=1&scene=1&srcid=0321CjdEk21qy8WjDgZ0I6sW&key=08039a5457341b11b054342370cc5462829ae3b54e4b265c42e28361773a6fa0e3105d706160d75b097b3ae41148dda265e2416b88f6b6a2391c1f33ec9f0bc62ea9edc86b75344494b598842ad620ac&ascene=1&uin=NzUwMTUxNzIx&devicetype=Windows+10&version=62060739&lang=zh_CN&pass_ticket=%2FD6keuc%2Fx%2Ba8YhupUUvefch8Gm07zVHa34Df5m1waxWQuCOohBN70NNcDEJsKE%2BV\"));\n        binding.vwShare.setOnClickListener(view -> {\n            String url = \"https://www.coolapk.com/apk/com.gedoor.monkeybook\";\n            Single.create((SingleOnSubscribe<Bitmap>) emitter -> {\n                QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);\n                Bitmap bitmap = QRCodeEncoder.syncEncodeQRCode(url, 600);\n                emitter.onSuccess(bitmap);\n            }).compose(RxUtils::toSimpleSingle)\n                    .subscribe(new MySingleObserver<Bitmap>() {\n                        @Override\n                        public void onSuccess(Bitmap bitmap) {\n                            if (bitmap != null) {\n                                moDialogHUD.showImageText(bitmap, url);\n                            }\n                        }\n                    });\n        });\n    }\n\n    private void joinGroup(String name) {\n        String key;\n        if (name.equals(allQQ[1])) {\n            key = \"-iolizL4cbJSutKRpeImHlXlpLDZnzeF\";\n            if (joinQQGroupError(key)) {\n                copyName(name.substring(5));\n            }\n        } else if (name.equals(allQQ[2])) {\n            key = \"6GlFKjLeIk5RhQnR3PNVDaKB6j10royo\";\n            if (joinQQGroupError(key)) {\n                copyName(name.substring(5));\n            }\n        } else if (name.equals(allQQ[3])) {\n            key = \"5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF\";\n            if (joinQQGroupError(key)) {\n                copyName(name.substring(5));\n            }\n        } else if (name.equals(allQQ[4])) {\n            key = \"g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9\";\n            if (joinQQGroupError(key)) {\n                copyName(name.substring(5));\n            }\n        } else {\n            copyName(name.substring(5));\n        }\n    }\n\n    private void copyName(String name) {\n        ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);\n        ClipData clipData = ClipData.newPlainText(null, name);\n        if (clipboard != null) {\n            clipboard.setPrimaryClip(clipData);\n            toast(R.string.copy_complete);\n        }\n    }\n\n    private boolean joinQQGroupError(String key) {\n        Intent intent = new Intent();\n        intent.setData(Uri.parse(\"mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D\" + key));\n        // 此Flag可根据具体产品需要自定义，如设置，则在加群界面按返回，返回手Q主界面，不设置，按返回会返回到呼起产品界面    //intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        try {\n            startActivity(intent);\n            return false;\n        } catch (Exception e) {\n            return true;\n        }\n    }\n\n    void openIntent(String intentName, String address) {\n        try {\n            Intent intent = new Intent(intentName);\n            intent.setData(Uri.parse(address));\n            startActivity(intent);\n        } catch (Exception e) {\n            toast(R.string.can_not_open, ERROR);\n        }\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.about);\n        }\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        Boolean mo = moDialogHUD.onKeyDown(keyCode, event);\n        return mo || super.onKeyDown(keyCode, event);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/BookCoverEditActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Intent;\nimport android.view.LayoutInflater;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.bumptech.glide.Glide;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.dao.SearchBookBeanDao;\nimport com.kunfei.bookshelf.databinding.ActivityBookCoverEditBinding;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.SearchBookModel;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\npublic class BookCoverEditActivity extends MBaseActivity<IPresenter> {\n    private ActivityBookCoverEditBinding binding;\n    private SearchBookModel searchBookModel;\n    private String name;\n    private String author;\n    private Boolean isLoading = true;\n    private List<String> urls = new ArrayList<>();\n    private List<String> origins = new ArrayList<>();\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityBookCoverEditBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.cover_change_source);\n        }\n    }\n\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected void initData() {\n        name = getIntent().getStringExtra(\"name\");\n        author = getIntent().getStringExtra(\"author\");\n        ChangeCoverAdapter changeCoverAdapter = new ChangeCoverAdapter();\n        binding.rfRvChangeCover.setLayoutManager(new GridLayoutManager(this, 3));\n        binding.rfRvChangeCover.setAdapter(changeCoverAdapter);\n        SearchBookModel.OnSearchListener searchListener = new SearchBookModel.OnSearchListener() {\n            @Override\n            public void refreshSearchBook() {\n                binding.swipeRefreshLayout.setRefreshing(true);\n            }\n\n            @Override\n            public void refreshFinish(Boolean value) {\n                binding.swipeRefreshLayout.setRefreshing(false);\n                isLoading = false;\n            }\n\n            @Override\n            public void loadMoreFinish(Boolean value) {\n                if (value) {\n                    isLoading = false;\n                }\n            }\n\n            @Override\n            public void loadMoreSearchBook(List<SearchBookBean> value) {\n                if (!value.isEmpty()) {\n                    SearchBookBean bookBean = value.get(0);\n                    if (bookBean.getName().equals(name)\n                            && bookBean.getCoverUrl() != null\n                            && !urls.contains(bookBean.getCoverUrl())) {\n                        urls.add(bookBean.getCoverUrl());\n                        origins.add(bookBean.getOrigin());\n                        changeCoverAdapter.notifyItemChanged(urls.size() - 1);\n                    }\n                }\n            }\n\n            @Override\n            public void searchBookError(Throwable throwable) {\n                binding.swipeRefreshLayout.setRefreshing(false);\n                isLoading = false;\n            }\n\n            @Override\n            public int getItemCount() {\n                return 0;\n            }\n        };\n        searchBookModel = new SearchBookModel(searchListener);\n        binding.swipeRefreshLayout.setColorSchemeColors(ThemeStore.accentColor(MApplication.getInstance()));\n        binding.swipeRefreshLayout.setOnRefreshListener(() -> {\n            if (!isLoading) {\n                isLoading = true;\n                long time = System.currentTimeMillis();\n                searchBookModel.setSearchTime(time);\n                searchBookModel.search(name, time, new ArrayList<>(), false);\n            }\n        });\n        Single.create((SingleOnSubscribe<Boolean>) e -> {\n            List<SearchBookBean> searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder()\n                    .where(SearchBookBeanDao.Properties.Name.eq(name), SearchBookBeanDao.Properties.Author.eq(author), SearchBookBeanDao.Properties.CoverUrl.isNotNull())\n                    .build().list();\n            for (SearchBookBean searchBook : searchBookBeans) {\n                BookSourceBean bean = BookSourceManager.getBookSourceByUrl(searchBook.getTag());\n                if (bean != null) {\n                    String url = searchBook.getCoverUrl();\n                    if (url != null && !urls.contains(url)) {\n                        urls.add(url);\n                        origins.add(searchBook.getOrigin());\n                    }\n                }\n            }\n            e.onSuccess(true);\n        }).compose(RxUtils::toSimpleSingle)\n                .subscribe(new MySingleObserver<Boolean>() {\n                    @Override\n                    public void onSuccess(Boolean aBoolean) {\n                        if (urls.isEmpty()) {\n                            binding.swipeRefreshLayout.setRefreshing(true);\n                            long time = System.currentTimeMillis();\n                            searchBookModel.setSearchTime(time);\n                            searchBookModel.search(name, time, new ArrayList<>(), false);\n                        } else {\n                            changeCoverAdapter.notifyDataSetChanged();\n                            isLoading = false;\n                        }\n                    }\n                });\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        searchBookModel.onDestroy();\n    }\n\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    public class ChangeCoverAdapter extends RefreshRecyclerViewAdapter {\n\n        ChangeCoverAdapter() {\n            super(false);\n        }\n\n        @Override\n        public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) {\n            return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_change_cover, parent, false));\n        }\n\n        @Override\n        public void onBindIViewHolder(RecyclerView.ViewHolder holder, int position) {\n            MyViewHolder myViewHolder = (MyViewHolder) holder;\n            myViewHolder.bind(urls.get(position), origins.get(position), holder);\n        }\n\n        @Override\n        public int getIViewType(int position) {\n            return 0;\n        }\n\n        @Override\n        public int getICount() {\n            return urls.size();\n        }\n\n        class MyViewHolder extends RecyclerView.ViewHolder {\n\n            ImageView ivCover;\n            TextView tvSourceName;\n\n            MyViewHolder(View itemView) {\n                super(itemView);\n                ivCover = itemView.findViewById(R.id.iv_cover);\n                tvSourceName = itemView.findViewById(R.id.tv_source_name);\n            }\n\n            public void bind(String url, String origin, RecyclerView.ViewHolder holder) {\n                tvSourceName.setText(origin);\n                Glide.with(holder.itemView.getContext())\n                        .load(url)\n                        .error(R.drawable.image_cover_default)\n                        .into(ivCover);\n                ivCover.setOnClickListener(view -> {\n                    Intent intent = new Intent();\n                    intent.putExtra(\"url\", url);\n                    setResult(RESULT_OK, intent);\n                    finish();\n                });\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/BookDetailActivity.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.activity;\n\nimport static com.kunfei.bookshelf.presenter.BookDetailPresenter.FROM_BOOKSHELF;\n\nimport android.annotation.SuppressLint;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.text.method.ScrollingMovementMethod;\nimport android.util.Log;\nimport android.view.Gravity;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.View;\nimport android.widget.PopupMenu;\nimport android.widget.RadioButton;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.FileProvider;\n\nimport com.bumptech.glide.RequestBuilder;\nimport com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;\nimport com.bumptech.glide.request.RequestOptions;\nimport com.google.zxing.EncodeHintType;\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.AppActivityManager;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.BuildConfig;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.ActivityBookDetailBinding;\nimport com.kunfei.bookshelf.help.BlurTransformation;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.glide.ImageLoader;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.presenter.BookDetailPresenter;\nimport com.kunfei.bookshelf.presenter.ReadBookPresenter;\nimport com.kunfei.bookshelf.presenter.contract.BookDetailContract;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.widget.modialog.ChangeSourceDialog;\nimport com.kunfei.bookshelf.widget.modialog.MoDialogHUD;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\n\nimport cn.bingoogolapple.qrcode.zxing.QRCodeEncoder;\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\npublic class BookDetailActivity extends MBaseActivity<BookDetailContract.Presenter> implements BookDetailContract.View {\n    private ActivityBookDetailBinding binding;\n    private MoDialogHUD moDialogHUD;\n    private String author;\n    private BookShelfBean bookShelfBean;\n    private String coverPath;\n    private String bookUrl;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected BookDetailContract.Presenter initInjector() {\n        return new BookDetailPresenter();\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        setTheme(R.style.CAppTransparentTheme);\n        binding = ActivityBookDetailBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void initData() {\n        mPresenter.initData(getIntent());\n    }\n\n    @Override\n    protected void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        String dataKey = String.valueOf(System.currentTimeMillis());\n        getIntent().putExtra(\"openFrom\", FROM_BOOKSHELF);\n        getIntent().putExtra(\"data_key\", dataKey);\n        BitIntentDataManager.getInstance().putData(dataKey, mPresenter.getBookShelf());\n    }\n\n    @Override\n    protected void bindView() {\n        //弹窗\n        moDialogHUD = new MoDialogHUD(this);\n        binding.tvIntro.setMovementMethod(ScrollingMovementMethod.getInstance());\n        if (mPresenter.getOpenFrom() == FROM_BOOKSHELF) {\n            updateView();\n        } else {\n            if (mPresenter.getSearchBook() == null) return;\n            SearchBookBean searchBookBean = mPresenter.getSearchBook();\n            upImageView(searchBookBean.getCoverUrl(), searchBookBean.getName(), searchBookBean.getAuthor());\n            binding.tvName.setText(searchBookBean.getName());\n            author = searchBookBean.getAuthor();\n            binding.tvAuthor.setText(TextUtils.isEmpty(author) ? \"未知\" : author);\n            bookUrl = searchBookBean.getNoteUrl();\n            String origin = TextUtils.isEmpty(searchBookBean.getOrigin()) ? \"未知\" : searchBookBean.getOrigin();\n            binding.tvOrigin.setText(origin);\n            binding.tvChapter.setText(searchBookBean.getLastChapter());  // newest\n            binding.tvIntro.setText(StringUtils.formatHtml2Intor(searchBookBean.getIntroduce()));\n            binding.tvShelf.setText(R.string.add_to_shelf);\n            binding.tvRead.setText(R.string.start_read);\n            binding.tvRead.setOnClickListener(v -> {\n                //放入书架\n            });\n            binding.tvIntro.setVisibility(View.INVISIBLE);\n            binding.tvLoading.setVisibility(View.VISIBLE);\n            binding.tvLoading.setText(R.string.loading);\n            binding.tvLoading.setOnClickListener(null);\n        }\n    }\n\n    @Override\n    public void updateView() {\n        bookShelfBean = mPresenter.getBookShelf();\n        BookInfoBean bookInfoBean;\n        if (null != bookShelfBean) {\n            if (BookShelfBean.LOCAL_TAG.equals(bookShelfBean.getTag())) {\n                binding.ivMenu.setVisibility(View.GONE);\n            } else {\n                binding.ivMenu.setVisibility(View.VISIBLE);\n            }\n            bookInfoBean = bookShelfBean.getBookInfoBean();\n            binding.tvName.setText(bookInfoBean.getName());\n            author = bookInfoBean.getAuthor();\n            binding.tvAuthor.setText(TextUtils.isEmpty(author) ? \"未知\" : author);\n            bookUrl = bookInfoBean.getNoteUrl();\n            ((RadioButton) binding.rgBookGroup.getChildAt(bookShelfBean.getGroup())).setChecked(true);\n            if (mPresenter.getInBookShelf()) {\n                binding.tvChapter.setText(bookShelfBean.getDurChapterName()); // last\n                binding.tvShelf.setText(R.string.remove_from_bookshelf);\n                binding.tvRead.setText(R.string.continue_read);\n                binding.tvShelf.setOnClickListener(v -> {\n                    //从书架移出\n                    mPresenter.removeFromBookShelf();\n                });\n            } else {\n                if (!TextUtils.isEmpty(bookShelfBean.getLastChapterName())) {\n                    binding.tvChapter.setText(bookShelfBean.getLastChapterName()); // last\n                }\n                binding.tvShelf.setText(R.string.add_to_shelf);\n                binding.tvRead.setText(R.string.start_read);\n                binding.tvShelf.setOnClickListener(v -> {\n                    //放入书架\n                    mPresenter.addToBookShelf();\n                });\n            }\n            binding.tvIntro.setText(StringUtils.formatHtml2Intor(bookInfoBean.getIntroduce()));\n            if (binding.tvIntro.getVisibility() != View.VISIBLE) {\n                binding.tvIntro.setVisibility(View.VISIBLE);\n            }\n            String origin = bookInfoBean.getOrigin();\n            if (!TextUtils.isEmpty(origin)) {\n                binding.ivWeb.setVisibility(View.VISIBLE);\n                binding.tvOrigin.setText(origin);\n            } else {\n                binding.ivWeb.setVisibility(View.INVISIBLE);\n                binding.tvOrigin.setVisibility(View.INVISIBLE);\n            }\n            if (!TextUtils.isEmpty(bookShelfBean.getCustomCoverPath())) {\n                upImageView(bookShelfBean.getCustomCoverPath(), bookInfoBean.getName(), bookInfoBean.getAuthor());\n            } else {\n                upImageView(bookInfoBean.getCoverUrl(), bookInfoBean.getName(), bookInfoBean.getAuthor());\n            }\n            if (bookShelfBean.getTag().equals(BookShelfBean.LOCAL_TAG)) {\n                binding.tvChangeOrigin.setVisibility(View.INVISIBLE);\n            } else {\n                binding.tvChangeOrigin.setVisibility(View.VISIBLE);\n            }\n            upChapterSizeTv();\n        }\n        binding.tvLoading.setVisibility(View.GONE);\n        binding.tvLoading.setOnClickListener(null);\n    }\n\n    @Override\n    public void getBookShelfError() {\n        binding.tvLoading.setVisibility(View.VISIBLE);\n        binding.tvLoading.setText(R.string.load_error_retry);\n        binding.tvLoading.setOnClickListener(v -> {\n            binding.tvLoading.setText(R.string.loading);\n            binding.tvLoading.setOnClickListener(null);\n            mPresenter.getBookShelfInfo();\n        });\n    }\n\n    private void upImageView(String path, String name, String author) {\n        binding.ivCover.load(path, name, author);\n        ImageLoader.INSTANCE.load(this, path)\n                .transition(DrawableTransitionOptions.withCrossFade(1500))\n                .thumbnail(defaultCover())\n                .centerCrop()\n                .apply(RequestOptions.bitmapTransform(new BlurTransformation(this, 25)))\n                .into(binding.ivBlurCover);  //模糊、渐变、缩小效果\n    }\n\n    private RequestBuilder<Drawable> defaultCover() {\n        return ImageLoader.INSTANCE.load(this, R.drawable.image_cover_default)\n                .apply(RequestOptions.bitmapTransform(new BlurTransformation(this, 25)));\n    }\n\n    private void refresh() {\n        binding.tvLoading.setVisibility(View.VISIBLE);\n        binding.tvLoading.setText(R.string.loading);\n        binding.tvLoading.setOnClickListener(null);\n        mPresenter.getBookShelf().getBookInfoBean().setBookInfoHtml(null);\n        mPresenter.getBookShelf().getBookInfoBean().setChapterListHtml(null);\n        mPresenter.getBookShelfInfo();\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    protected void bindEvent() {\n        binding.tvName.setOnClickListener(v -> {\n            if (bookShelfBean == null) return;\n            if (TextUtils.isEmpty(bookShelfBean.getBookInfoBean().getName())) return;\n            if (!AppActivityManager.getInstance().isExist(SearchBookActivity.class)) {\n                SearchBookActivity.startByKey(this, bookShelfBean.getBookInfoBean().getName());\n            } else {\n                RxBus.get().post(RxBusTag.SEARCH_BOOK, bookShelfBean.getBookInfoBean().getName());\n            }\n            finish();\n        });\n        binding.ivBlurCover.setOnClickListener(null);\n        binding.iflContent.setOnClickListener(v -> finish());\n        binding.tvToc.setOnClickListener(v -> {\n            ChapterListActivity.startThis(this, mPresenter.getBookShelf(), mPresenter.getChapterList());\n        });\n        binding.tvChangeOrigin.setOnClickListener(view ->\n                ChangeSourceDialog.builder(BookDetailActivity.this, mPresenter.getBookShelf())\n                        .setCallback(searchBookBean -> {\n                            binding.tvOrigin.setText(searchBookBean.getOrigin());\n                            binding.tvLoading.setVisibility(View.VISIBLE);\n                            binding.tvLoading.setText(R.string.loading);\n                            binding.tvLoading.setOnClickListener(null);\n                            if (mPresenter.getOpenFrom() == FROM_BOOKSHELF) {\n                                mPresenter.changeBookSource(searchBookBean);\n                            } else {\n                                mPresenter.initBookFormSearch(searchBookBean);\n                                mPresenter.getBookShelfInfo();\n                            }\n                        }).show());\n\n        binding.tvRead.setOnClickListener(v -> readBook());\n\n        binding.ivMenu.setOnClickListener(view -> {\n            PopupMenu popupMenu = new PopupMenu(this, view, Gravity.END);\n            if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) {\n                popupMenu.getMenu().add(Menu.NONE, R.id.menu_refresh, Menu.NONE, R.string.refresh);\n            }\n            if (mPresenter.getInBookShelf() && !mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) {\n                if (mPresenter.getBookShelf().getAllowUpdate()) {\n                    popupMenu.getMenu().add(Menu.NONE, R.id.menu_disable_update, Menu.NONE, R.string.disable_update);\n                } else {\n                    popupMenu.getMenu().add(Menu.NONE, R.id.menu_allow_update, Menu.NONE, R.string.allow_update);\n                }\n            }\n            if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) {\n                popupMenu.getMenu().add(Menu.NONE, R.id.menu_edit, Menu.NONE, R.string.edit_book_source);\n            }\n            if (!mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG)) {\n                popupMenu.getMenu().add(Menu.NONE, R.id.menu_copy_url, Menu.NONE, R.string.copy_url);\n            }\n            popupMenu.getMenu().add(Menu.NONE, R.id.menu_share, Menu.NONE, R.string.share_book);\n            popupMenu.setOnMenuItemClickListener(menuItem -> {\n                int itemId = menuItem.getItemId();\n                if (itemId == R.id.menu_refresh) {\n                    refresh();\n                } else if (itemId == R.id.menu_allow_update) {\n                    mPresenter.getBookShelf().setAllowUpdate(true);\n                    mPresenter.addToBookShelf();\n                } else if (itemId == R.id.menu_disable_update) {\n                    mPresenter.getBookShelf().setAllowUpdate(false);\n                    mPresenter.addToBookShelf();\n                } else if (itemId == R.id.menu_edit) {\n                    BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(mPresenter.getBookShelf().getTag());\n                    if (sourceBean != null) {\n                        SourceEditActivity.startThis(this, sourceBean);\n                    }\n                } else if (itemId == R.id.menu_copy_url) {\n                    ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);\n                    ClipData clipData = ClipData.newPlainText(null, mPresenter.getBookShelf().getNoteUrl());\n                    if (clipboard != null) {\n                        clipboard.setPrimaryClip(clipData);\n                        toast(R.string.copy_complete);\n                    }\n                } else if (itemId == R.id.menu_share) {\n                    share();\n                }\n                return true;\n            });\n            popupMenu.show();\n        });\n\n        binding.ivCover.setOnClickListener(view -> {\n            if (mPresenter.getOpenFrom() == FROM_BOOKSHELF) {\n                BookInfoEditActivity.startThis(this, mPresenter.getBookShelf().getNoteUrl());\n            }\n        });\n\n        binding.tvAuthor.setOnClickListener(view -> {\n            if (TextUtils.isEmpty(author)) return;\n            if (!AppActivityManager.getInstance().isExist(SearchBookActivity.class)) {\n                SearchBookActivity.startByKey(this, author);\n            } else {\n                RxBus.get().post(RxBusTag.SEARCH_BOOK, author);\n            }\n            finish();\n        });\n\n        binding.rgBookGroup.setOnCheckedChangeListener((radioGroup, i) -> {\n            View checkView = radioGroup.findViewById(i);\n            if (!checkView.isPressed()) {\n                return;\n            }\n            int idx = radioGroup.indexOfChild(checkView) % (getResources().getStringArray(R.array.book_group_array).length - 1);\n            mPresenter.getBookShelf().setGroup(idx);\n            if (mPresenter.getInBookShelf()) {\n                mPresenter.addToBookShelf();\n            }\n        });\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        if (mPresenter.getOpenFrom() == BookDetailPresenter.FROM_SEARCH) {\n            //网络请求\n            mPresenter.getBookShelfInfo();\n        }\n    }\n\n    @Override\n    public void readBook() {\n        if (!mPresenter.getInBookShelf()) {\n            BookshelfHelp.saveBookToShelf(mPresenter.getBookShelf());\n            if (mPresenter.getChapterList() != null)\n                DbHelper.getDaoSession().getBookChapterBeanDao().insertOrReplaceInTx(mPresenter.getChapterList());\n        }\n        Intent intent = new Intent(BookDetailActivity.this, ReadBookActivity.class);\n        intent.putExtra(\"openFrom\", ReadBookPresenter.OPEN_FROM_APP);\n        intent.putExtra(\"inBookshelf\", mPresenter.getInBookShelf());\n        String key = String.valueOf(System.currentTimeMillis());\n        String bookKey = \"book\" + key;\n        intent.putExtra(\"bookKey\", bookKey);\n        BitIntentDataManager.getInstance().putData(bookKey, mPresenter.getBookShelf().clone());\n        startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out);\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            if (getStart_share_ele()) {\n                finishAfterTransition();\n            } else {\n                finish();\n                overridePendingTransition(0, android.R.anim.fade_out);\n            }\n        } else {\n            finish();\n            overridePendingTransition(0, android.R.anim.fade_out);\n        }\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    private void upChapterSizeTv() {\n        String chapterSize = \"\";\n        if (mPresenter.getOpenFrom() == FROM_BOOKSHELF && bookShelfBean.getChapterListSize() > 0) {\n            int newChapterNum = bookShelfBean.getChapterListSize() - 1 - bookShelfBean.getDurChapter();\n            if (newChapterNum > 0)\n                chapterSize = String.format(\"(+%d)\", newChapterNum);\n        }\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        Boolean mo = moDialogHUD.onKeyDown(keyCode, event);\n        if (mo) return true;\n        return super.onKeyDown(keyCode, event);\n    }\n\n    @Override\n    public void finish() {\n        super.finish();\n        overridePendingTransition(0, android.R.anim.fade_out);\n    }\n\n    @Override\n    public void onDestroy() {\n        moDialogHUD.dismiss();\n        super.onDestroy();\n    }\n\n    private void share() {\n\n        Single.create((SingleOnSubscribe<Bitmap>) emitter -> {\n            // 使用url\n            String url = mPresenter.getBookShelf().getNoteUrl();\n            if (url == null)\n                url = \"\";\n            int maxLength = 1273 - 1 - url.length();\n\n            BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(mPresenter.getBookShelf().getTag());\n\n            if (sourceBean != null) {\n//                    url=tvBookUrl.getText().toString()+\"#\"+ gson.toJson(sourceBean).replaceAll(\"\\n\\\\s*\\\"[a-zA-Z]+\\\"(:\\\"\\\"|: \\\"\\\"| :\\\"\\\"| : \\\"\\\")\\\\s*,\\\\s*\\n\",\"\\n\").trim();\n                url = url + \"#\" + sourceBean.getJson(maxLength);\n\n                Log.d(\"QRcode\", \"Length=\" + url.length() + \"\\n\" + url);\n                Bitmap bitmap;\n                QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);\n                if (url.length() > 300)\n                    bitmap = QRCodeEncoder.syncEncodeQRCode(url, 800);\n                else if (url.length() > 100)\n                    bitmap = QRCodeEncoder.syncEncodeQRCode(url, 500);\n                else\n                    bitmap = QRCodeEncoder.syncEncodeQRCode(url, 300);\n                QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);\n                emitter.onSuccess(bitmap);\n            }\n        }).compose(RxUtils::toSimpleSingle)\n                .subscribe(new MySingleObserver<Bitmap>() {\n\n                    @Override\n                    public void onSuccess(Bitmap bitmap2) {\n\n                        try {\n                            File file = new File(BookDetailActivity.this.getExternalCacheDir(), binding.tvName.getText().toString() + \".png\");\n                            FileOutputStream fOut = new FileOutputStream(file);\n                            bitmap2.compress(Bitmap.CompressFormat.PNG, 80, fOut);\n                            fOut.flush();\n                            fOut.close();\n                            //noinspection ResultOfMethodCallIgnored\n                            file.setReadable(true, false);\n                            Uri contentUri = FileProvider.getUriForFile(BookDetailActivity.this, BuildConfig.APPLICATION_ID + \".fileProvider\", file);\n                            final Intent intent = new Intent(Intent.ACTION_SEND);\n                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                            intent.putExtra(Intent.EXTRA_STREAM, contentUri);\n                            intent.setType(\"image/png\");\n                            startActivity(Intent.createChooser(intent, \"分享书籍\"));\n                        } catch (Exception e) {\n                            toast(e.getLocalizedMessage());\n                        }\n                    }\n                });\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/BookInfoEditActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.ActivityBookInfoEditBinding;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.utils.RealPathUtil;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.widget.modialog.MoDialogHUD;\n\nimport kotlin.Unit;\n\npublic class BookInfoEditActivity extends MBaseActivity<IPresenter> {\n    private final int ResultSelectCover = 103;\n    private final int ResultEditCover = 104;\n    private ActivityBookInfoEditBinding binding;\n    private String noteUrl;\n    private BookShelfBean book;\n    private MoDialogHUD moDialogHUD;\n\n\n    public static void startThis(Context context, String noteUrl) {\n        Intent intent = new Intent(context, BookInfoEditActivity.class);\n        intent.putExtra(\"noteUrl\", noteUrl);\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        context.startActivity(intent);\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        if (savedInstanceState != null && !TextUtils.isEmpty(savedInstanceState.getString(\"noteUrl\"))) {\n            noteUrl = savedInstanceState.getString(\"noteUrl\");\n        }\n    }\n\n    @Override\n    protected void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putString(\"noteUrl\", noteUrl);\n    }\n\n    /**\n     * 布局载入  setContentView()\n     */\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityBookInfoEditBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        binding.tilBookName.setHint(getString(R.string.book_name));\n        binding.tilBookAuthor.setHint(getString(R.string.author));\n        binding.tilCoverUrl.setHint(getString(R.string.cover_path));\n        binding.tilBookJj.setHint(getString(R.string.book_intro));\n        moDialogHUD = new MoDialogHUD(this);\n    }\n\n    /**\n     * 数据初始化\n     */\n    @Override\n    protected void initData() {\n        if (!TextUtils.isEmpty(getIntent().getStringExtra(\"noteUrl\"))) {\n            noteUrl = getIntent().getStringExtra(\"noteUrl\");\n        }\n        if (!TextUtils.isEmpty(noteUrl)) {\n            book = BookshelfHelp.getBook(noteUrl);\n            if (book != null) {\n                binding.tieBookName.setText(book.getBookInfoBean().getName());\n                binding.tieBookAuthor.setText(book.getBookInfoBean().getAuthor());\n                binding.tieBookJj.setText(book.getBookInfoBean().getIntroduce());\n                if (TextUtils.isEmpty(book.getCustomCoverPath())) {\n                    binding.tieCoverUrl.setText(book.getBookInfoBean().getCoverUrl());\n                } else {\n                    binding.tieCoverUrl.setText(book.getCustomCoverPath());\n                }\n            }\n            initCover();\n        }\n    }\n\n    /**\n     * 事件触发绑定\n     */\n    @Override\n    protected void bindEvent() {\n        super.bindEvent();\n        binding.tvSelectCover.setOnClickListener(view -> selectCover());\n        binding.tvChangeCover.setOnClickListener(view -> {\n            Intent intent = new Intent(BookInfoEditActivity.this, BookCoverEditActivity.class);\n            intent.putExtra(\"name\", book.getBookInfoBean().getName());\n            intent.putExtra(\"author\", book.getBookInfoBean().getAuthor());\n            startActivityForResult(intent, ResultEditCover);\n        });\n        binding.tvRefreshCover.setOnClickListener(view -> {\n            book.setCustomCoverPath(binding.tieCoverUrl.getText().toString());\n            initCover();\n        });\n    }\n\n    private void selectCover() {\n        new PermissionsCompat.Builder(this)\n                .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                .rationale(R.string.bg_image_per)\n                .onGranted((requestCode) -> {\n                    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);\n                    intent.addCategory(Intent.CATEGORY_OPENABLE);\n                    intent.setType(\"image/*\");\n                    startActivityForResult(intent, ResultSelectCover);\n                    return Unit.INSTANCE;\n                })\n                .request();\n    }\n\n    private void initCover() {\n        if (!this.isFinishing() && book != null) {\n            binding.ivCover.load(book.getCoverPath(), book.getName(), book.getAuthor());\n        }\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.book_info);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_book_info, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_save) {\n            saveInfo();\n        } else if (id == android.R.id.home) {\n            SoftInputUtil.hideIMM(getCurrentFocus());\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    private void saveInfo() {\n        if (book != null) {\n            book.getBookInfoBean().setName(binding.tieBookName.getText().toString());\n            book.getBookInfoBean().setAuthor(binding.tieBookAuthor.getText().toString());\n            book.getBookInfoBean().setIntroduce(binding.tieBookJj.getText().toString());\n            book.setCustomCoverPath(binding.tieCoverUrl.getText().toString());\n            initCover();\n            BookshelfHelp.saveBookToShelf(book);\n            RxBus.get().post(RxBusTag.HAD_ADD_BOOK, book);\n            SoftInputUtil.hideIMM(getCurrentFocus());\n        }\n        finish();\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        Boolean mo = moDialogHUD.onKeyDown(keyCode, event);\n        if (mo) return true;\n        return super.onKeyDown(keyCode, event);\n    }\n\n    @Override\n    public void onDestroy() {\n        moDialogHUD.dismiss();\n        super.onDestroy();\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        switch (requestCode) {\n            case ResultSelectCover:\n                if (resultCode == RESULT_OK && null != data) {\n                    binding.tieCoverUrl.setText(RealPathUtil.getPath(this, data.getData()));\n                    book.setCustomCoverPath(binding.tieCoverUrl.getText().toString());\n                    initCover();\n                }\n                break;\n            case ResultEditCover:\n                if (resultCode == RESULT_OK && null != data) {\n                    String url = data.getStringExtra(\"url\");\n                    binding.tieCoverUrl.setText(url);\n                    book.setCustomCoverPath(url);\n                    initCover();\n                }\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/BookSourceActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.SubMenu;\nimport android.widget.LinearLayout;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.recyclerview.widget.DividerItemDecoration;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.dao.BookSourceBeanDao;\nimport com.kunfei.bookshelf.databinding.ActivityBookSourceBinding;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.presenter.BookSourcePresenter;\nimport com.kunfei.bookshelf.presenter.contract.BookSourceContract;\nimport com.kunfei.bookshelf.service.ShareService;\nimport com.kunfei.bookshelf.utils.ACache;\nimport com.kunfei.bookshelf.utils.RealPathUtil;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.BookSourceAdapter;\nimport com.kunfei.bookshelf.widget.filepicker.picker.FilePicker;\nimport com.kunfei.bookshelf.widget.modialog.InputDialog;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport kotlin.Unit;\n\n/**\n * Created by GKF on 2017/12/16.\n * 书源管理\n */\n\npublic class BookSourceActivity extends MBaseActivity<BookSourceContract.Presenter> implements BookSourceContract.View {\n    private final int IMPORT_SOURCE = 102;\n    private final int REQUEST_QR = 202;\n    private ActivityBookSourceBinding binding;\n    private ItemTouchCallback itemTouchCallback;\n    private boolean selectAll = true;\n    private MenuItem groupItem;\n    private SubMenu groupMenu;\n    private BookSourceAdapter adapter;\n    private SearchView.SearchAutoComplete mSearchAutoComplete;\n    private boolean isSearch;\n\n    public static void startThis(Activity activity, int requestCode) {\n        activity.startActivityForResult(new Intent(activity, BookSourceActivity.class), requestCode);\n    }\n\n    @Override\n    protected BookSourceContract.Presenter initInjector() {\n        return new BookSourcePresenter();\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityBookSourceBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n\n    @Override\n    protected void initData() {\n\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        initSearchView();\n        initRecyclerView();\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        refreshBookSource();\n    }\n\n    private void initSearchView() {\n        mSearchAutoComplete = binding.searchView.findViewById(R.id.search_src_text);\n        mSearchAutoComplete.setTextSize(16);\n        binding.searchView.setQueryHint(getString(R.string.search_book_source));\n        binding.searchView.onActionViewExpanded();\n        binding.searchView.clearFocus();\n        binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                return false;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                isSearch = !TextUtils.isEmpty(newText);\n                refreshBookSource();\n                return false;\n            }\n        });\n    }\n\n    private void initRecyclerView() {\n        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));\n        binding.recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));\n        adapter = new BookSourceAdapter(this);\n        binding.recyclerView.setAdapter(adapter);\n        itemTouchCallback = new ItemTouchCallback();\n        itemTouchCallback.setOnItemTouchCallbackListener(adapter.getItemTouchCallbackListener());\n        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback);\n        itemTouchHelper.attachToRecyclerView(binding.recyclerView);\n        setDragEnable(getSort());\n    }\n\n    private void setDragEnable(int sort) {\n        if (itemTouchCallback == null) {\n            return;\n        }\n        adapter.setSort(sort);\n        itemTouchCallback.setDragEnable(sort == 0);\n    }\n\n    public void upDateSelectAll() {\n        selectAll = true;\n        for (BookSourceBean bookSourceBean : adapter.getDataList()) {\n            if (!bookSourceBean.getEnable()) {\n                selectAll = false;\n                break;\n            }\n        }\n    }\n\n    private void selectAllDataS() {\n        for (BookSourceBean bookSourceBean : adapter.getDataList()) {\n            bookSourceBean.setEnable(!selectAll);\n        }\n        adapter.notifyDataSetChanged();\n        selectAll = !selectAll;\n        AsyncTask.execute(() -> DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplaceInTx(adapter.getDataList()));\n        setResult(RESULT_OK);\n    }\n\n    private void revertSelection() {\n        for (BookSourceBean bookSourceBean : adapter.getDataList()) {\n            bookSourceBean.setEnable(!bookSourceBean.getEnable());\n        }\n        adapter.notifyDataSetChanged();\n        saveDate(adapter.getDataList());\n        setResult(RESULT_OK);\n    }\n\n    public void upSearchView(int size) {\n        binding.searchView.setQueryHint(getString(R.string.search_book_source_num, size));\n    }\n\n    @Override\n    public void refreshBookSource() {\n        if (isSearch) {\n            List<BookSourceBean> sourceBeanList;\n            if (binding.searchView.getQuery().toString().equals(\"enabled\")) {\n                sourceBeanList = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                        .where(BookSourceBeanDao.Properties.Enable.eq(1))\n                        .orderRaw(BookSourceManager.getBookSourceSort())\n                        .orderAsc(BookSourceBeanDao.Properties.SerialNumber)\n                        .list();\n            } else {\n                String term = \"%\" + binding.searchView.getQuery() + \"%\";\n                sourceBeanList = DbHelper.getDaoSession().getBookSourceBeanDao().queryBuilder()\n                        .whereOr(BookSourceBeanDao.Properties.BookSourceName.like(term),\n                                BookSourceBeanDao.Properties.BookSourceGroup.like(term),\n                                BookSourceBeanDao.Properties.BookSourceUrl.like(term))\n                        .orderRaw(BookSourceManager.getBookSourceSort())\n                        .orderAsc(BookSourceBeanDao.Properties.SerialNumber)\n                        .list();\n            }\n\n            adapter.resetDataS(sourceBeanList);\n        } else {\n            adapter.resetDataS(BookSourceManager.getAllBookSource());\n        }\n    }\n\n    public void delBookSource(BookSourceBean bookSource) {\n        mPresenter.delData(bookSource);\n        setResult(RESULT_OK);\n    }\n\n    public void saveDate(BookSourceBean date) {\n        mPresenter.saveData(date);\n        setResult(RESULT_OK);\n    }\n\n    public void saveDate(List<BookSourceBean> date) {\n        mPresenter.saveData(date);\n        setResult(RESULT_OK);\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.book_source_manage);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_book_source_activity, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    @Override\n    public boolean onPrepareOptionsMenu(Menu menu) {\n        groupItem = menu.findItem(R.id.action_group);\n        groupMenu = groupItem.getSubMenu();\n        upGroupMenu();\n        upSortMenu();\n        return super.onPrepareOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_add_book_source) {\n            addBookSource();\n        } else if (id == R.id.action_select_all) {\n            selectAllDataS();\n        } else if (id == R.id.action_import_book_source_local) {\n            selectBookSourceFile();\n        } else if (id == R.id.action_import_book_source_onLine) {\n            importBookSourceOnLine();\n        } else if (id == R.id.action_import_book_source_rwm) {\n            scanBookSource();\n        } else if (id == R.id.action_revert_selection) {\n            revertSelection();\n        } else if (id == R.id.action_del_select) {\n            deleteDialog();\n        } else if (id == R.id.action_check_book_source) {\n            mPresenter.checkBookSource(adapter.getSelectDataList());\n        } else if (id == R.id.action_check_find_source) {\n            mPresenter.checkFindSource(adapter.getSelectDataList());\n        } else if (id == R.id.sort_manual) {\n            upSourceSort(0);\n        } else if (id == R.id.sort_auto) {\n            upSourceSort(1);\n        } else if (id == R.id.sort_pin_yin) {\n            upSourceSort(2);\n        } else if (id == R.id.show_enabled) {\n            binding.searchView.setQuery(\"enabled\", false);\n        } else if (id == R.id.action_share_wifi) {\n            ShareService.startThis(this, adapter.getSelectDataList());\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        if (item.getGroupId() == R.id.source_group) {\n            binding.searchView.setQuery(item.getTitle(), true);\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    public void upGroupMenu() {\n        if (groupMenu == null) return;\n        groupMenu.removeGroup(R.id.source_group);\n        List<String> groupList = BookSourceManager.getGroupList();\n        for (String groupName : new ArrayList<>(groupList)) {\n            groupMenu.add(R.id.source_group, Menu.NONE, Menu.NONE, groupName);\n        }\n    }\n\n    private void upSortMenu() {\n        groupMenu.getItem(0).setChecked(false);\n        groupMenu.getItem(1).setChecked(false);\n        groupMenu.getItem(2).setChecked(false);\n        groupMenu.getItem(getSort()).setChecked(true);\n    }\n\n    private void upSourceSort(int sort) {\n        preferences.edit().putInt(\"SourceSort\", sort).apply();\n        upSortMenu();\n        setDragEnable(sort);\n        refreshBookSource();\n    }\n\n    public int getSort() {\n        return preferences.getInt(\"SourceSort\", 0);\n    }\n\n    private void scanBookSource() {\n        Intent intent = new Intent(this, QRCodeScanActivity.class);\n        startActivityForResult(intent, REQUEST_QR);\n    }\n\n    private void addBookSource() {\n        Intent intent = new Intent(this, SourceEditActivity.class);\n        startActivityForResult(intent, SourceEditActivity.EDIT_SOURCE);\n    }\n\n    private void deleteDialog() {\n        AlertDialog alertDialog = new AlertDialog.Builder(this)\n                .setTitle(R.string.delete)\n                .setMessage(R.string.del_msg)\n                .setPositiveButton(R.string.ok, (dialog, which) -> mPresenter.delData(adapter.getSelectDataList()))\n                .setNegativeButton(R.string.cancel, (dialogInterface, i) -> {\n                })\n                .show();\n        ATH.setAlertDialogTint(alertDialog);\n    }\n\n    private void selectBookSourceFile() {\n        new PermissionsCompat.Builder(this)\n                .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                .rationale(R.string.please_grant_storage_permission)\n                .onGranted((requestCode) -> {\n                    FilePicker filePicker = new FilePicker(BookSourceActivity.this, FilePicker.FILE);\n                    filePicker.setBackgroundColor(getResources().getColor(R.color.background));\n                    filePicker.setTopBackgroundColor(getResources().getColor(R.color.background));\n                    filePicker.setAllowExtensions(getResources().getStringArray(R.array.text_suffix));\n                    filePicker.setOnFilePickListener(s -> mPresenter.importBookSourceLocal(s));\n                    filePicker.show();\n                    filePicker.getSubmitButton().setText(R.string.sys_file_picker);\n                    filePicker.getSubmitButton().setOnClickListener(view -> {\n                        filePicker.dismiss();\n                        selectFileSys();\n                    });\n                    return Unit.INSTANCE;\n                })\n                .request();\n    }\n\n    private void importBookSourceOnLine() {\n        String cu = ACache.get(this).getAsString(\"sourceUrl\");\n        String[] cacheUrls = cu == null ? new String[]{} : cu.split(\";\");\n        List<String> urlList = new ArrayList<>(Arrays.asList(cacheUrls));\n        InputDialog.builder(this)\n                .setDefaultValue(\"\")\n                .setTitle(getString(R.string.input_book_source_url))\n                .setShowDel(true)\n                .setAdapterValues(urlList)\n                .setCallback(new InputDialog.Callback() {\n                    @Override\n                    public void setInputText(String inputText) {\n                        inputText = StringUtils.trim(inputText);\n                        if (!urlList.contains(inputText)) {\n                            urlList.add(0, inputText);\n                            ACache.get(BookSourceActivity.this).put(\"sourceUrl\", TextUtils.join(\";\", urlList));\n                        }\n                        mPresenter.importBookSource(inputText);\n                    }\n\n                    @Override\n                    public void delete(String value) {\n                        urlList.remove(value);\n                        ACache.get(BookSourceActivity.this).put(\"sourceUrl\", TextUtils.join(\";\", urlList));\n                    }\n                }).show();\n    }\n\n    private void selectFileSys() {\n        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);\n        intent.addCategory(Intent.CATEGORY_OPENABLE);\n        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{\"text/*\", \"application/json\"});\n        intent.setType(\"*/*\");//设置类型\n        startActivityForResult(intent, IMPORT_SOURCE);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (resultCode == RESULT_OK) {\n            switch (requestCode) {\n                case SourceEditActivity.EDIT_SOURCE:\n                    refreshBookSource();\n                    setResult(RESULT_OK);\n                    break;\n                case IMPORT_SOURCE:\n                    if (data != null && data.getData() != null) {\n                        mPresenter.importBookSourceLocal(RealPathUtil.getPath(this, data.getData()));\n                    }\n                    break;\n                case REQUEST_QR:\n                    if (data != null) {\n                        String result = data.getStringExtra(\"result\");\n                        if (!StringUtils.isTrimEmpty(result)) {\n\n                        if(result.replaceAll(\"\\\\s\",\"\").matches(\"^\\\\{.*\\\\}$\")) {\n                            mPresenter.importBookSource(result);\n                            break;\n                        }\n                            result=result.trim();\n                        String[] string=result.split(\"#\",2);\n                        if(string.length==2){\n                            if(string[1].replaceAll(\"\\\\s\",\"\").matches(\"^\\\\{.*\\\\}$\")) {\n                                mPresenter.importBookSource(string[1]);\n                                break;\n                            }\n                        }\n                        mPresenter.importBookSource(result);\n                    }}\n                    break;\n            }\n        }\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        if (keyCode == KeyEvent.KEYCODE_BACK) {\n            if (isSearch) {\n                try {\n                    //如果搜索框中有文字，则会先清空文字.\n                    mSearchAutoComplete.setText(\"\");\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n                return true;\n            }\n        }\n        return super.onKeyDown(keyCode, event);\n    }\n\n    @Override\n    public Snackbar getSnackBar(String msg, int length) {\n        return Snackbar.make(binding.llContent, msg, length);\n    }\n\n    @Override\n    public void showSnackBar(String msg, int length) {\n        super.showSnackBar(binding.llContent, msg, length);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ChapterListActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport static android.view.View.GONE;\nimport static android.view.View.VISIBLE;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.graphics.Color;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuItem;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.fragment.app.Fragment;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.BaseTabActivity;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.databinding.ActivityChapterlistBinding;\nimport com.kunfei.bookshelf.help.ReadBookControl;\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.MaterialValueHelper;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.fragment.BookmarkFragment;\nimport com.kunfei.bookshelf.view.fragment.ChapterListFragment;\n\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class ChapterListActivity extends BaseTabActivity<IPresenter> {\n\n    private ActivityChapterlistBinding binding;\n    private ReadBookControl readBookControl = ReadBookControl.getInstance();\n    private SearchView searchView;\n    private BookShelfBean bookShelf;\n    private List<BookChapterBean> chapterBeanList;\n\n    public static void startThis(Activity activity, BookShelfBean bookShelf, List<BookChapterBean> chapterBeanList) {\n        Intent intent = new Intent(activity, ChapterListActivity.class);\n        String key = String.valueOf(System.currentTimeMillis());\n        String bookKey = \"book\" + key;\n        intent.putExtra(\"bookKey\", bookKey);\n        BitIntentDataManager.getInstance().putData(bookKey, bookShelf.clone());\n        String chapterListKey = \"chapterList\" + key;\n        intent.putExtra(\"chapterListKey\", chapterListKey);\n        BitIntentDataManager.getInstance().putData(chapterListKey, chapterBeanList);\n        activity.startActivity(intent);\n    }\n\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        setOrientation(readBookControl.getScreenDirection());\n        super.onCreate(savedInstanceState);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    protected void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        if (bookShelf != null) {\n            String key = String.valueOf(System.currentTimeMillis());\n            String bookKey = \"book\" + key;\n            getIntent().putExtra(\"bookKey\", bookKey);\n            BitIntentDataManager.getInstance().putData(bookKey, bookShelf.clone());\n            String chapterListKey = \"chapterList\" + key;\n            getIntent().putExtra(\"chapterListKey\", chapterListKey);\n            BitIntentDataManager.getInstance().putData(chapterListKey, chapterBeanList);\n        }\n    }\n\n    @Override\n    protected void onDestroy() {\n        RxBus.get().unregister(this);\n        super.onDestroy();\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityChapterlistBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        setupActionBar();\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        mTlIndicator.setSelectedTabIndicatorColor(ThemeStore.accentColor(this));\n        mTlIndicator.setTabTextColors(ColorUtils.isColorLight(ThemeStore.primaryColor(this)) ? Color.BLACK : Color.WHITE,\n                ThemeStore.accentColor(this));\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    protected void initData() {\n        String bookKey = getIntent().getStringExtra(\"bookKey\");\n        bookShelf = (BookShelfBean) BitIntentDataManager.getInstance().getData(bookKey);\n        String chapterListKey = getIntent().getStringExtra(\"chapterListKey\");\n        chapterBeanList = (List<BookChapterBean>) BitIntentDataManager.getInstance().getData(chapterListKey);\n    }\n\n    /**************abstract***********/\n    @Override\n    protected List<Fragment> createTabFragments() {\n        ChapterListFragment chapterListFragment = new ChapterListFragment();\n        BookmarkFragment bookmarkFragment = new BookmarkFragment();\n        return Arrays.asList(chapterListFragment, bookmarkFragment);\n    }\n\n    @Override\n    protected List<String> createTabTitles() {\n        return Arrays.asList(getString(R.string.chapter_list), getString(R.string.bookmark));\n    }\n\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_search_view, menu);\n        MenuItem search = menu.findItem(R.id.action_search);\n        searchView = (SearchView) search.getActionView();\n        ATH.setTint(searchView, MaterialValueHelper.getPrimaryTextColor(this, ColorUtils.isColorLight(ThemeStore.primaryColor(this))));\n        searchView.setMaxWidth(getResources().getDisplayMetrics().widthPixels);\n        searchView.onActionViewCollapsed();\n        searchView.setOnCloseListener(() -> {\n            mTlIndicator.setVisibility(VISIBLE);\n            return false;\n        });\n        searchView.setOnSearchClickListener(view -> mTlIndicator.setVisibility(GONE));\n        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                return false;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                if (mTlIndicator.getSelectedTabPosition() == 1) {\n                    ((BookmarkFragment) mFragmentList.get(1)).startSearch(newText);\n                } else {\n                    ((ChapterListFragment) mFragmentList.get(0)).startSearch(newText);\n                }\n                return false;\n            }\n        });\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        if (item.getItemId() == android.R.id.home) {\n            onBackPressed();\n            return true;\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    public void onBackPressed() {\n        if (mTlIndicator.getVisibility() != VISIBLE) {\n            searchViewCollapsed();\n        }\n        finish();\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        setSupportActionBar(binding.toolbar);\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n        }\n    }\n\n    public BookShelfBean getBookShelf() {\n        return bookShelf;\n    }\n\n    public List<BookChapterBean> getChapterBeanList() {\n        return chapterBeanList;\n    }\n\n    public void searchViewCollapsed() {\n        searchView.onActionViewCollapsed();\n        mTlIndicator.setVisibility(VISIBLE);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ChoiceBookActivity.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.activity;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport android.view.LayoutInflater;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.databinding.ActivityBookChoiceBinding;\nimport com.kunfei.bookshelf.presenter.BookDetailPresenter;\nimport com.kunfei.bookshelf.presenter.ChoiceBookPresenter;\nimport com.kunfei.bookshelf.presenter.contract.ChoiceBookContract;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.ChoiceBookAdapter;\nimport com.kunfei.bookshelf.widget.recycler.refresh.OnLoadMoreListener;\n\nimport java.util.List;\n\npublic class ChoiceBookActivity extends MBaseActivity<ChoiceBookContract.Presenter> implements ChoiceBookContract.View {\n\n    private ActivityBookChoiceBinding binding;\n    private ChoiceBookAdapter searchBookAdapter;\n    private View viewRefreshError;\n\n    @Override\n    protected ChoiceBookContract.Presenter initInjector() {\n        return new ChoiceBookPresenter(getIntent());\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityBookChoiceBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n    }\n\n    @Override\n    protected void initData() {\n        searchBookAdapter = new ChoiceBookAdapter(this);\n    }\n\n    @SuppressLint(\"InflateParams\")\n    @Override\n    protected void bindView() {\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n\n        binding.rfRvSearchBooks.setRefreshRecyclerViewAdapter(searchBookAdapter, new LinearLayoutManager(this));\n\n        viewRefreshError = LayoutInflater.from(this).inflate(R.layout.view_refresh_error, null);\n        viewRefreshError.findViewById(R.id.tv_refresh_again).setOnClickListener(v -> {\n            searchBookAdapter.replaceAll(null);\n            //刷新失败 ，重试\n            mPresenter.initPage();\n            mPresenter.toSearchBooks(null);\n            startRefreshAnim();\n        });\n        binding.rfRvSearchBooks.setNoDataAndRefreshErrorView(LayoutInflater.from(this).inflate(R.layout.view_refresh_no_data, null),\n                viewRefreshError);\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(mPresenter.getTitle());\n        }\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected void bindEvent() {\n        searchBookAdapter.setCallback((animView, position, searchBookBean) -> {\n            String dataKey = String.valueOf(System.currentTimeMillis());\n            Intent intent = new Intent(ChoiceBookActivity.this, BookDetailActivity.class);\n            intent.putExtra(\"openFrom\", BookDetailPresenter.FROM_SEARCH);\n            intent.putExtra(\"data_key\", dataKey);\n            BitIntentDataManager.getInstance().putData(dataKey, searchBookBean);\n            startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out);\n        });\n\n        binding.rfRvSearchBooks.setBaseRefreshListener(() -> {\n            mPresenter.initPage();\n            mPresenter.toSearchBooks(null);\n            startRefreshAnim();\n        });\n        binding.rfRvSearchBooks.setLoadMoreListener(new OnLoadMoreListener() {\n            @Override\n            public void startLoadMore() {\n                mPresenter.toSearchBooks(null);\n            }\n\n            @Override\n            public void loadMoreErrorTryAgain() {\n                mPresenter.toSearchBooks(null);\n            }\n        });\n    }\n\n    @Override\n    public void refreshSearchBook(List<SearchBookBean> books) {\n        searchBookAdapter.replaceAll(books);\n    }\n\n    @Override\n    public void refreshFinish(Boolean isAll) {\n        binding.rfRvSearchBooks.finishRefresh(isAll, true);\n    }\n\n    @Override\n    public void loadMoreFinish(Boolean isAll) {\n        binding.rfRvSearchBooks.finishLoadMore(isAll, true);\n    }\n\n    @Override\n    public void loadMoreSearchBook(final List<SearchBookBean> books) {\n        if (books.size() <= 0) {\n            loadMoreFinish(true);\n            return;\n        }\n        searchBookAdapter.addAll(books);\n        loadMoreFinish(false);\n    }\n\n    @Override\n    public void searchBookError(String msg) {\n        if (mPresenter.getPage() > 1) {\n            binding.rfRvSearchBooks.finishLoadMore(true, true);\n        } else {\n            //刷新失败\n            binding.rfRvSearchBooks.refreshError();\n            if (msg != null) {\n                ((TextView) viewRefreshError.findViewById(R.id.tv_error_msg)).setText(msg);\n            } else {\n                ((TextView) viewRefreshError.findViewById(R.id.tv_error_msg)).setText(R.string.get_data_error);\n            }\n        }\n\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n\n    @Override\n    public void addBookShelfFailed(String massage) {\n        toast(massage, ERROR);\n    }\n\n    @Override\n    public void startRefreshAnim() {\n        binding.rfRvSearchBooks.startRefresh();\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/DonateActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.view.MenuItem;\nimport android.widget.Toast;\n\nimport androidx.appcompat.app.ActionBar;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.databinding.ActivityDonateBinding;\nimport com.kunfei.bookshelf.help.Donate;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n\n/**\n * Created by GKF on 2018/1/13.\n * 捐赠页面\n */\n\npublic class DonateActivity extends MBaseActivity<IPresenter> {\n\n    private ActivityDonateBinding binding;\n\n    public static void startThis(Context context) {\n        Intent intent = new Intent(context, DonateActivity.class);\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        context.startActivity(intent);\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityDonateBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void bindView() {\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n    }\n\n    @Override\n    protected void initData() {\n\n    }\n\n    @Override\n    protected void bindEvent() {\n        binding.vwZfbTz.setOnClickListener(view -> Donate.aliDonate(this));\n        binding.cvWxGzh.setOnClickListener(view -> {\n            ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);\n            ClipData clipData = ClipData.newPlainText(null, \"开源阅读\");\n            if (clipboard != null) {\n                clipboard.setPrimaryClip(clipData);\n                toast(R.string.copy_complete);\n            }\n        });\n        binding.vwZfbHb.setOnClickListener(view -> openActionViewIntent(\"https://gitee.com/gekunfei/Donate/raw/master/zfbhbrwm.png\"));\n        binding.vwZfbRwm.setOnClickListener(view -> openActionViewIntent(\"https://gitee.com/gekunfei/Donate/raw/master/zfbskrwm.jpg\"));\n        binding.vwWxRwm.setOnClickListener(view -> openActionViewIntent(\"https://gitee.com/gekunfei/Donate/raw/master/wxskrwm.jpg\"));\n        binding.vwQqRwm.setOnClickListener(view -> openActionViewIntent(\"https://gitee.com/gekunfei/Donate/raw/master/qqskrwm.jpg\"));\n        binding.vwZfbHbSsm.setOnClickListener(view -> getZfbHb(this));\n    }\n\n    public static void getZfbHb(Context context) {\n        ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);\n        ClipData clipData = ClipData.newPlainText(null, \"537954522\");\n        if (clipboard != null) {\n            clipboard.setPrimaryClip(clipData);\n            Toast.makeText(context, \"高级功能已开启\\n红包码已复制\\n支付宝首页搜索“537954522” 立即领红包\", Toast.LENGTH_LONG).show();\n        }\n        try {\n            PackageManager packageManager = context.getApplicationContext().getPackageManager();\n            Intent intent = packageManager.getLaunchIntentForPackage(\"com.eg.android.AlipayGphone\");\n            assert intent != null;\n            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n            context.startActivity(intent);\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            MApplication.getInstance().upDonateHb();\n        }\n    }\n\n    private void openActionViewIntent(String address) {\n        try {\n            Intent intent = new Intent(Intent.ACTION_VIEW);\n            intent.setData(Uri.parse(address));\n            startActivity(intent);\n        } catch (Exception e) {\n            e.printStackTrace();\n            Toast.makeText(this, R.string.can_not_open, Toast.LENGTH_SHORT).show();\n        }\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.donate);\n        }\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/DownloadActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport static com.kunfei.bookshelf.service.DownloadService.addDownloadAction;\nimport static com.kunfei.bookshelf.service.DownloadService.finishDownloadAction;\nimport static com.kunfei.bookshelf.service.DownloadService.obtainDownloadListAction;\nimport static com.kunfei.bookshelf.service.DownloadService.progressDownloadAction;\nimport static com.kunfei.bookshelf.service.DownloadService.removeDownloadAction;\n\nimport android.app.Activity;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuItem;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.bean.DownloadBookBean;\nimport com.kunfei.bookshelf.databinding.ActivityRecyclerVewBinding;\nimport com.kunfei.bookshelf.service.DownloadService;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.DownloadAdapter;\n\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayList;\n\npublic class DownloadActivity extends MBaseActivity<IPresenter> {\n\n    private ActivityRecyclerVewBinding binding;\n    private DownloadAdapter adapter;\n    private DownloadReceiver receiver;\n\n    public static void startThis(Activity activity) {\n        Intent intent = new Intent(activity, DownloadActivity.class);\n        activity.startActivity(intent);\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n\n    }\n\n    @Override\n    protected void onDestroy() {\n        if (receiver != null) {\n            unregisterReceiver(receiver);\n        }\n        super.onDestroy();\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    /**\n     * 布局载入  setContentView()\n     */\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityRecyclerVewBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n    }\n\n    /**\n     * 数据初始化\n     */\n    @Override\n    protected void initData() {\n        receiver = new DownloadReceiver(this);\n        IntentFilter filter = new IntentFilter();\n        filter.addAction(addDownloadAction);\n        filter.addAction(removeDownloadAction);\n        filter.addAction(progressDownloadAction);\n        filter.addAction(obtainDownloadListAction);\n        filter.addAction(finishDownloadAction);\n        registerReceiver(receiver, filter);\n    }\n\n    @Override\n    protected void bindView() {\n        initRecyclerView();\n    }\n\n    private void initRecyclerView() {\n        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));\n        adapter = new DownloadAdapter(this);\n        binding.recyclerView.setAdapter(adapter);\n        binding.recyclerView.setItemAnimator(null);\n\n        DownloadService.obtainDownloadList(this);\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.download_offline);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_book_download, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_cancel) {\n            DownloadService.cancelDownload(this);\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    private static class DownloadReceiver extends BroadcastReceiver {\n\n        WeakReference<DownloadActivity> ref;\n\n        public DownloadReceiver(DownloadActivity activity) {\n            this.ref = new WeakReference<>(activity);\n        }\n\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            DownloadAdapter adapter = ref.get().adapter;\n            if (adapter == null || intent == null) {\n                return;\n            }\n            String action = intent.getAction();\n            if (action != null) {\n                switch (action) {\n                    case addDownloadAction:\n                        DownloadBookBean downloadBook = intent.getParcelableExtra(\"downloadBook\");\n                        adapter.addData(downloadBook);\n                        break;\n                    case removeDownloadAction:\n                        downloadBook = intent.getParcelableExtra(\"downloadBook\");\n                        adapter.removeData(downloadBook);\n                        break;\n                    case progressDownloadAction:\n                        downloadBook = intent.getParcelableExtra(\"downloadBook\");\n                        adapter.upData(downloadBook);\n                        break;\n                    case finishDownloadAction:\n                        adapter.upDataS(null);\n                        break;\n                    case obtainDownloadListAction:\n                        ArrayList<DownloadBookBean> downloadBooks = intent.getParcelableArrayListExtra(\"downloadBooks\");\n                        adapter.upDataS(downloadBooks);\n                        break;\n\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ImportBookActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.view.MenuItem;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.fragment.app.Fragment;\nimport androidx.viewpager.widget.ViewPager;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.BaseTabActivity;\nimport com.kunfei.bookshelf.databinding.ActivityImportBookBinding;\nimport com.kunfei.bookshelf.presenter.ImportBookPresenter;\nimport com.kunfei.bookshelf.presenter.contract.ImportBookContract;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.fragment.BaseFileFragment;\nimport com.kunfei.bookshelf.view.fragment.FileCategoryFragment;\nimport com.kunfei.bookshelf.view.fragment.LocalBookFragment;\n\nimport java.io.File;\nimport java.util.Arrays;\nimport java.util.List;\n\n\n/**\n * 导入本地书籍\n */\npublic class ImportBookActivity extends BaseTabActivity<ImportBookContract.Presenter> implements ImportBookContract.View {\n    private static final String TAG = \"ImportBookActivity\";\n\n    private ActivityImportBookBinding binding;\n    private LocalBookFragment mLocalFragment;\n    private FileCategoryFragment mCategoryFragment;\n    private BaseFileFragment mCurFragment;\n\n    private BaseFileFragment.OnFileCheckedListener mListener = new BaseFileFragment.OnFileCheckedListener() {\n        @Override\n        public void onItemCheckedChange(boolean isChecked) {\n            changeMenuStatus();\n        }\n\n        @Override\n        public void onCategoryChanged() {\n            //状态归零\n            mCurFragment.setCheckedAll(false);\n            //改变菜单\n            changeMenuStatus();\n            //改变是否能够全选\n            changeCheckedAllStatus();\n        }\n    };\n\n\n    @Override\n    protected ImportBookContract.Presenter initInjector() {\n        return new ImportBookPresenter();\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityImportBookBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        setSupportActionBar(binding.toolbar);\n        setupActionBar();\n\n    }\n\n    @Override\n    protected void initData() {\n\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        mTlIndicator.setSelectedTabIndicatorColor(ThemeStore.accentColor(this));\n        mTlIndicator.setTabTextColors(getResources().getColor(R.color.tv_text_default), ThemeStore.accentColor(this));\n    }\n\n    @Override\n    protected List<Fragment> createTabFragments() {\n        mCategoryFragment = new FileCategoryFragment();\n        mLocalFragment = new LocalBookFragment();\n        return Arrays.asList(mCategoryFragment, mLocalFragment);\n    }\n\n    @Override\n    protected List<String> createTabTitles() {\n        return Arrays.asList(getString(R.string.files_tree), getString(R.string.intelligent_import));\n    }\n\n    @Override\n    protected void bindEvent() {\n        binding.fileSystemCbSelectedAll.setOnClickListener(\n                (view) -> {\n                    //设置全选状态\n                    boolean isChecked = binding.fileSystemCbSelectedAll.isChecked();\n                    mCurFragment.setCheckedAll(isChecked);\n                    //改变菜单状态\n                    changeMenuStatus();\n                }\n        );\n\n        mVp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {\n            @Override\n            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {\n\n            }\n\n            @Override\n            public void onPageSelected(int position) {\n                mCurFragment = (BaseFileFragment) mFragmentList.get(position);\n                //改变菜单状态\n                changeMenuStatus();\n            }\n\n            @Override\n            public void onPageScrollStateChanged(int state) {\n\n            }\n        });\n\n        binding.fileSystemBtnAddBook.setOnClickListener(\n                (v) -> {\n                    //获取选中的文件\n                    List<File> files = mCurFragment.getCheckedFiles();\n                    //转换成CollBook,并存储\n                    mPresenter.importBooks(files);\n                }\n        );\n\n        binding.fileSystemBtnDelete.setOnClickListener(\n                (v) -> {\n                    //弹出，确定删除文件吗。\n                    new AlertDialog.Builder(this)\n                            .setTitle(getString(R.string.del_file))\n                            .setMessage(getString(R.string.sure_del_file))\n                            .setPositiveButton(getResources().getString(R.string.ok), (dialog, which) -> {\n                                //删除选中的文件\n                                mCurFragment.deleteCheckedFiles();\n                                //提示删除文件成功\n                                toast(R.string.del_file_success);\n                            })\n                            .setNegativeButton(getResources().getString(R.string.cancel), null)\n                            .show();\n                }\n        );\n        mCategoryFragment.setOnFileCheckedListener(mListener);\n        mLocalFragment.setOnFileCheckedListener(mListener);\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        mCurFragment = (BaseFileFragment) mFragmentList.get(0);\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.book_local);\n        }\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    /**\n     * 改变底部选择栏的状态\n     */\n    private void changeMenuStatus() {\n\n        //点击、删除状态的设置\n        if (mCurFragment.getCheckedCount() == 0) {\n            binding.fileSystemBtnAddBook.setText(getString(R.string.nb_file_add_shelf));\n            //设置某些按钮的是否可点击\n            setMenuClickable(false);\n\n            if (binding.fileSystemCbSelectedAll.isChecked()) {\n                mCurFragment.setChecked(false);\n                binding.fileSystemCbSelectedAll.setChecked(mCurFragment.isCheckedAll());\n            }\n\n        } else {\n            binding.fileSystemBtnAddBook.setText(getString(R.string.nb_file_add_shelves, mCurFragment.getCheckedCount()));\n            setMenuClickable(true);\n\n            //全选状态的设置\n\n            //如果选中的全部的数据，则判断为全选\n            if (mCurFragment.getCheckedCount() == mCurFragment.getCheckableCount()) {\n                //设置为全选\n                mCurFragment.setChecked(true);\n                binding.fileSystemCbSelectedAll.setChecked(mCurFragment.isCheckedAll());\n            }\n            //如果曾今是全选则替换\n            else if (mCurFragment.isCheckedAll()) {\n                mCurFragment.setChecked(false);\n                binding.fileSystemCbSelectedAll.setChecked(mCurFragment.isCheckedAll());\n            }\n        }\n\n        //重置全选的文字\n        if (mCurFragment.isCheckedAll()) {\n            binding.fileSystemCbSelectedAll.setText(R.string.cancel);\n        } else {\n            binding.fileSystemCbSelectedAll.setText(getString(R.string.select_all));\n        }\n\n    }\n\n    private void setMenuClickable(boolean isClickable) {\n\n        //设置是否可删除\n        binding.fileSystemBtnDelete.setEnabled(isClickable);\n        binding.fileSystemBtnDelete.setClickable(isClickable);\n\n        //设置是否可添加书籍\n        binding.fileSystemBtnAddBook.setEnabled(isClickable);\n        binding.fileSystemBtnAddBook.setClickable(isClickable);\n    }\n\n    /**\n     * 改变全选按钮的状态\n     */\n    private void changeCheckedAllStatus() {\n        //获取可选择的文件数量\n        int count = mCurFragment.getCheckableCount();\n\n        //设置是否能够全选\n        if (count > 0) {\n            binding.fileSystemCbSelectedAll.setClickable(true);\n            binding.fileSystemCbSelectedAll.setEnabled(true);\n        } else {\n            binding.fileSystemCbSelectedAll.setClickable(false);\n            binding.fileSystemCbSelectedAll.setEnabled(false);\n        }\n    }\n\n    @Override\n    public void addSuccess() {\n        //设置HashMap为false\n        mCurFragment.setCheckedAll(false);\n        //改变菜单状态\n        changeMenuStatus();\n        //改变是否可以全选\n        changeCheckedAllStatus();\n    }\n\n    @Override\n    public void addError(String msg) {\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/MainActivity.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.activity;\n\nimport static com.kunfei.bookshelf.utils.NetworkUtils.isNetWorkAvailable;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.content.res.Configuration;\nimport android.graphics.PorterDuff;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.PopupMenu;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.ActionBarDrawerToggle;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.widget.AppCompatImageView;\nimport androidx.core.view.GravityCompat;\nimport androidx.fragment.app.Fragment;\nimport androidx.viewpager.widget.ViewPager;\n\nimport com.google.android.material.appbar.AppBarLayout;\nimport com.google.android.material.tabs.TabLayout;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.bookshelf.BuildConfig;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.BaseTabActivity;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.ActivityMainBinding;\nimport com.kunfei.bookshelf.help.FileHelp;\nimport com.kunfei.bookshelf.help.ProcessTextHelp;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.help.storage.BackupRestoreUi;\nimport com.kunfei.bookshelf.model.UpLastChapterModel;\nimport com.kunfei.bookshelf.presenter.BookSourcePresenter;\nimport com.kunfei.bookshelf.presenter.MainPresenter;\nimport com.kunfei.bookshelf.presenter.contract.MainContract;\nimport com.kunfei.bookshelf.service.WebService;\nimport com.kunfei.bookshelf.utils.ACache;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.theme.NavigationViewUtil;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.fragment.BookListFragment;\nimport com.kunfei.bookshelf.view.fragment.FindBookFragment;\nimport com.kunfei.bookshelf.widget.modialog.InputDialog;\nimport com.kunfei.bookshelf.widget.modialog.MoDialogHUD;\n\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Objects;\n\nimport kotlin.Unit;\n\npublic class MainActivity extends BaseTabActivity<MainContract.Presenter> implements MainContract.View,\n        BookListFragment.CallbackValue {\n    private final int requestSource = 14;\n    private String[] mTitles;\n    private final int REQUEST_QR = 202;\n\n    private ActivityMainBinding binding;\n    private AppCompatImageView vwNightTheme;\n    private int group;\n    private ActionBarDrawerToggle mDrawerToggle;\n    private MoDialogHUD moDialogHUD;\n    private long exitTime = 0;\n    private boolean resumed = false;\n    private final Handler handler = new Handler(Looper.getMainLooper());\n\n    @Override\n    protected MainContract.Presenter initInjector() {\n        return new MainPresenter();\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            resumed = savedInstanceState.getBoolean(\"resumed\");\n        }\n        group = preferences.getInt(\"bookshelfGroup\", 0);\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putBoolean(\"resumed\", resumed);\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityMainBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n\n        String shared_url = preferences.getString(\"shared_url\", \"\");\n        if (shared_url.length() > 1) {\n            InputDialog.builder(this)\n                    .setTitle(getString(R.string.add_book_url))\n                    .setDefaultValue(shared_url)\n                    .setCallback(new InputDialog.Callback() {\n                        @Override\n                        public void setInputText(String inputText) {\n                            inputText = StringUtils.trim(inputText);\n                            mPresenter.addBookUrl(inputText);\n                        }\n\n                        @Override\n                        public void delete(String value) {\n\n                        }\n                    }).show();\n            preferences.edit()\n                    .putString(\"shared_url\", \"\")\n                    .apply();\n        }\n    }\n\n\n    /**\n     * 沉浸状态栏\n     */\n    @Override\n    public void initImmersionBar() {\n        super.initImmersionBar();\n    }\n\n    @Override\n    protected void initData() {\n        mTitles = new String[]{getString(R.string.bookshelf), getString(R.string.find)};\n    }\n\n    @Override\n    public boolean isRecreate() {\n        return isRecreate;\n    }\n\n    @Override\n    public int getGroup() {\n        return group;\n    }\n\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent ev) {\n        return super.dispatchTouchEvent(ev);\n    }\n\n    @Override\n    protected List<Fragment> createTabFragments() {\n        BookListFragment bookListFragment = null;\n        FindBookFragment findBookFragment = null;\n        for (Fragment fragment : getSupportFragmentManager().getFragments()) {\n            if (fragment instanceof BookListFragment) {\n                bookListFragment = (BookListFragment) fragment;\n            } else if (fragment instanceof FindBookFragment) {\n                findBookFragment = (FindBookFragment) fragment;\n            }\n        }\n        if (bookListFragment == null)\n            bookListFragment = new BookListFragment();\n        if (findBookFragment == null)\n            findBookFragment = new FindBookFragment();\n        return Arrays.asList(bookListFragment, findBookFragment);\n    }\n\n    @Override\n    protected List<String> createTabTitles() {\n        return Arrays.asList(mTitles);\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        setSupportActionBar(binding.mainView.toolbar);\n        setupActionBar();\n        binding.mainView.cardSearch.setCardBackgroundColor(ThemeStore.primaryColorDark(this));\n        initDrawer();\n        initTabLayout();\n        upGroup(group);\n        moDialogHUD = new MoDialogHUD(this);\n        if (!preferences.getBoolean(\"behaviorMain\", true)) {\n            AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) binding.mainView.toolbar.getLayoutParams();\n            params.setScrollFlags(0);\n        }\n        //点击跳转搜索页\n        binding.mainView.cardSearch.setOnClickListener(view -> startActivityByAnim(new Intent(this, SearchBookActivity.class),\n                binding.mainView.toolbar, \"sharedView\", android.R.anim.fade_in, android.R.anim.fade_out));\n    }\n\n    //初始化TabLayout和ViewPager\n    private void initTabLayout() {\n        mTlIndicator.setBackgroundColor(ThemeStore.backgroundColor(this));\n        mTlIndicator.setSelectedTabIndicatorColor(ThemeStore.accentColor(this));\n        //TabLayout使用自定义Item\n        for (int i = 0; i < mTlIndicator.getTabCount(); i++) {\n            TabLayout.Tab tab = mTlIndicator.getTabAt(i);\n            if (tab == null) return;\n            tab.setCustomView(tab_icon(mTitles[i]));\n            View customView = tab.getCustomView();\n            if (customView == null) return;\n            TextView tv = customView.findViewById(R.id.tabtext);\n            tab.setContentDescription(String.format(\"%s,%s\", tv.getText(), getString(R.string.click_on_selected_show_menu)));\n            ImageView im = customView.findViewById(R.id.tabicon);\n            if (tab.isSelected()) {\n                im.setVisibility(View.VISIBLE);\n            } else {\n                im.setVisibility(View.GONE);\n            }\n        }\n        mTlIndicator.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {\n            @Override\n            public void onTabSelected(TabLayout.Tab tab) {\n                View customView = tab.getCustomView();\n                if (customView == null) return;\n                ImageView im = customView.findViewById(R.id.tabicon);\n                im.setVisibility(View.VISIBLE);\n            }\n\n            @Override\n            public void onTabUnselected(TabLayout.Tab tab) {\n                View customView = tab.getCustomView();\n                if (customView == null) return;\n                ImageView im = customView.findViewById(R.id.tabicon);\n                im.setVisibility(View.GONE);\n            }\n\n            @Override\n            public void onTabReselected(TabLayout.Tab tab) {\n                View tabView = (View) Objects.requireNonNull(tab.getCustomView()).getParent();\n                if (tab.getPosition() == 0) {\n                    showBookGroupMenu(tabView);\n                } else {\n                    showFindMenu(tabView);\n                }\n            }\n        });\n    }\n\n    /**\n     * 显示分组菜单\n     */\n    private void showBookGroupMenu(View view) {\n        PopupMenu popupMenu = new PopupMenu(this, view);\n        for (int j = 0; j < getResources().getStringArray(R.array.book_group_array).length; j++) {\n            popupMenu.getMenu().add(0, 0, j, getResources().getStringArray(R.array.book_group_array)[j]);\n        }\n        popupMenu.setOnMenuItemClickListener(menuItem -> {\n            upGroup(menuItem.getOrder());\n            return true;\n        });\n        popupMenu.setOnDismissListener(popupMenu1 -> updateTabItemIcon(0, false));\n        popupMenu.show();\n        updateTabItemIcon(0, true);\n    }\n\n    /**\n     * 显示发现菜单\n     */\n    private void showFindMenu(View view) {\n        PopupMenu popupMenu = new PopupMenu(this, view);\n        popupMenu.getMenu().add(0, 0, 0, getString(R.string.switch_display_style));\n        popupMenu.getMenu().add(0, 0, 1, getString(R.string.clear_find_cache));\n        boolean findTypeIsFlexBox = preferences.getBoolean(\"findTypeIsFlexBox\", true);\n        boolean showFindLeftView = preferences.getBoolean(\"showFindLeftView\", true);\n        if (findTypeIsFlexBox) {\n            popupMenu.getMenu().add(0, 0, 2, showFindLeftView ? \"隐藏左侧栏\" : \"显示左侧栏\");\n        }\n        popupMenu.setOnMenuItemClickListener(menuItem -> {\n            FindBookFragment findBookFragment = getFindFragment();\n            switch (menuItem.getOrder()) {\n                case 0:\n                    preferences.edit()\n                            .putBoolean(\"findTypeIsFlexBox\", !findTypeIsFlexBox)\n                            .apply();\n                    if (findBookFragment != null) {\n                        findBookFragment.upStyle();\n                    }\n                    break;\n                case 1:\n                    ACache.get(this, \"findCache\").clear();\n                    if (findBookFragment != null) {\n                        findBookFragment.refreshData();\n                    }\n                    break;\n                case 2:\n                    preferences.edit()\n                            .putBoolean(\"showFindLeftView\", !showFindLeftView)\n                            .apply();\n                    if (findBookFragment != null) {\n                        findBookFragment.upUI();\n                    }\n                    break;\n            }\n            return true;\n        });\n        popupMenu.setOnDismissListener(popupMenu1 -> updateTabItemIcon(1, false));\n        popupMenu.show();\n        updateTabItemIcon(1, true);\n    }\n\n    /**\n     * 更新Tab图标\n     */\n    private void updateTabItemIcon(int index, boolean showMenu) {\n        TabLayout.Tab tab = mTlIndicator.getTabAt(index);\n        if (tab == null) return;\n        View customView = tab.getCustomView();\n        if (customView == null) return;\n        ImageView im = customView.findViewById(R.id.tabicon);\n        if (showMenu) {\n            im.setImageResource(R.drawable.ic_arrow_drop_up);\n        } else {\n            im.setImageResource(R.drawable.ic_arrow_drop_down);\n        }\n    }\n\n    /**\n     * 更新Tab文字\n     */\n    private void updateTabItemText(int group) {\n        TabLayout.Tab tab = mTlIndicator.getTabAt(0);\n        if (tab == null) return;\n        View customView = tab.getCustomView();\n        if (customView == null) return;\n        TextView tv = customView.findViewById(R.id.tabtext);\n        tv.setText(getResources().getStringArray(R.array.book_group_array)[group]);\n        tab.setContentDescription(String.format(\"%s,%s\", tv.getText(), getString(R.string.click_on_selected_show_menu)));\n    }\n\n    private View tab_icon(String name) {\n        @SuppressLint(\"InflateParams\")\n        View tabView = LayoutInflater.from(this).inflate(R.layout.tab_view_icon_right, null);\n        TextView tv = tabView.findViewById(R.id.tabtext);\n        tv.setText(name);\n        ImageView im = tabView.findViewById(R.id.tabicon);\n        im.setVisibility(View.VISIBLE);\n        im.setImageResource(R.drawable.ic_arrow_drop_down);\n        return tabView;\n    }\n\n    public ViewPager getViewPager() {\n        return mVp;\n    }\n\n    public BookListFragment getBookListFragment() {\n        try {\n            return (BookListFragment) mFragmentList.get(0);\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    public FindBookFragment getFindFragment() {\n        try {\n            return (FindBookFragment) mFragmentList.get(1);\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    @Override\n    protected void onPostCreate(Bundle savedInstanceState) {\n        super.onPostCreate(savedInstanceState);\n        // 这个必须要，没有的话进去的默认是个箭头。。正常应该是三横杠的\n        mDrawerToggle.syncState();\n        if (vwNightTheme != null) {\n            upThemeVw();\n        }\n    }\n\n    @Override\n    public boolean onPrepareOptionsMenu(Menu menu) {\n        return super.onPrepareOptionsMenu(menu);\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_main_activity, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    /**\n     * 菜单事件\n     */\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_add_local) {\n            new PermissionsCompat.Builder(this)\n                    .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                    .rationale(R.string.import_per)\n                    .onGranted((requestCode) -> {\n                        startActivity(new Intent(MainActivity.this, ImportBookActivity.class));\n                        return Unit.INSTANCE;\n                    })\n                    .request();\n        } else if (id == R.id.action_add_url) {\n            InputDialog.builder(this)\n                    .setTitle(getString(R.string.add_book_url))\n                    .setCallback(new InputDialog.Callback() {\n                        @Override\n                        public void setInputText(String inputText) {\n                            inputText = StringUtils.trim(inputText);\n                            mPresenter.addBookUrl(inputText);\n                        }\n\n                        @Override\n                        public void delete(String value) {\n\n                        }\n                    }).show();\n        } else if (id == R.id.action_add_qrcode) {\n            Intent intent = new Intent(this, QRCodeScanActivity.class);\n            //noinspection deprecation\n            startActivityForResult(intent, REQUEST_QR);\n        } else if (id == R.id.action_download_all) {\n            if (!isNetWorkAvailable()) {\n                toast(R.string.network_connection_unavailable);\n            } else {\n                RxBus.get().post(RxBusTag.DOWNLOAD_ALL, 10000);\n            }\n        } else if (id == R.id.menu_bookshelf_layout) {\n            selectBookshelfLayout();\n        } else if (id == R.id.action_arrange_bookshelf) {\n            if (getBookListFragment() != null) {\n                getBookListFragment().setArrange(true);\n            }\n        } else if (id == R.id.action_web_start) {\n            boolean startedThisTime = WebService.startThis(this);\n            if (!startedThisTime) {\n                toast(getString(R.string.web_service_already_started_hint));\n            }\n        } else if (id == android.R.id.home) {\n            if (binding.drawer.isDrawerOpen(GravityCompat.START)) {\n                binding.drawer.closeDrawers();\n            } else {\n                binding.drawer.openDrawer(GravityCompat.START, !MApplication.isEInkMode);\n            }\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n        }\n    }\n\n    //初始化侧边栏\n    private void initDrawer() {\n        mDrawerToggle = new ActionBarDrawerToggle(this, binding.drawer, R.string.navigation_drawer_open, R.string.navigation_drawer_close);\n        mDrawerToggle.syncState();\n        binding.drawer.addDrawerListener(mDrawerToggle);\n\n        setUpNavigationView();\n    }\n\n    @Override\n    public void onConfigurationChanged(Configuration newConfig) {\n        super.onConfigurationChanged(newConfig);\n        mDrawerToggle.onConfigurationChanged(newConfig);\n\n    }\n\n    private void upGroup(int group) {\n        if (this.group != group) {\n            SharedPreferences.Editor editor = preferences.edit();\n            editor.putInt(\"bookshelfGroup\", group);\n            editor.apply();\n        }\n        this.group = group;\n        RxBus.get().post(RxBusTag.UPDATE_GROUP, group);\n        RxBus.get().post(RxBusTag.REFRESH_BOOK_LIST, false);\n        //更换Tab文字\n        updateTabItemText(group);\n\n    }\n\n    /**\n     * 侧边栏按钮\n     */\n    private void setUpNavigationView() {\n        binding.navigationView.setBackgroundColor(ThemeStore.backgroundColor(this));\n        NavigationViewUtil.setItemIconColors(binding.navigationView, getResources().getColor(R.color.tv_text_default), ThemeStore.accentColor(this));\n        NavigationViewUtil.disableScrollbar(binding.navigationView);\n        @SuppressLint(\"InflateParams\") View headerView = LayoutInflater.from(this).inflate(R.layout.navigation_header, null);\n        AppCompatImageView imageView = headerView.findViewById(R.id.iv_read);\n        imageView.setColorFilter(ThemeStore.accentColor(this));\n        binding.navigationView.addHeaderView(headerView);\n        Menu drawerMenu = binding.navigationView.getMenu();\n        vwNightTheme = drawerMenu.findItem(R.id.action_theme).getActionView().findViewById(R.id.iv_theme_day_night);\n        upThemeVw();\n        vwNightTheme.setOnClickListener(view -> setNightTheme(!isNightTheme()));\n        binding.navigationView.setNavigationItemSelectedListener(menuItem -> {\n            binding.drawer.closeDrawer(GravityCompat.START, !MApplication.isEInkMode);\n            int itemId = menuItem.getItemId();\n            if (itemId == R.id.action_book_source_manage) {\n                handler.postDelayed(() -> BookSourceActivity.startThis(this, requestSource), 200);\n            } else if (itemId == R.id.action_replace_rule) {\n                handler.postDelayed(() -> ReplaceRuleActivity.startThis(this, null), 200);\n            } else if (itemId == R.id.action_download) {\n                handler.postDelayed(() -> DownloadActivity.startThis(this), 200);\n            } else if (itemId == R.id.action_setting) {\n                handler.postDelayed(() -> SettingActivity.startThis(this), 200);\n            } else if (itemId == R.id.action_about) {\n                handler.postDelayed(() -> AboutActivity.startThis(this), 200);\n            } else if (itemId == R.id.action_donate) {\n                handler.postDelayed(() -> DonateActivity.startThis(this), 200);\n            } else if (itemId == R.id.action_backup) {\n                handler.postDelayed(() -> BackupRestoreUi.INSTANCE.backup(this), 200);\n            } else if (itemId == R.id.action_restore) {\n                handler.postDelayed(() -> BackupRestoreUi.INSTANCE.restore(this), 200);\n            } else if (itemId == R.id.action_theme) {\n                handler.postDelayed(() -> ThemeSettingActivity.startThis(this), 200);\n            }\n            return true;\n        });\n    }\n\n    /**\n     * 更新主题切换按钮\n     */\n    private void upThemeVw() {\n        if (isNightTheme()) {\n            vwNightTheme.setImageResource(R.drawable.ic_daytime);\n            vwNightTheme.setContentDescription(getString(R.string.click_to_day));\n        } else {\n            vwNightTheme.setImageResource(R.drawable.ic_brightness);\n            vwNightTheme.setContentDescription(getString(R.string.click_to_night));\n        }\n        vwNightTheme.getDrawable().mutate().setColorFilter(ThemeStore.accentColor(this), PorterDuff.Mode.SRC_ATOP);\n    }\n\n    private void selectBookshelfLayout() {\n        new AlertDialog.Builder(this)\n                .setTitle(\"选择书架布局\")\n                .setItems(R.array.bookshelf_layout, (dialog, which) -> {\n                    preferences.edit().putInt(\"bookshelfLayout\", which).apply();\n                    recreate();\n                }).show();\n    }\n\n    /**\n     * 新版本运行\n     */\n    private void versionUpRun() {\n        if (preferences.getInt(\"versionCode\", 0) != MApplication.getVersionCode()) {\n            //保存版本号\n            preferences.edit()\n                    .putInt(\"versionCode\", MApplication.getVersionCode())\n                    .apply();\n            //更新日志\n            moDialogHUD.showAssetMarkdown(\"updateLog.md\");\n        }\n    }\n\n    @Override\n    protected void firstRequest() {\n        if (!isRecreate) {\n            versionUpRun();\n        }\n        if (!Objects.equals(MApplication.downloadPath, FileHelp.getFilesPath())) {\n            new PermissionsCompat.Builder(this)\n                    .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                    .rationale(R.string.get_storage_per)\n                    .request();\n        }\n        handler.postDelayed(() -> {\n            UpLastChapterModel.getInstance().startUpdate();\n            if (BuildConfig.DEBUG) {\n                ProcessTextHelp.setProcessTextEnable(false);\n            }\n        }, 60 * 1000);\n    }\n\n    @Override\n    public void dismissHUD() {\n        moDialogHUD.dismiss();\n    }\n\n    public void onRestore(String msg) {\n        moDialogHUD.showLoading(msg);\n    }\n\n    @SuppressLint(\"RtlHardcoded\")\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        Boolean mo = moDialogHUD.onKeyDown(keyCode, event);\n        if (mo) {\n            return true;\n        } else if (mTlIndicator.getSelectedTabPosition() != 0) {\n            Objects.requireNonNull(mTlIndicator.getTabAt(0)).select();\n            return true;\n        } else {\n            if (keyCode == KeyEvent.KEYCODE_BACK) {\n                if (binding.drawer.isDrawerOpen(GravityCompat.START)) {\n                    binding.drawer.closeDrawer(GravityCompat.START, !MApplication.isEInkMode);\n                    return true;\n                }\n                exit();\n                return true;\n            }\n            return super.onKeyDown(keyCode, event);\n        }\n    }\n\n    /**\n     * 退出\n     */\n    public void exit() {\n        if ((System.currentTimeMillis() - exitTime) > 2000) {\n            showSnackBar(binding.mainView.toolbar, getString(R.string.double_click_exit));\n            exitTime = System.currentTimeMillis();\n        } else {\n            finish();\n        }\n    }\n\n    @Override\n    public void recreate() {\n        super.recreate();\n    }\n\n    @Override\n    protected void onDestroy() {\n        UpLastChapterModel.destroy();\n        DbHelper.getDaoSession().getBookContentBeanDao().deleteAll();\n        super.onDestroy();\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        BackupRestoreUi.INSTANCE.onActivityResult(requestCode, resultCode, data);\n        switch (requestCode) {\n            case requestSource:\n                if (resultCode == RESULT_OK) {\n                    FindBookFragment findBookFragment = getFindFragment();\n                    if (findBookFragment != null) {\n                        findBookFragment.refreshData();\n                    }\n                }\n                break;\n            case REQUEST_QR:\n                if (resultCode == RESULT_OK) {\n                    String result = data.getStringExtra(\"result\");\n                    if (!StringUtils.isTrimEmpty(result)) {\n                        result=result.trim();\n                        // 如果只有书源,则导入书源\n                        if(result.replaceAll(\"(\\\\s|\\n)*\",\"\").matches(\"^\\\\{.*$\")) {\n                            new BookSourcePresenter().importBookSource(result);\n                            break;\n                        }\n//                        String[] string=result.split(\"#\",2);\n//                        mPresenter.addBookUrl(string[0]);\n                        mPresenter.addBookUrl(result);\n                    }\n                }\n                break;\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/QRCodeScanActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\n\nimport androidx.appcompat.app.ActionBar;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.databinding.ActivityQrcodeCaptureBinding;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.utils.RealPathUtil;\nimport com.kunfei.bookshelf.widget.filepicker.picker.FilePicker;\n\nimport cn.bingoogolapple.qrcode.core.QRCodeView;\nimport kotlin.Unit;\n\n/**\n * Created by GKF on 2018/1/29.\n */\n\npublic class QRCodeScanActivity extends MBaseActivity<IPresenter> implements QRCodeView.Delegate {\n\n    private ActivityQrcodeCaptureBinding binding;\n    private final int REQUEST_QR_IMAGE = 202;\n    private boolean flashlightIsOpen;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    /**\n     * 布局载入  setContentView()\n     */\n    @Override\n    protected void onCreateActivity() {\n        binding = ActivityQrcodeCaptureBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n    }\n\n    /**\n     * 数据初始化\n     */\n    @Override\n    protected void initData() {\n        binding.zxingview.setDelegate(this);\n        binding.fabFlashlight.setOnClickListener(view -> {\n            if (flashlightIsOpen) {\n                flashlightIsOpen = false;\n                binding.zxingview.closeFlashlight();\n            } else {\n                flashlightIsOpen = true;\n                binding.zxingview.openFlashlight();\n            }\n        });\n    }\n\n    @Override\n    protected void onStart() {\n        super.onStart();\n        startCamera();\n    }\n\n    private void startCamera() {\n        new PermissionsCompat.Builder(this)\n                .addPermissions(Permissions.CAMERA)\n                .rationale(R.string.qr_per)\n                .onGranted((requestCode) -> {\n                    binding.zxingview.setVisibility(View.VISIBLE);\n                    binding.zxingview.startSpotAndShowRect(); // 显示扫描框，并开始识别\n                    return Unit.INSTANCE;\n                })\n                .request();\n    }\n\n    @Override\n    protected void onStop() {\n        binding.zxingview.stopCamera(); // 关闭摄像头预览，并且隐藏扫描框\n        super.onStop();\n    }\n\n    @Override\n    protected void onDestroy() {\n        binding.zxingview.onDestroy(); // 销毁二维码扫描控件\n        super.onDestroy();\n    }\n\n    @Override\n    public void onScanQRCodeSuccess(String result) {\n        Intent intent = new Intent();\n        intent.putExtra(\"result\", result);\n        setResult(RESULT_OK, intent);\n        finish();\n    }\n\n    @Override\n    public void onCameraAmbientBrightnessChanged(boolean isDark) {\n\n    }\n\n    @Override\n    public void onScanQRCodeOpenCameraError() {\n\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.scan_qr_code);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_qr_code_scan, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_choose_from_gallery) {\n            new PermissionsCompat.Builder(this)\n                    .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                    .rationale(R.string.get_storage_per)\n                    .onGranted((requestCode) -> {\n                        chooseFromGallery();\n                        return Unit.INSTANCE;\n                    })\n                    .request();\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n\n        binding.zxingview.startSpotAndShowRect(); // 显示扫描框，并开始识别\n\n        if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_QR_IMAGE) {\n            final String picturePath = RealPathUtil.getPath(this, data.getData());\n            // 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法，走通用的回调\n            binding.zxingview.decodeQRCode(picturePath);\n        }\n    }\n\n    private void chooseFromGallery() {\n        try {\n            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);\n            intent.addCategory(Intent.CATEGORY_OPENABLE);\n            intent.setType(\"image/*\");\n            startActivityForResult(intent, REQUEST_QR_IMAGE);\n        } catch (Exception ignored) {\n            FilePicker picker = new FilePicker(this, FilePicker.FILE);\n            picker.setBackgroundColor(getResources().getColor(R.color.background));\n            picker.setTopBackgroundColor(getResources().getColor(R.color.background));\n            picker.setItemHeight(30);\n            picker.setOnFilePickListener(currentPath -> binding.zxingview.decodeQRCode(currentPath));\n            picker.show();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ReadBookActivity.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.activity;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.SCRIPT_ENGINE;\n\nimport android.annotation.SuppressLint;\nimport android.content.BroadcastReceiver;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.content.res.Configuration;\nimport android.graphics.Color;\nimport android.graphics.PorterDuff;\nimport android.net.Uri;\nimport android.os.BatteryManager;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.text.TextUtils;\nimport android.view.Gravity;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.WindowManager;\nimport android.view.animation.Animation;\nimport android.view.animation.AnimationUtils;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.AlertDialog;\n\nimport com.kunfei.basemvplib.AppActivityManager;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.BookmarkBean;\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\nimport com.kunfei.bookshelf.bean.TxtChapterRuleBean;\nimport com.kunfei.bookshelf.dao.TxtChapterRuleBeanDao;\nimport com.kunfei.bookshelf.databinding.ActivityBookReadBinding;\nimport com.kunfei.bookshelf.help.ChapterContentHelp;\nimport com.kunfei.bookshelf.help.ReadBookControl;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.help.storage.Backup;\nimport com.kunfei.bookshelf.model.ReplaceRuleManager;\nimport com.kunfei.bookshelf.model.TxtChapterRuleManager;\nimport com.kunfei.bookshelf.model.analyzeRule.AnalyzeUrl;\nimport com.kunfei.bookshelf.presenter.ReadBookPresenter;\nimport com.kunfei.bookshelf.presenter.contract.ReadBookContract;\nimport com.kunfei.bookshelf.service.ReadAloudService;\nimport com.kunfei.bookshelf.utils.ActivityExtensionsKt;\nimport com.kunfei.bookshelf.utils.BatteryUtil;\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.SystemUtil;\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.dialog.SourceLoginDialog;\nimport com.kunfei.bookshelf.view.popupwindow.CheckAddShelfPop;\nimport com.kunfei.bookshelf.view.popupwindow.MoreSettingPop;\nimport com.kunfei.bookshelf.view.popupwindow.ReadAdjustMarginPop;\nimport com.kunfei.bookshelf.view.popupwindow.ReadAdjustPop;\nimport com.kunfei.bookshelf.view.popupwindow.ReadBottomMenu;\nimport com.kunfei.bookshelf.view.popupwindow.ReadInterfacePop;\nimport com.kunfei.bookshelf.view.popupwindow.ReadLongPressPop;\nimport com.kunfei.bookshelf.widget.modialog.BookmarkDialog;\nimport com.kunfei.bookshelf.widget.modialog.ChangeSourceDialog;\nimport com.kunfei.bookshelf.widget.modialog.DownLoadDialog;\nimport com.kunfei.bookshelf.widget.modialog.InputDialog;\nimport com.kunfei.bookshelf.widget.modialog.MoDialogHUD;\nimport com.kunfei.bookshelf.widget.modialog.ReplaceRuleDialog;\nimport com.kunfei.bookshelf.widget.page.PageLoader;\nimport com.kunfei.bookshelf.widget.page.PageLoaderNet;\nimport com.kunfei.bookshelf.widget.page.PageView;\nimport com.kunfei.bookshelf.widget.page.TxtChapter;\nimport com.kunfei.bookshelf.widget.page.animation.PageAnimation;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.regex.Pattern;\n\nimport javax.script.SimpleBindings;\n\nimport kotlin.Unit;\n\n\n/**\n * 阅读界面\n */\npublic class ReadBookActivity extends MBaseActivity<ReadBookContract.Presenter> implements ReadBookContract.View, View.OnTouchListener {\n\n    private final int payActivityRequest = 1234;\n    public final int fontDirRequest = 24345;\n    private ActivityBookReadBinding binding;\n    private Animation menuTopIn;\n    private Animation menuTopOut;\n    private Animation menuBottomIn;\n    private Animation menuBottomOut;\n    private ActionBar actionBar;\n    private PageLoader mPageLoader;\n    private final Handler mHandler = new Handler();\n    private Runnable autoPageRunnable;\n    private Runnable keepScreenRunnable;\n    private Runnable upHpbNextPage;\n    private int nextPageTime;\n    private String noteUrl;\n    private Boolean isAdd = false; //判断是否已经添加进书架\n    private ReadAloudService.Status aloudStatus = ReadAloudService.Status.STOP;\n    private int screenTimeOut;\n    private final int upHpbInterval = 100;\n    private Menu menu;\n    private CheckAddShelfPop checkAddShelfPop;\n    private MoDialogHUD moDialogHUD;\n    private ThisBatInfoReceiver batInfoReceiver;\n    private final ReadBookControl readBookControl = ReadBookControl.getInstance();\n\n    private boolean autoPage = false;\n    private boolean aloudNextPage;\n    private int lastX, lastY;\n\n    @Override\n    protected ReadBookContract.Presenter initInjector() {\n        return new ReadBookPresenter();\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            noteUrl = savedInstanceState.getString(\"noteUrl\");\n            isAdd = savedInstanceState.getBoolean(\"isAdd\");\n        }\n        readBookControl.initTextDrawableIndex();\n        super.onCreate(savedInstanceState);\n        screenTimeOut = getResources().getIntArray(R.array.screen_time_out_value)[readBookControl.getScreenTimeOut()];\n        keepScreenRunnable = this::unKeepScreenOn;\n        autoPageRunnable = this::nextPage;\n        upHpbNextPage = this::upHpbNextPage;\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        setOrientation(readBookControl.getScreenDirection());\n        binding = ActivityBookReadBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && readBookControl.getToLh()) {\n            if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {\n                WindowManager.LayoutParams lp = getWindow().getAttributes();\n                lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;\n                getWindow().setAttributes(lp);\n            }\n        }\n    }\n\n    @Override\n    protected void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        if (mPresenter.getBookShelf() != null) {\n            outState.putString(\"noteUrl\", mPresenter.getBookShelf().getNoteUrl());\n            outState.putBoolean(\"isAdd\", isAdd);\n            String key = String.valueOf(System.currentTimeMillis());\n            String bookKey = \"book\" + key;\n            getIntent().putExtra(\"bookKey\", bookKey);\n            BitIntentDataManager.getInstance().putData(bookKey, mPresenter.getBookShelf().clone());\n            String chapterListKey = \"chapterList\" + key;\n            getIntent().putExtra(\"chapterListKey\", chapterListKey);\n            BitIntentDataManager.getInstance().putData(chapterListKey, mPresenter.getChapterList());\n        }\n    }\n\n    @Override\n    public void onWindowFocusChanged(boolean hasFocus) {\n        super.onWindowFocusChanged(hasFocus);\n        if (hasFocus) {\n            initImmersionBar();\n        }\n    }\n\n    /**\n     * 状态栏\n     */\n    @Override\n    protected void initImmersionBar() {\n        ActivityExtensionsKt.fullScreen(this);\n        int flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n                | View.SYSTEM_UI_FLAG_IMMERSIVE\n                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);\n        flag = flag | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;\n        if (readBookControl.getHideNavigationBar()) {\n            flag = flag | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;\n            if (binding.readMenuBottom.getVisibility() != View.VISIBLE) {\n                flag = flag | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;\n            }\n        }\n        if (readBookControl.getHideStatusBar()) {\n            if (binding.readMenuBottom.getVisibility() != View.VISIBLE) {\n                flag = flag | View.SYSTEM_UI_FLAG_FULLSCREEN;\n            }\n        }\n        getWindow().getDecorView().setSystemUiVisibility(flag);\n        if (binding.readMenuBottom.getVisibility() == View.VISIBLE) {\n            if (isImmersionBarEnabled()) {\n                ActivityExtensionsKt.setStatusBarColorAuto(this, ThemeStore.primaryColor(this), false, true);\n            } else {\n                ActivityExtensionsKt.setStatusBarColorAuto(this, ColorUtils.darkenColor(ThemeStore.primaryColor(this)), false, true);\n            }\n            changeNavigationBarColor();\n        } else {\n            if (isImmersionBarEnabled()) {\n                getWindow().setStatusBarColor(Color.TRANSPARENT);\n                ActivityExtensionsKt.setLightStatusBar(this, readBookControl.getDarkStatusIcon());\n            } else {\n                getWindow().setStatusBarColor(getResources().getColor(R.color.ate_switch_track_normal_light));\n                ActivityExtensionsKt.setLightStatusBar(this, false);\n            }\n        }\n        changeNavigationBarColor();\n        screenOffTimerStart();\n    }\n\n    /**\n     * 修改导航栏颜色\n     */\n    private void changeNavigationBarColor() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n            getWindow().setNavigationBarDividerColor(Color.TRANSPARENT);\n        }\n        int barColorType = readBookControl.getNavBarColor();\n        if (binding.readMenuBottom.getVisibility() == View.VISIBLE\n                || binding.readAdjustPop.getVisibility() == View.VISIBLE\n                || binding.readInterfacePop.getVisibility() == View.VISIBLE\n                || binding.readAdjustMarginPop.getVisibility() == View.VISIBLE\n                || binding.moreSettingPop.getVisibility() == View.VISIBLE) {\n            barColorType = 0;\n        }\n        switch (barColorType) {\n            case 1:\n                ActivityExtensionsKt.setNavigationBarColorAuto(this, getResources().getColor(R.color.black));\n                break;\n            case 2:\n                ActivityExtensionsKt.setNavigationBarColorAuto(this, getResources().getColor(R.color.white));\n                break;\n            case 3:\n                ActivityExtensionsKt.setNavigationBarColorAuto(this, readBookControl.getBgColor());\n                break;\n        }\n    }\n\n    /**\n     * 取消亮屏保持\n     */\n    private void unKeepScreenOn() {\n        keepScreenOn(false);\n    }\n\n    /**\n     * @param keepScreenOn 是否保持亮屏\n     */\n    public void keepScreenOn(boolean keepScreenOn) {\n        if (keepScreenOn) {\n            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n        } else {\n            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n        }\n    }\n\n    /**\n     * 重置黑屏时间\n     */\n    private void screenOffTimerStart() {\n        if (screenTimeOut < 0) {\n            keepScreenOn(true);\n            return;\n        }\n        int screenOffTime = screenTimeOut * 1000 - SystemUtil.getScreenOffTime(this);\n        if (screenOffTime > 0) {\n            mHandler.removeCallbacks(keepScreenRunnable);\n            keepScreenOn(true);\n            mHandler.postDelayed(keepScreenRunnable, screenOffTime);\n        } else {\n            keepScreenOn(false);\n        }\n    }\n\n    /**\n     * 自动翻页\n     */\n    private void autoPage() {\n        mHandler.removeCallbacks(upHpbNextPage);\n        mHandler.removeCallbacks(autoPageRunnable);\n        if (autoPage) {\n            binding.pbNextPage.setVisibility(View.VISIBLE);\n            //每页按字数计算一次时间\n            nextPageTime = mPageLoader.curPageLength() * 60 * 1000 / readBookControl.getCPM();\n            if (0 == nextPageTime) nextPageTime = 1000;\n            binding.pbNextPage.setMax(nextPageTime);\n            mHandler.postDelayed(upHpbNextPage, upHpbInterval);\n            mHandler.postDelayed(autoPageRunnable, nextPageTime);\n        } else {\n            binding.pbNextPage.setVisibility(View.INVISIBLE);\n        }\n        binding.readMenuBottom.setAutoPage(autoPage);\n    }\n\n    /**\n     * 更新自动翻页进度条\n     */\n    private void upHpbNextPage() {\n        nextPageTime = nextPageTime - upHpbInterval;\n        binding.pbNextPage.setProgress(nextPageTime);\n        mHandler.postDelayed(upHpbNextPage, upHpbInterval);\n    }\n\n    /**\n     * 停止自动翻页\n     */\n    private void autoPageStop() {\n        autoPage = false;\n        autoPage();\n    }\n\n    /**\n     * 下一页\n     */\n    private void nextPage() {\n        runOnUiThread(() -> {\n            screenOffTimerStart();\n            if (mPageLoader != null) {\n                mPageLoader.skipToNextPage();\n            }\n        });\n    }\n\n    @Override\n    protected void initData() {\n        mPresenter.saveProgress();\n        //显示菜单\n        menuTopIn = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_top_in);\n        menuTopIn.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {\n                initImmersionBar();\n                BookChapterBean durChapter = mPresenter.getDurChapter();\n                BookSourceBean source = mPresenter.getBookSource();\n                if (durChapter != null && source != null) {\n                    if (TextUtils.isEmpty(source.getLoginUrl())) {\n                        binding.login.setVisibility(View.GONE);\n                        binding.pay.setVisibility(View.GONE);\n                    } else if (durChapter.getIsVip() && !durChapter.getIsPay() && !TextUtils.isEmpty(source.getPayAction())) {\n                        binding.login.setVisibility(View.VISIBLE);\n                        binding.pay.setVisibility(View.VISIBLE);\n                    } else {\n                        binding.login.setVisibility(View.VISIBLE);\n                    }\n                } else {\n                    binding.login.setVisibility(View.GONE);\n                    binding.pay.setVisibility(View.GONE);\n                }\n            }\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n                binding.vMenuBg.setOnClickListener(v -> popMenuOut());\n                initImmersionBar();\n                int nbh = ActivityExtensionsKt.getNavigationBarHeight(ReadBookActivity.this);\n                binding.readMenuBottom.setNavigationBarHeight(nbh);\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {\n\n            }\n        });\n        menuBottomIn = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_bottom_in);\n        menuBottomIn.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {\n                initImmersionBar();\n            }\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n                binding.vMenuBg.setOnClickListener(v -> popMenuOut());\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {\n\n            }\n        });\n\n        //隐藏菜单\n        menuTopOut = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_top_out);\n        menuTopOut.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {\n                binding.vMenuBg.setOnClickListener(null);\n                initImmersionBar();\n            }\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n                binding.flMenu.setVisibility(View.INVISIBLE);\n                binding.llMenuTop.setVisibility(View.INVISIBLE);\n                binding.readMenuBottom.setVisibility(View.INVISIBLE);\n                binding.readAdjustPop.setVisibility(View.INVISIBLE);\n                binding.readAdjustMarginPop.setVisibility(View.INVISIBLE);\n                binding.readInterfacePop.setVisibility(View.INVISIBLE);\n                binding.moreSettingPop.setVisibility(View.INVISIBLE);\n                initImmersionBar();\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {\n\n            }\n        });\n        menuBottomOut = AnimationUtils.loadAnimation(this, R.anim.anim_readbook_bottom_out);\n        menuBottomOut.setAnimationListener(new Animation.AnimationListener() {\n            @Override\n            public void onAnimationStart(Animation animation) {\n                binding.vMenuBg.setOnClickListener(null);\n                initImmersionBar();\n            }\n\n            @Override\n            public void onAnimationEnd(Animation animation) {\n                binding.flMenu.setVisibility(View.INVISIBLE);\n                binding.llMenuTop.setVisibility(View.INVISIBLE);\n                binding.readMenuBottom.setVisibility(View.INVISIBLE);\n                binding.readAdjustPop.setVisibility(View.INVISIBLE);\n                binding.readAdjustMarginPop.setVisibility(View.INVISIBLE);\n                binding.readInterfacePop.setVisibility(View.INVISIBLE);\n                binding.moreSettingPop.setVisibility(View.INVISIBLE);\n                initImmersionBar();\n            }\n\n            @Override\n            public void onAnimationRepeat(Animation animation) {\n\n            }\n        });\n        if (MApplication.isEInkMode) {\n            menuTopIn.setDuration(0);\n            menuTopOut.setDuration(0);\n            menuBottomIn.setDuration(0);\n            menuBottomOut.setDuration(0);\n        }\n    }\n\n    @Override\n    protected void bindView() {\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        mPresenter.initData(this);\n        binding.appBar.setPadding(0, ScreenUtils.getStatusBarHeight(), 0, 0);\n        binding.appBar.setBackgroundColor(ThemeStore.primaryColor(this));\n        binding.readMenuBottom.setFabNightTheme(isNightTheme());\n        //弹窗\n        moDialogHUD = new MoDialogHUD(this);\n        initBottomMenu();\n        initReadInterfacePop();\n        initReadAdjustPop();\n        initReadAdjustMarginPop();\n        initMoreSettingPop();\n        initMediaPlayer();\n        initReadLongPressPop();\n        binding.pageView.setBackground(readBookControl.getTextBackground(this));\n        binding.cursorLeft.getDrawable().setColorFilter(ThemeStore.accentColor(this), PorterDuff.Mode.SRC_ATOP);\n        binding.cursorRight.getDrawable().setColorFilter(ThemeStore.accentColor(this), PorterDuff.Mode.SRC_ATOP);\n    }\n\n    /**\n     * 初始化播放界面\n     */\n    private void initMediaPlayer() {\n        binding.mediaPlayerPop.setIvChapterClickListener(v -> ChapterListActivity.startThis(ReadBookActivity.this, mPresenter.getBookShelf(), mPresenter.getChapterList()));\n        binding.mediaPlayerPop.setIvTimerClickListener(v -> ReadAloudService.setTimer(getContext(), 10));\n        binding.mediaPlayerPop.setIvCoverBgClickListener(v -> {\n            binding.flMenu.setVisibility(View.VISIBLE);\n            binding.llMenuTop.setVisibility(View.VISIBLE);\n            binding.llMenuTop.startAnimation(menuTopIn);\n        });\n        binding.mediaPlayerPop.setPlayClickListener(v -> onMediaButton(ReadAloudService.ActionMediaPlay));\n        binding.mediaPlayerPop.setPrevClickListener(v -> {\n            mPresenter.getBookShelf().setDurChapterPage(0);\n            mPageLoader.skipToPrePage();\n        });\n        binding.mediaPlayerPop.setNextClickListener(v -> {\n            mPresenter.getBookShelf().setDurChapterPage(0);\n            mPageLoader.skipToNextPage();\n        });\n        binding.mediaPlayerPop.setCallback(dur -> ReadAloudService.setProgress(ReadBookActivity.this, dur));\n    }\n\n    /**\n     * 初始化底部菜单\n     */\n    private void initBottomMenu() {\n        binding.readMenuBottom.setListener(new ReadBottomMenu.Callback() {\n            @Override\n            public void skipToPage(int page) {\n                if (mPageLoader != null) {\n                    mPageLoader.skipToPage(page);\n                }\n            }\n\n            @Override\n            public void onMediaButton() {\n                ReadBookActivity.this.onMediaButton(ReadAloudService.ActionMediaPlay);\n            }\n\n            @Override\n            public void autoPage() {\n                if (ReadAloudService.running) {\n                    ReadBookActivity.this.toast(R.string.aloud_can_not_auto_page);\n                    return;\n                }\n                ReadBookActivity.this.autoPage = !ReadBookActivity.this.autoPage;\n                ReadBookActivity.this.autoPage();\n            }\n\n            @Override\n            public void setNightTheme() {\n                ReadBookActivity.this.setNightTheme(!isNightTheme());\n            }\n\n            @Override\n            public void skipPreChapter() {\n                if (mPresenter.getBookShelf() != null) {\n                    mPageLoader.skipPreChapter();\n                }\n            }\n\n            @Override\n            public void skipNextChapter() {\n                if (mPresenter.getBookShelf() != null) {\n                    mPageLoader.skipNextChapter();\n                }\n            }\n\n            @Override\n            public void openReplaceRule() {\n                popMenuOut();\n                ReplaceRuleActivity.startThis(ReadBookActivity.this, mPresenter.getBookShelf());\n            }\n\n            @Override\n            public void openChapterList() {\n                ReadBookActivity.this.popMenuOut();\n                if (!mPresenter.getChapterList().isEmpty()) {\n                    mHandler.postDelayed(() -> ChapterListActivity.startThis(ReadBookActivity.this, mPresenter.getBookShelf(), mPresenter.getChapterList()), menuTopOut.getDuration());\n                }\n            }\n\n            @Override\n            public void openAdjust() {\n                ReadBookActivity.this.popMenuOut();\n                mHandler.postDelayed(ReadBookActivity.this::readAdjustIn, menuBottomOut.getDuration() + 100);\n            }\n\n            @Override\n            public void openReadInterface() {\n                ReadBookActivity.this.popMenuOut();\n                mHandler.postDelayed(ReadBookActivity.this::readInterfaceIn, menuBottomOut.getDuration() + 100);\n            }\n\n            @Override\n            public void openMoreSetting() {\n                ReadBookActivity.this.popMenuOut();\n                mHandler.postDelayed(ReadBookActivity.this::moreSettingIn, menuBottomOut.getDuration() + 100);\n            }\n\n            @Override\n            public void toast(int id) {\n                ReadBookActivity.this.toast(id);\n            }\n\n            @Override\n            public void dismiss() {\n                popMenuOut();\n            }\n        });\n    }\n\n\n    /**\n     * 初始化调节\n     */\n    private void initReadAdjustPop() {\n        binding.readAdjustPop.setListener(this, new ReadAdjustPop.Callback() {\n            @Override\n            public void speechRateFollowSys() {\n                if (ReadAloudService.running) {\n                    ReadAloudService.stop(ReadBookActivity.this);\n                }\n            }\n\n            @Override\n            public void changeSpeechRate(int speechRate) {\n                if (ReadAloudService.running) {\n                    ReadAloudService.pause(ReadBookActivity.this);\n                    ReadAloudService.resume(ReadBookActivity.this);\n                }\n            }\n        });\n    }\n\n    /**\n     * 初始化调节\n     */\n    private void initReadAdjustMarginPop() {\n        binding.readAdjustMarginPop.setListener(this, new ReadAdjustMarginPop.Callback() {\n\n            @Override\n            public void upTextSize() {\n                if (mPageLoader != null) {\n                    mPageLoader.setTextSize();\n                }\n            }\n\n            @Override\n            public void upMargin() {\n                if (mPageLoader != null) {\n                    mPageLoader.upMargin();\n                }\n            }\n\n            @Override\n            public void refresh() {\n                if (mPageLoader != null) {\n                    mPageLoader.refreshUi();\n                }\n            }\n\n        });\n    }\n\n    /**\n     * 初始化界面设置\n     */\n    private void initReadInterfacePop() {\n        binding.readInterfacePop.setListener(this, new ReadInterfacePop.Callback() {\n\n            @Override\n            public void upPageMode() {\n                if (mPageLoader != null) {\n                    mPageLoader.setPageMode(PageAnimation.Mode.getPageMode(readBookControl.getPageMode()));\n                }\n            }\n\n            @Override\n            public void upTextSize() {\n                if (mPageLoader != null) {\n                    mPageLoader.setTextSize();\n                }\n            }\n\n            @Override\n            public void upMargin() {\n                if (mPageLoader != null) {\n                    mPageLoader.upMargin();\n                }\n            }\n\n            @Override\n            public void bgChange() {\n                readBookControl.initTextDrawableIndex();\n                binding.pageView.setBackground(readBookControl.getTextBackground(ReadBookActivity.this));\n                initImmersionBar();\n                if (mPageLoader != null) {\n                    mPageLoader.refreshUi();\n                }\n            }\n\n            @Override\n            public void refresh() {\n                if (mPageLoader != null) {\n                    mPageLoader.refreshUi();\n                }\n            }\n\n        });\n    }\n\n    /**\n     * 初始化其它设置\n     */\n    private void initMoreSettingPop() {\n        binding.moreSettingPop.setListener(new MoreSettingPop.Callback() {\n            @Override\n            public void upBar() {\n                initImmersionBar();\n            }\n\n            @Override\n            public void keepScreenOnChange(int keepScreenOn) {\n                screenTimeOut = getResources().getIntArray(R.array.screen_time_out_value)[keepScreenOn];\n                screenOffTimerStart();\n            }\n\n            @Override\n            public void recreate() {\n                ReadBookActivity.this.recreate();\n            }\n\n            @Override\n            public void refreshPage() {\n                if (mPageLoader != null) {\n                    mPageLoader.refreshUi();\n                }\n            }\n        });\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    protected void bindEvent() {\n        binding.tvChapterName.setOnClickListener(v -> {\n            if (mPresenter.getBookSource() != null) {\n                SourceEditActivity.startThis(this, mPresenter.getBookSource());\n            }\n        });\n        //打开URL\n        binding.tvChapterUrl.setOnClickListener(view -> {\n            try {\n                String url = binding.tvChapterUrl.getText().toString();\n                Intent intent = new Intent(Intent.ACTION_VIEW);\n                intent.setData(Uri.parse(url));\n                startActivity(intent);\n            } catch (Exception e) {\n                toast(R.string.can_not_open);\n            }\n        });\n        binding.login.setOnClickListener(v -> login());\n        binding.pay.setOnClickListener(v -> pay());\n        binding.cursorLeft.setOnTouchListener(this);\n        binding.cursorRight.setOnTouchListener(this);\n        binding.flContent.setOnTouchListener(this);\n    }\n\n    /**\n     * 开始加载\n     */\n    @Override\n    public void startLoadingBook() {\n        initPageView();\n        binding.mediaPlayerPop.setCover(mPresenter.getBookShelf().getCustomCoverPath() != null ? mPresenter.getBookShelf().getCustomCoverPath()\n                : mPresenter.getBookShelf().getBookInfoBean().getCoverUrl());\n    }\n\n    /**\n     * 加载阅读页面\n     */\n    private void initPageView() {\n        mPageLoader = binding.pageView.getPageLoader(this, mPresenter.getBookShelf(),\n                new PageLoader.Callback() {\n                    @Override\n                    public List<BookChapterBean> getChapterList() {\n                        return mPresenter.getChapterList();\n                    }\n\n                    /**\n                     * @param pos:切换章节的序号\n                     */\n                    @Override\n                    public void onChapterChange(int pos) {\n                        if (mPresenter.getChapterList().isEmpty()) return;\n                        if (pos >= mPresenter.getChapterList().size()) return;\n                        mPresenter.getBookShelf().setDurChapterName(mPresenter.getChapterList().get(pos).getDurChapterName());\n                        actionBar.setTitle(mPresenter.getBookShelf().getBookInfoBean().getName());\n                        if (mPresenter.getBookShelf().getChapterListSize() > 0) {\n                            BookChapterBean chapter = mPresenter.getChapterList().get(pos);\n                            if (chapter.getIsVip() && !chapter.getIsPay()) {\n                                toast(\"付费章节未购买,如已购买请登录并刷新目录\");\n                            }\n                            binding.tvChapterName.setVisibility(View.VISIBLE);\n                            binding.tvChapterName.setText(mPresenter.getChapterList().get(pos).getDurChapterName());\n                            binding.tvChapterUrl.setVisibility(View.VISIBLE);\n                            binding.tvChapterUrl.setText(NetworkUtils.getAbsoluteURL(mPresenter.getBookShelf().getBookInfoBean().getChapterUrl(),\n                                    chapter.getDurChapterUrl()));\n                        } else {\n                            binding.tvChapterName.setVisibility(View.GONE);\n                            binding.tvChapterUrl.setVisibility(View.GONE);\n                        }\n\n                        if (mPresenter.getBookShelf().getChapterListSize() == 1) {\n                            binding.readMenuBottom.setTvPre(false);\n                            binding.readMenuBottom.setTvNext(false);\n                        } else {\n                            if (pos == 0) {\n                                binding.readMenuBottom.setTvPre(false);\n                                binding.readMenuBottom.setTvNext(true);\n                            } else if (pos == mPresenter.getBookShelf().getChapterListSize() - 1) {\n                                binding.readMenuBottom.setTvPre(true);\n                                binding.readMenuBottom.setTvNext(false);\n                            } else {\n                                binding.readMenuBottom.setTvPre(true);\n                                binding.readMenuBottom.setTvNext(true);\n                            }\n                        }\n                    }\n\n                    /**\n                     * @param chapters：返回章节目录\n                     */\n                    @Override\n                    public void onCategoryFinish(List<BookChapterBean> chapters) {\n                        mPresenter.setChapterList(chapters);\n                        mPresenter.getBookShelf().setChapterListSize(chapters.size());\n                        mPresenter.getBookShelf().setDurChapterName(chapters.get(mPresenter.getBookShelf().getDurChapter()).getDurChapterName());\n                        mPresenter.getBookShelf().setLastChapterName(chapters.get(mPresenter.getChapterList().size() - 1).getDurChapterName());\n                        mPresenter.saveProgress();\n                    }\n\n                    /**\n                     * 总页数变化\n                     */\n                    @Override\n                    public void onPageCountChange(int count) {\n                        binding.readMenuBottom.getReadProgress().setMax(Math.max(0, count - 1));\n                        binding.readMenuBottom.getReadProgress().setProgress(0);\n                        // 如果处于错误状态，那么就冻结使用\n                        binding.readMenuBottom.getReadProgress().setEnabled(\n                                mPageLoader.getPageStatus() != TxtChapter.Status.LOADING\n                                        && mPageLoader.getPageStatus() != TxtChapter.Status.ERROR\n                        );\n                    }\n\n                    /**\n                     * 翻页成功\n                     */\n                    @Override\n                    public void onPageChange(int chapterIndex, int pageIndex, boolean resetReadAloud) {\n                        mPresenter.getBookShelf().setDurChapter(chapterIndex);\n                        mPresenter.getBookShelf().setDurChapterPage(pageIndex);\n                        mPresenter.saveProgress();\n                        binding.readMenuBottom.getReadProgress().post(\n                                () -> binding.readMenuBottom.getReadProgress().setProgress(pageIndex)\n                        );\n                        Long end = mPresenter.getDurChapter().getEnd();\n                        int audioSize = end != null ? end.intValue() : 0;\n                        binding.mediaPlayerPop.upAudioSize(audioSize);\n                        binding.mediaPlayerPop.upAudioDur(mPresenter.getBookShelf().getDurChapterPage());\n                        if (mPresenter.getBookShelf().isAudio() && mPageLoader.getPageStatus() == TxtChapter.Status.FINISH) {\n                            if (binding.mediaPlayerPop.getVisibility() != View.VISIBLE) {\n                                binding.mediaPlayerPop.setVisibility(View.VISIBLE);\n                            }\n                        } else {\n                            if (binding.mediaPlayerPop.getVisibility() == View.VISIBLE) {\n                                binding.mediaPlayerPop.setVisibility(View.GONE);\n                            }\n                        }\n                        if ((ReadAloudService.running)) {\n                            if (resetReadAloud) {\n                                readAloud();\n                                return;\n                            }\n                            if (pageIndex == 0) {\n                                readAloud();\n                                return;\n                            }\n                        }\n\n                        //启动朗读\n                        if (getIntent().getBooleanExtra(\"readAloud\", false)\n                                && pageIndex >= 0 && mPageLoader.getContent() != null) {\n                            getIntent().putExtra(\"readAloud\", false);\n                            onMediaButton(ReadAloudService.ActionMediaPlay);\n                            return;\n                        }\n                        autoPage();\n                    }\n\n                    @Override\n                    public void vipPop() {\n                        moDialogHUD.showTwoButton(ReadBookActivity.this.getString(R.string.donate_s), \"领取红包\", (v) -> {\n                                    DonateActivity.getZfbHb(ReadBookActivity.this);\n                                    mHandler.postDelayed(() -> {\n                                        ReadBookActivity.this.refreshDurChapter();\n                                        moDialogHUD.dismiss();\n                                    }, 2000);\n                                },\n                                \"关注公众号\",\n                                (v) -> {\n                                    ClipboardManager clipboard = (ClipboardManager) ReadBookActivity.this.getSystemService(Context.CLIPBOARD_SERVICE);\n                                    ClipData clipData = ClipData.newPlainText(null, \"开源阅读软件\");\n                                    if (clipboard != null) {\n                                        clipboard.setPrimaryClip(clipData);\n                                        toast(\"[开源阅读软件],已复制成功,可到微信搜索\");\n                                    }\n                                    MApplication.getInstance().upDonateHb();\n                                    mHandler.postDelayed(() -> {\n                                        ReadBookActivity.this.refreshDurChapter();\n                                        moDialogHUD.dismiss();\n                                    }, 1000);\n                                },\n                                true);\n                    }\n                }\n        );\n        mPageLoader.updateBattery(BatteryUtil.getLevel(this));\n        binding.pageView.setTouchListener(new PageView.TouchListener() {\n            @Override\n            public void onTouch() {\n                screenOffTimerStart();\n            }\n\n            @Override\n            public void center() {\n                popMenuIn();\n            }\n\n            @Override\n            public void onTouchClearCursor() {\n                binding.cursorLeft.setVisibility(View.INVISIBLE);\n                binding.cursorRight.setVisibility(View.INVISIBLE);\n                binding.readLongPress.setVisibility(View.INVISIBLE);\n            }\n\n            @Override\n            public void onLongPress() {\n                if (!binding.pageView.isRunning()) {\n                    selectTextCursorShow();\n                    showAction(binding.cursorLeft);\n                }\n            }\n        });\n        mPageLoader.refreshChapterList();\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouch(View v, MotionEvent event) {\n\n        if (v.getId() == R.id.cursor_left || v.getId() == R.id.cursor_right) {\n            int ea = event.getAction();\n            switch (ea) {\n                case MotionEvent.ACTION_DOWN:\n                    lastX = (int) event.getRawX();// 获取触摸事件触摸位置的原始X坐标\n                    lastY = (int) event.getRawY();\n\n                    binding.readLongPress.setVisibility(View.INVISIBLE);\n\n                    break;\n                case MotionEvent.ACTION_MOVE:\n                    int dx = (int) event.getRawX() - lastX;\n                    int dy = (int) event.getRawY() - lastY;\n                    int l = v.getLeft() + dx;\n                    int b = v.getBottom() + dy;\n                    int r = v.getRight() + dx;\n                    int t = v.getTop() + dy;\n\n                    v.layout(l, t, r, b);\n                    lastX = (int) event.getRawX();\n                    lastY = (int) event.getRawY();\n                    v.postInvalidate();\n\n                    //移动过程中要画线\n                    binding.pageView.setSelectMode(PageView.SelectMode.SelectMoveForward);\n\n                    int hh = binding.cursorLeft.getHeight();\n                    int ww = binding.cursorLeft.getWidth();\n\n                    if (v.getId() == R.id.cursor_left) {\n                        binding.pageView.setFirstSelectTxtChar(binding.pageView.getCurrentTxtChar(lastX + ww, lastY - hh));\n                    } else {\n                        binding.pageView.setLastSelectTxtChar(binding.pageView.getCurrentTxtChar(lastX - ww, lastY - hh));\n                    }\n\n                    binding.pageView.invalidate();\n\n                    break;\n                case MotionEvent.ACTION_UP:\n                    showAction(v);\n                    //v.layout(l, t, r, b);\n                    break;\n                default:\n                    break;\n            }\n        }\n        return true;\n    }\n\n    public void showAction(View clickView) {\n\n        binding.readLongPress.setVisibility(View.VISIBLE);\n        //如果太靠右，则靠左\n        int[] aa = ScreenUtils.getScreenSize(this);\n        if ((binding.cursorLeft.getX() + ScreenUtils.dpToPx(200)) > aa[0]) {\n            binding.readLongPress.setX(aa[0] - ScreenUtils.dpToPx(200));\n        } else {\n            binding.readLongPress.setX(binding.cursorLeft.getX() + binding.cursorLeft.getWidth() + ScreenUtils.dpToPx(5));\n        }\n\n        //如果太靠上\n        if ((binding.cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()) - ScreenUtils.dpToPx(60)) < 0) {\n            binding.readLongPress.setY(binding.cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()));\n        } else {\n            binding.readLongPress.setY(binding.cursorLeft.getY() - ScreenUtils.spToPx(readBookControl.getTextSize()) - ScreenUtils.dpToPx(40));\n        }\n\n    }\n\n    /**\n     * 显示\n     */\n    private void selectTextCursorShow() {\n        if (binding.pageView.getFirstSelectTxtChar() == null || binding.pageView.getLastSelectTxtChar() == null)\n            return;\n        //show Cursor on current position\n        cursorShow();\n        //set current word selected\n        binding.pageView.invalidate();\n\n        hideSnackBar();\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    private void cursorShow() {\n\n        binding.cursorLeft.setVisibility(View.VISIBLE);\n        binding.cursorRight.setVisibility(View.VISIBLE);\n        int hh = binding.cursorLeft.getHeight();\n        int ww = binding.cursorLeft.getWidth();\n        if (binding.pageView.getFirstSelectTxtChar() != null) {\n            binding.cursorLeft.setX(binding.pageView.getFirstSelectTxtChar().getTopLeftPosition().x - ww);\n            binding.cursorLeft.setY(binding.pageView.getFirstSelectTxtChar().getBottomLeftPosition().y);\n            binding.cursorRight.setX(binding.pageView.getFirstSelectTxtChar().getBottomRightPosition().x);\n            binding.cursorRight.setY(binding.pageView.getFirstSelectTxtChar().getBottomRightPosition().y);\n        }\n    }\n\n    /**\n     * 长按选择按钮\n     */\n    private void initReadLongPressPop() {\n        binding.readLongPress.setListener(new ReadLongPressPop.OnBtnClickListener() {\n            @Override\n            public void copySelect() {\n                ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);\n                ClipData clipData = ClipData.newPlainText(null, binding.pageView.getSelectStr());\n                if (clipboard != null) {\n                    clipboard.setPrimaryClip(clipData);\n                    toast(\"所选内容已经复制到剪贴板\");\n                }\n\n                binding.cursorLeft.setVisibility(View.INVISIBLE);\n                binding.cursorRight.setVisibility(View.INVISIBLE);\n                binding.readLongPress.setVisibility(View.INVISIBLE);\n                binding.pageView.clearSelect();\n\n            }\n\n            @Override\n            public void replaceSelect() {\n                ReplaceRuleBean oldRuleBean = new ReplaceRuleBean();\n                oldRuleBean.setReplaceSummary(binding.pageView.getSelectStr().trim());\n                oldRuleBean.setEnable(true);\n                oldRuleBean.setRegex(binding.pageView.getSelectStr().trim());\n                oldRuleBean.setIsRegex(false);\n                oldRuleBean.setReplacement(\"\");\n                oldRuleBean.setSerialNumber(0);\n                oldRuleBean.setUseTo(String.format(\"%s,%s\", mPresenter.getBookShelf().getBookInfoBean().getName(), mPresenter.getBookShelf().getTag()));\n\n                ReplaceRuleDialog.builder(ReadBookActivity.this, oldRuleBean, mPresenter.getBookShelf(), ReplaceRuleDialog.DefaultUI)\n                        .setPositiveButton(replaceRuleBean1 ->\n                                ReplaceRuleManager.saveData(replaceRuleBean1)\n                                        .subscribe(new MySingleObserver<Boolean>() {\n                                            @Override\n                                            public void onSuccess(@NonNull Boolean aBoolean) {\n                                                binding.cursorLeft.setVisibility(View.INVISIBLE);\n                                                binding.cursorRight.setVisibility(View.INVISIBLE);\n                                                binding.readLongPress.setVisibility(View.INVISIBLE);\n\n                                                binding.pageView.setSelectMode(PageView.SelectMode.Normal);\n\n                                                moDialogHUD.dismiss();\n\n                                                refresh(false);\n                                            }\n                                        })).show();\n            }\n\n            @Override\n            public void replaceSelectAd() {\n                String selectString = binding.pageView.getSelectStr();\n\n                if (selectString != null) {\n                    String spacer = null;\n                    String name = (mPresenter.getBookShelf().getBookInfoBean().getName());\n                    if (name != null)\n                        if (name.trim().length() > 0)\n                            spacer = \"|\" + Pattern.quote(name.trim());\n//                        spacer = \"|\" + Matcher.quoteReplacement(name.trim());\n\n                    name = (mPresenter.getBookShelf().getBookInfoBean().getAuthor());\n                    if (name != null)\n                        if (name.trim().length() > 0)\n                            if (spacer != null)\n                                spacer = spacer + \"|\" + Pattern.quote(name.trim());\n                            else\n                                spacer = \"|\" + Pattern.quote(name.trim());\n                    String rule = \"(\\\\s*\\n\\\\s*\" + spacer + \")\";\n                    selectString = ReplaceRuleManager.formateAdRule(\n                            selectString.replaceAll(rule, \"\\n\")\n                    );\n\n                }\n\n                ReplaceRuleBean oldRuleBean = new ReplaceRuleBean();\n                oldRuleBean.setReplaceSummary(getString(R.string.replace_ad) + \"-\" + mPresenter.getBookShelf().getTag());\n                oldRuleBean.setEnable(true);\n                oldRuleBean.setRegex(selectString);\n                oldRuleBean.setIsRegex(false);\n                oldRuleBean.setReplacement(\"\");\n                oldRuleBean.setSerialNumber(0);\n                oldRuleBean.setUseTo(mPresenter.getBookShelf().getTag());\n\n                ReplaceRuleDialog.builder(ReadBookActivity.this, oldRuleBean, mPresenter.getBookShelf(), ReplaceRuleDialog.AddAdUI)\n                        .setPositiveButton(replaceRuleBean1 ->\n                                ReplaceRuleManager.mergeAdRules(replaceRuleBean1)\n                                        .subscribe(new MySingleObserver<Boolean>() {\n                                            @Override\n                                            public void onSuccess(@NonNull Boolean aBoolean) {\n                                                binding.cursorLeft.setVisibility(View.INVISIBLE);\n                                                binding.cursorRight.setVisibility(View.INVISIBLE);\n                                                binding.readLongPress.setVisibility(View.INVISIBLE);\n\n                                                binding.pageView.setSelectMode(PageView.SelectMode.Normal);\n\n                                                moDialogHUD.dismiss();\n\n                                                refresh(false);\n                                            }\n                                        })).show();\n\n            }\n        });\n    }\n\n\n    /**\n     * 设置ToolBar\n     */\n    private void setupActionBar() {\n        actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n        }\n    }\n\n    /**\n     * 添加菜单\n     */\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_book_read_activity, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    @Override\n    public boolean onPrepareOptionsMenu(Menu menu) {\n        this.menu = menu;\n        upMenu();\n        return super.onPrepareOptionsMenu(menu);\n    }\n\n    /**\n     * 菜单事件\n     */\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.enable_replace) {\n            mPresenter.getBookShelf().setReplaceEnable(!mPresenter.getBookShelf().getReplaceEnable());\n            refresh(false);\n        } else if (id == R.id.action_change_source) {\n            changeSource();\n        } else if (id == R.id.action_refresh) {\n            refreshDurChapter();\n        } else if (id == R.id.action_download) {\n            download();\n        } else if (id == R.id.add_bookmark) {\n            showBookmark(null);\n        } else if (id == R.id.action_copy_text) {\n            popMenuOut();\n            if (mPageLoader != null) {\n                moDialogHUD.showText(mPageLoader.getAllContent());\n            }\n        } else if (id == R.id.disable_book_source) {\n            mPresenter.disableDurBookSource();\n        } else if (id == R.id.action_book_info) {\n            BookInfoEditActivity.startThis(this, mPresenter.getBookShelf().getNoteUrl());\n        } else if (id == R.id.action_set_charset) {\n            setCharset();\n        } else if (id == R.id.update_chapter_list) {\n            if (mPageLoader != null) {\n                mPageLoader.updateChapter();\n            }\n        } else if (id == R.id.action_set_regex) {\n            setTextChapterRegex();\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    private void login() {\n        BookSourceBean source = mPresenter.getBookSource();\n        if (TextUtils.isEmpty(source.getLoginUi())) {\n            SourceLoginActivity.startThis(this, source);\n        } else {\n            SourceLoginDialog.Companion.start(\n                    getSupportFragmentManager(),\n                    source.getBookSourceUrl()\n            );\n        }\n    }\n\n    private void pay() {\n        BookShelfBean book = mPresenter.getBookShelf();\n        BookChapterBean chapter = mPresenter.getDurChapter();\n        BookSourceBean source = mPresenter.getBookSource();\n        String payRule = source.getPayAction();\n        if (chapter.getIsVip() && !chapter.getIsPay() && !TextUtils.isEmpty(payRule)) {\n            String result = \"\";\n            if (payRule.startsWith(\"http\")) {\n                result = payRule;\n            } else {\n                try {\n                    SimpleBindings bindings = new SimpleBindings();\n                    bindings.put(\"java\", source);\n                    bindings.put(\"source\", source);\n                    bindings.put(\"book\", book);\n                    bindings.put(\"chapter\", chapter);\n                    result = SCRIPT_ENGINE.eval(payRule, bindings).toString();\n                } catch (Exception e) {\n                    e.printStackTrace();\n                }\n            }\n            if (result.startsWith(\"http\")) {\n                Intent webIntent = new Intent(this, WebViewActivity.class);\n                webIntent.putExtra(\"url\", result);\n                webIntent.putExtra(\"title\", \"购买\");\n                BitIntentDataManager.getInstance().putData(result, mPresenter.getBookSource().getHeaderMap(true));\n                //noinspection deprecation\n                startActivityForResult(webIntent, payActivityRequest);\n            }\n        }\n    }\n\n    /**\n     * 刷新当前章节\n     */\n    private void refreshDurChapter() {\n        if (!NetworkUtils.isNetWorkAvailable()) {\n            toast(\"网络不可用，无法刷新当前章节!\");\n            return;\n        }\n        ReadBookActivity.this.popMenuOut();\n        if (mPageLoader != null) {\n            if (mPageLoader instanceof PageLoaderNet) {\n                ((PageLoaderNet) mPageLoader).refreshDurChapter();\n            }\n        }\n    }\n\n    /**\n     * 书签\n     */\n    @Override\n    public void showBookmark(BookmarkBean bookmarkBean) {\n        this.popMenuOut();\n        boolean isAdd = false;\n        if (mPresenter.getBookShelf() != null) {\n            if (bookmarkBean == null) {\n                isAdd = true;\n                bookmarkBean = new BookmarkBean();\n                bookmarkBean.setNoteUrl(mPresenter.getBookShelf().getNoteUrl());\n                bookmarkBean.setBookName(mPresenter.getBookShelf().getBookInfoBean().getName());\n                bookmarkBean.setChapterIndex(mPresenter.getBookShelf().getDurChapter());\n                bookmarkBean.setPageIndex(mPresenter.getBookShelf().getDurChapterPage());\n                bookmarkBean.setChapterName(mPresenter.getBookShelf().getDurChapterName());\n            }\n            BookmarkDialog.builder(this, bookmarkBean, isAdd)\n                    .setPositiveButton(new BookmarkDialog.Callback() {\n                        @Override\n                        public void saveBookmark(BookmarkBean bookmarkBean) {\n                            mPresenter.saveBookmark(bookmarkBean);\n                        }\n\n                        @Override\n                        public void delBookmark(BookmarkBean bookmarkBean) {\n                            mPresenter.delBookmark(bookmarkBean);\n                        }\n\n                        @Override\n                        public void openChapter(int chapterIndex, int pageIndex) {\n                            skipToChapter(chapterIndex, pageIndex);\n                        }\n                    }).show();\n        }\n\n    }\n\n    @Override\n    public void skipToChapter(int chapterIndex, int pageIndex) {\n        if (mPageLoader != null) {\n            mPageLoader.skipToChapter(chapterIndex, pageIndex);\n        }\n    }\n\n    /**\n     * 自动换源\n     */\n    public void autoChangeSource() {\n        mPresenter.autoChangeSource();\n    }\n\n    /**\n     * 换源\n     */\n    private void changeSource() {\n        if (!NetworkUtils.isNetWorkAvailable()) {\n            toast(R.string.network_connection_unavailable);\n            return;\n        }\n        ReadBookActivity.this.popMenuOut();\n        if (mPresenter.getBookShelf() != null) {\n            ChangeSourceDialog.builder(this, mPresenter.getBookShelf())\n                    .setCallback(searchBookBean -> {\n                        if (!Objects.equals(searchBookBean.getNoteUrl(), mPresenter.getBookShelf().getNoteUrl())) {\n                            if (mPageLoader != null) {\n                                mPageLoader.setStatus(TxtChapter.Status.CHANGE_SOURCE);\n                            }\n                            mPresenter.changeBookSource(searchBookBean);\n                        }\n                    }).show();\n        }\n    }\n\n    /**\n     * 下载\n     */\n    private void download() {\n        if (!NetworkUtils.isNetWorkAvailable()) {\n            toast(R.string.network_connection_unavailable);\n            return;\n        }\n        ReadBookActivity.this.popMenuOut();\n        if (mPresenter.getBookShelf() != null) {\n            //弹出离线下载界面\n            int endIndex = mPresenter.getBookShelf().getChapterListSize() - 1;\n            DownLoadDialog.builder(this, mPresenter.getBookShelf().getDurChapter(), endIndex, mPresenter.getBookShelf().getChapterListSize())\n                    .setPositiveButton((start, end) -> mPresenter.addDownload(start, end)).show();\n        }\n    }\n\n    /**\n     * 设置编码\n     */\n    private void setCharset() {\n        final String charset = mPresenter.getBookShelf().getBookInfoBean().getCharset();\n        String[] a = new String[]{\"UTF-8\", \"GB2312\", \"GBK\", \"Unicode\", \"UTF-16\", \"UTF-16LE\", \"ASCII\"};\n        List<String> values = new ArrayList<>(Arrays.asList(a));\n        InputDialog.builder(this)\n                .setTitle(getString(R.string.input_charset))\n                .setDefaultValue(charset)\n                .setAdapterValues(values)\n                .setCallback(new InputDialog.Callback() {\n                    @Override\n                    public void setInputText(String inputText) {\n                        inputText = inputText.trim();\n                        if (!Objects.equals(charset, inputText)) {\n                            mPresenter.getBookShelf().getBookInfoBean().setCharset(inputText);\n                            mPresenter.saveProgress();\n                            if (mPageLoader != null) {\n                                mPageLoader.updateChapter();\n                            }\n                        }\n                    }\n\n                    @Override\n                    public void delete(String value) {\n\n                    }\n                }).show();\n    }\n\n    /**\n     * 设置TXT目录正则\n     */\n    private void setTextChapterRegex() {\n        if (mPresenter.getBookShelf().getNoteUrl().toLowerCase().matches(\".*\\\\.txt\")) {\n            int checkedItem = 0;\n            List<TxtChapterRuleBean> ruleBeanList = TxtChapterRuleManager.getEnabled();\n            List<String> ruleNameList = new ArrayList<>();\n            String rule = mPresenter.getBookShelf().getBookInfoBean().getChapterUrl();\n            if (!TextUtils.isEmpty(rule)) {\n                TxtChapterRuleBean ruleBean = DbHelper.getDaoSession().getTxtChapterRuleBeanDao().queryBuilder()\n                        .where(TxtChapterRuleBeanDao.Properties.Rule.eq(rule))\n                        .limit(1).unique();\n                if (ruleBean != null) {\n                    if (!ruleBean.getEnable()) {\n                        ruleBeanList.add(ruleBean);\n                        checkedItem = ruleBeanList.size() - 1;\n                    } else {\n                        checkedItem = ruleBeanList.indexOf(ruleBean);\n                    }\n                } else {\n                    ruleBean = new TxtChapterRuleBean();\n                    ruleBean.setName(rule);\n                    ruleBean.setRule(rule);\n                    ruleBeanList.add(ruleBean);\n                    checkedItem = ruleBeanList.size() - 1;\n                }\n            }\n            for (TxtChapterRuleBean bean : ruleBeanList) {\n                ruleNameList.add(bean.getName());\n            }\n            if (checkedItem < 0) {\n                checkedItem = 0;\n            }\n            AlertDialog dialog = new AlertDialog.Builder(this, R.style.alertDialogTheme)\n                    .setTitle(\"选择目录正则\")\n                    .setSingleChoiceItems(ruleNameList.toArray(new String[0]), checkedItem, (dialog1, which) -> {\n                        if (which < 0) return;\n                        mPresenter.getBookShelf().getBookInfoBean().setChapterUrl(ruleBeanList.get(which).getRule());\n                        mPresenter.saveProgress();\n                        if (mPageLoader != null) {\n                            mPageLoader.updateChapter();\n                        }\n                        dialog1.dismiss();\n                    })\n                    .setPositiveButton(\"管理正则\", (dialog12, which) -> TxtChapterRuleActivity.startThis(ReadBookActivity.this))\n                    .show();\n            ATH.setAlertDialogTint(dialog);\n        }\n    }\n\n    /**\n     * 显示调节\n     */\n    private void readAdjustIn() {\n        binding.flMenu.setVisibility(View.VISIBLE);\n        binding.readAdjustPop.show();\n        binding.readAdjustPop.setVisibility(View.VISIBLE);\n        binding.readAdjustPop.startAnimation(menuBottomIn);\n    }\n\n    /**\n     * 显示自定义边界调节\n     */\n    public void readAdjustMarginIn() {\n        binding.flMenu.setVisibility(View.VISIBLE);\n        binding.readAdjustMarginPop.show();\n        binding.readAdjustMarginPop.setVisibility(View.VISIBLE);\n        binding.readAdjustMarginPop.startAnimation(menuBottomIn);\n    }\n\n    /**\n     * 显示界面设置\n     */\n    private void readInterfaceIn() {\n        binding.flMenu.setVisibility(View.VISIBLE);\n        binding.readInterfacePop.setVisibility(View.VISIBLE);\n        binding.readInterfacePop.startAnimation(menuBottomIn);\n    }\n\n    /**\n     * 显示更多设置\n     */\n    private void moreSettingIn() {\n        binding.flMenu.setVisibility(View.VISIBLE);\n        binding.moreSettingPop.setVisibility(View.VISIBLE);\n        binding.moreSettingPop.startAnimation(menuBottomIn);\n    }\n\n    /**\n     * 显示菜单\n     */\n    private void popMenuIn() {\n        binding.flMenu.setVisibility(View.VISIBLE);\n        binding.llMenuTop.setVisibility(View.VISIBLE);\n        binding.readMenuBottom.setVisibility(View.VISIBLE);\n        binding.llMenuTop.startAnimation(menuTopIn);\n        binding.readMenuBottom.startAnimation(menuBottomIn);\n        hideSnackBar();\n    }\n\n    /**\n     * 隐藏菜单\n     */\n    private void popMenuOut() {\n        if (binding.flMenu.getVisibility() == View.VISIBLE) {\n            if (binding.llMenuTop.getVisibility() == View.VISIBLE) {\n                binding.llMenuTop.startAnimation(menuTopOut);\n            }\n            if (binding.readMenuBottom.getVisibility() == View.VISIBLE) {\n                binding.readMenuBottom.startAnimation(menuBottomOut);\n            }\n            if (binding.moreSettingPop.getVisibility() == View.VISIBLE) {\n                binding.moreSettingPop.startAnimation(menuBottomOut);\n            }\n            if (binding.readAdjustPop.getVisibility() == View.VISIBLE) {\n                binding.readAdjustPop.startAnimation(menuBottomOut);\n            }\n            if (binding.readInterfacePop.getVisibility() == View.VISIBLE) {\n                binding.readInterfacePop.startAnimation(menuBottomOut);\n            }\n            if (binding.readAdjustMarginPop.getVisibility() == View.VISIBLE) {\n                binding.readAdjustMarginPop.startAnimation(menuBottomOut);\n            }\n        }\n    }\n\n    /**\n     * 朗读\n     */\n    private void readAloud() {\n        aloudNextPage = false;\n        String unReadContent = mPageLoader.getUnReadContent();\n        if (mPresenter.getBookShelf().isAudio()) {\n            try {\n                unReadContent = new AnalyzeUrl(unReadContent,\n                        mPresenter.getBookSource().getBookSourceUrl(),\n                        mPresenter.getBookSource(),\n                        mPresenter.getBookSource().getHeaderMap(true)).getRuleUrl();\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        }\n        if (mPresenter.getBookShelf() != null && mPageLoader != null && !StringUtils.isTrimEmpty(unReadContent)) {\n            ReadAloudService.play(ReadBookActivity.this, false, unReadContent,\n                    mPresenter.getBookShelf().getBookInfoBean().getName(),\n                    ChapterContentHelp.getInstance().replaceContent(mPresenter.getBookShelf().getBookInfoBean().getName(),\n                            mPresenter.getBookShelf().getTag(),\n                            mPresenter.getBookShelf().getDurChapterName(),\n                            mPresenter.getBookShelf().getReplaceEnable()),\n                    mPresenter.getBookShelf().isAudio(),\n                    mPresenter.getBookShelf().getDurChapterPage());\n        }\n    }\n\n    /**\n     * 检查是否加入书架\n     */\n    public boolean checkAddShelf() {\n        if (isAdd || mPresenter.getBookShelf() == null\n                || TextUtils.isEmpty(mPresenter.getBookShelf().getBookInfoBean().getName())) {\n            return true;\n        } else if (mPresenter.getChapterList().isEmpty()) {\n            mPresenter.removeFromShelf();\n            return true;\n        } else {\n            if (checkAddShelfPop == null) {\n                checkAddShelfPop = new CheckAddShelfPop(this, mPresenter.getBookShelf().getBookInfoBean().getName(),\n                        new CheckAddShelfPop.OnItemClickListener() {\n                            @Override\n                            public void clickExit() {\n                                mPresenter.removeFromShelf();\n                            }\n\n                            @Override\n                            public void clickAddShelf() {\n                                mPresenter.addToShelf(null);\n                                checkAddShelfPop.dismiss();\n                            }\n                        });\n            }\n            if (!checkAddShelfPop.isShowing()) {\n                checkAddShelfPop.showAtLocation(binding.flContent, Gravity.CENTER, 0, 0);\n            }\n            return false;\n        }\n    }\n\n    /**\n     * 更新朗读状态\n     */\n    @Override\n    public void upAloudState(ReadAloudService.Status status) {\n        aloudStatus = status;\n        autoPageStop();\n        switch (status) {\n            case NEXT:\n                if (mPageLoader == null) {\n                    ReadAloudService.stop(this);\n                    break;\n                }\n                if (!mPageLoader.skipNextChapter()) {\n                    ReadAloudService.stop(this);\n                }\n                break;\n            case PLAY:\n                binding.readMenuBottom.setFabReadAloudImage(R.drawable.ic_pause_outline_24dp);\n                binding.readMenuBottom.setReadAloudTimer(true);\n                binding.mediaPlayerPop.setFabReadAloudImage(R.drawable.ic_pause_24dp);\n                binding.mediaPlayerPop.setSeekBarEnable(true);\n                break;\n            case PAUSE:\n                binding.readMenuBottom.setFabReadAloudImage(R.drawable.ic_play_outline_24dp);\n                binding.readMenuBottom.setReadAloudTimer(true);\n                binding.mediaPlayerPop.setFabReadAloudImage(R.drawable.ic_play_24dp);\n                binding.mediaPlayerPop.setSeekBarEnable(false);\n                break;\n            default:\n                binding.readMenuBottom.setFabReadAloudImage(R.drawable.ic_read_aloud);\n                binding.readMenuBottom.setReadAloudTimer(false);\n                binding.mediaPlayerPop.setFabReadAloudImage(R.drawable.ic_play_24dp);\n                binding.pageView.drawPage(0);\n                binding.pageView.invalidate();\n                binding.pageView.drawPage(-1);\n                binding.pageView.drawPage(1);\n                binding.pageView.invalidate();\n        }\n    }\n\n    /**\n     * 更新定时\n     */\n    @Override\n    public void upAloudTimer(String text) {\n        binding.readMenuBottom.setReadAloudTimer(text);\n    }\n\n    /**\n     * 开始朗读第start个字符\n     */\n    @Override\n    public void readAloudStart(int start) {\n        aloudNextPage = true;\n        if (mPageLoader != null) {\n            mPageLoader.readAloudStart(start);\n        }\n    }\n\n    /**\n     * 朗读长度\n     */\n    @Override\n    public void readAloudLength(int readAloudLength) {\n        if (mPageLoader != null && aloudNextPage) {\n            mPageLoader.readAloudLength(readAloudLength);\n        }\n    }\n\n    /**\n     * 刷新\n     */\n    @Override\n    public void refresh(boolean recreate) {\n        if (recreate) {\n            recreate();\n        } else {\n            binding.flContent.setBackground(readBookControl.getTextBackground(this));\n            if (mPageLoader != null) {\n                mPageLoader.refreshUi();\n            }\n            binding.readInterfacePop.setBg();\n            initImmersionBar();\n        }\n    }\n\n    /**\n     * 按键事件\n     */\n    @Override\n    public boolean dispatchKeyEvent(KeyEvent event) {\n        int keyCode = event.getKeyCode();\n        int action = event.getAction();\n        boolean isDown = action == 0;\n\n        if (keyCode == KeyEvent.KEYCODE_MENU) {\n            return isDown ? this.onKeyDown(keyCode, event) : this.onKeyUp(keyCode, event);\n        }\n        return super.dispatchKeyEvent(event);\n    }\n\n    /**\n     * 按键事件\n     */\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        Boolean mo = moDialogHUD.onKeyDown(keyCode, event);\n        if (mo) {\n            return true;\n        } else {\n            if (keyCode == KeyEvent.KEYCODE_BACK) {\n                if (binding.readInterfacePop.getVisibility() == View.VISIBLE\n                        || binding.readAdjustPop.getVisibility() == View.VISIBLE\n                        || binding.readAdjustMarginPop.getVisibility() == View.VISIBLE\n                        || binding.moreSettingPop.getVisibility() == View.VISIBLE) {\n                    popMenuOut();\n                    return true;\n                } else if (binding.flMenu.getVisibility() == View.VISIBLE) {\n                    finish();\n                    return true;\n                } else if (ReadAloudService.running && aloudStatus == ReadAloudService.Status.PLAY) {\n                    ReadAloudService.pause(this);\n                    if (!mPresenter.getBookShelf().isAudio()) {\n                        toast(R.string.read_aloud_pause);\n                    }\n                    return true;\n                } else {\n                    finish();\n                    return true;\n                }\n            } else if (keyCode == KeyEvent.KEYCODE_MENU) {\n                if (binding.flMenu.getVisibility() == View.VISIBLE) {\n                    popMenuOut();\n                } else {\n                    popMenuIn();\n                }\n                return true;\n            } else if (binding.flMenu.getVisibility() != View.VISIBLE) {\n                if (keyCode == preferences.getInt(\"nextKeyCode\", 0)) {\n                    if (mPageLoader != null && keyCode != 0) {\n                        mPageLoader.skipToNextPage();\n                    }\n                    return true;\n                }\n                if (keyCode == preferences.getInt(\"prevKeyCode\", 0)) {\n                    if (mPageLoader != null && keyCode != 0) {\n                        mPageLoader.skipToPrePage();\n                    }\n                    return true;\n                }\n                if (readBookControl.getCanKeyTurn(aloudStatus == ReadAloudService.Status.PLAY) && keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {\n                    if (mPageLoader != null) {\n                        mPageLoader.skipToNextPage();\n                    }\n                    return true;\n                } else if (readBookControl.getCanKeyTurn(aloudStatus == ReadAloudService.Status.PLAY) && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {\n                    if (mPageLoader != null) {\n                        mPageLoader.skipToPrePage();\n                    }\n                    return true;\n                } else if (keyCode == KeyEvent.KEYCODE_SPACE) {\n                    nextPage();\n                    return true;\n                }\n            }\n            return super.onKeyDown(keyCode, event);\n        }\n    }\n\n    @Override\n    public boolean onKeyUp(int keyCode, KeyEvent event) {\n        if (binding.flMenu.getVisibility() != View.VISIBLE) {\n            if (readBookControl.getCanKeyTurn(aloudStatus == ReadAloudService.Status.PLAY)\n                    && keyCode != 0\n                    && (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN\n                    || keyCode == KeyEvent.KEYCODE_VOLUME_UP\n                    || keyCode == preferences.getInt(\"nextKeyCode\", 0)\n                    || keyCode == preferences.getInt(\"prevKeyCode\", 0))) {\n                return true;\n            }\n        }\n        return super.onKeyUp(keyCode, event);\n    }\n\n    /**\n     * 更新菜单\n     */\n    @Override\n    public void upMenu() {\n        if (menu == null) return;\n        boolean onLine = mPresenter.getBookShelf() != null && !mPresenter.getBookShelf().getTag().equals(BookShelfBean.LOCAL_TAG);\n        if (onLine) {\n            binding.tvChapterUrl.setVisibility(View.VISIBLE);\n            binding.atvLine.setVisibility(View.VISIBLE);\n        } else {\n            binding.tvChapterUrl.setVisibility(View.GONE);\n            binding.atvLine.setVisibility(View.GONE);\n        }\n        for (int i = 0; i < menu.size(); i++) {\n            int groupId = menu.getItem(i).getGroupId();\n            if (groupId == R.id.menuOnLine) {\n                menu.getItem(i).setVisible(onLine);\n                menu.getItem(i).setEnabled(onLine);\n            } else if (groupId == R.id.menuLocal) {\n                menu.getItem(i).setVisible(!onLine);\n                menu.getItem(i).setEnabled(!onLine);\n            } else if (groupId == R.id.menu_text) {\n                boolean isTxt = mPresenter.getBookShelf() != null && mPresenter.getBookShelf().getNoteUrl().toLowerCase().endsWith(\".txt\");\n                menu.getItem(i).setVisible(isTxt);\n                menu.getItem(i).setEnabled(isTxt);\n            }\n            if (menu.getItem(i).getItemId() == R.id.enable_replace) {\n                menu.getItem(i).setChecked(mPresenter.getBookShelf() != null && mPresenter.getBookShelf().getReplaceEnable());\n            }\n        }\n\n    }\n\n    /**\n     * 更新音频长度\n     */\n    @Override\n    public void upAudioSize(int audioSize) {\n        binding.mediaPlayerPop.upAudioSize(audioSize);\n    }\n\n    /**\n     * 更新播放进度\n     */\n    @Override\n    public void upAudioDur(int audioDur) {\n        binding.mediaPlayerPop.upAudioDur(audioDur);\n        mPresenter.getBookShelf().setDurChapterPage(audioDur);\n        mPresenter.saveProgress();\n    }\n\n    @Override\n    public String getNoteUrl() {\n        return noteUrl;\n    }\n\n    @Override\n    public Boolean getAdd() {\n        return isAdd;\n    }\n\n    @Override\n    public void setAdd(Boolean isAdd) {\n        this.isAdd = isAdd;\n    }\n\n    @Override\n    public void openBookFromOther() {\n        new PermissionsCompat.Builder(this)\n                .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                .rationale(R.string.please_grant_storage_permission)\n                .onGranted((requestCode) -> {\n                    mPresenter.openBookFromOther(ReadBookActivity.this);\n                    return Unit.INSTANCE;\n                })\n                .request();\n    }\n\n    /**\n     * 朗读按钮\n     */\n    @Override\n    public void onMediaButton(String cmd) {\n        if (!ReadAloudService.running) {\n            aloudStatus = ReadAloudService.Status.STOP;\n            SystemUtil.ignoreBatteryOptimization(this);\n        }\n        switch (aloudStatus) {\n            case PAUSE:\n                switch (cmd) {\n                    case ReadAloudService.ActionMediaPlay:\n                        ReadAloudService.resume(this);\n                        binding.readMenuBottom.setFabReadAloudText(getString(R.string.read_aloud));\n                        break;\n                    case ReadAloudService.ActionMediaPrev:\n                        //停止倒计时\n                        ReadAloudService.setTimer(getContext(), ReadAloudService.maxTimeMinute + 1);\n                        //语音提示倒计时结束\n                        ReadAloudService.tts_ui_timer_stop(this);\n                        break;\n                    case ReadAloudService.ActionMediaNext:\n                        //翻到上一章并开始朗读\n                        if (mPageLoader != null) {\n                            mPageLoader.skipPreChapter();\n                        }\n                        ReadAloudService.resume(this);\n                        binding.readMenuBottom.setFabReadAloudText(getString(R.string.read_aloud));\n                        break;\n                }\n                break;\n            case PLAY:\n                switch (cmd) {\n                    case ReadAloudService.ActionMediaPlay:\n                        ReadAloudService.pause(this);\n                        binding.readMenuBottom.setFabReadAloudText(getString(R.string.read_aloud_pause));\n                        break;\n                    case ReadAloudService.ActionMediaPrev:\n                        //倒计时增加\n                        ReadAloudService.setTimer(getContext(), 10);\n                        //语音提示剩余时间\n                        ReadAloudService.tts_ui_timer_remaining(this);\n                        break;\n                    case ReadAloudService.ActionMediaNext:\n                        //翻到下一章\n                        if (mPageLoader != null) {\n                            mPageLoader.skipNextChapter();\n                        }\n                        break;\n                }\n                break;\n            default:\n                ReadBookActivity.this.popMenuOut();\n                readAloud();\n        }\n    }\n\n    public void selectFontDir() {\n        try {\n            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);\n            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n            //noinspection deprecation\n            startActivityForResult(intent, fontDirRequest);\n        } catch (Exception e) {\n            e.printStackTrace();\n            toast(e.getLocalizedMessage());\n        }\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (requestCode == fontDirRequest && resultCode == RESULT_OK) {\n            if (data != null) {\n                Uri uri = data.getData();\n                if (uri != null) {\n                    int modeFlags =\n                            Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;\n                    getContentResolver().takePersistableUriPermission(uri, modeFlags);\n                    binding.readInterfacePop.showFontSelector(uri);\n                }\n            }\n        }\n        initImmersionBar();\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    @Override\n    protected void onResume() {\n        super.onResume();\n        SoftInputUtil.hideIMM(getCurrentFocus());\n        if (batInfoReceiver == null) {\n            batInfoReceiver = new ThisBatInfoReceiver();\n            batInfoReceiver.registerThis();\n        }\n        screenOffTimerStart();\n        if (mPageLoader != null) {\n            if (!mPageLoader.updateBattery(BatteryUtil.getLevel(this))) {\n                mPageLoader.updateTime();\n            }\n        }\n    }\n\n    @Override\n    protected void onPause() {\n        super.onPause();\n        if (batInfoReceiver != null) {\n            batInfoReceiver.unregisterThis();\n        }\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        if (batInfoReceiver != null) {\n            batInfoReceiver.unregisterThis();\n        }\n        ReadAloudService.stop(this);\n        if (mPageLoader != null) {\n            mPageLoader.closeBook();\n            mPageLoader = null;\n        }\n    }\n\n    /**\n     * 结束\n     */\n    @Override\n    public void finish() {\n        if (!checkAddShelf()) {\n            return;\n        }\n        if (!AppActivityManager.getInstance().isExist(MainActivity.class)) {\n            Intent intent = new Intent(this, MainActivity.class);\n            startActivity(intent);\n        }\n        Backup.INSTANCE.autoBack();\n        super.finish();\n    }\n\n    @Override\n    public void changeSourceFinish(BookShelfBean book) {\n        if (mPageLoader != null && mPageLoader instanceof PageLoaderNet) {\n            ((PageLoaderNet) mPageLoader).changeSourceFinish(book);\n        }\n    }\n\n    /**\n     * 时间和电量广播\n     */\n    class ThisBatInfoReceiver extends BroadcastReceiver {\n        @SuppressLint(\"DefaultLocale\")\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            if (readBookControl.getHideStatusBar()) {\n                if (Intent.ACTION_TIME_TICK.equals(intent.getAction())) {\n                    if (mPageLoader != null) {\n                        mPageLoader.updateTime();\n                    }\n                } else if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {\n                    int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);\n                    if (mPageLoader != null) {\n                        mPageLoader.updateBattery(level);\n                    }\n                }\n            }\n        }\n\n        public void registerThis() {\n            IntentFilter filter = new IntentFilter();\n            filter.addAction(Intent.ACTION_TIME_TICK);\n            filter.addAction(Intent.ACTION_BATTERY_CHANGED);\n            ReadBookActivity.this.registerReceiver(batInfoReceiver, filter);\n        }\n\n        public void unregisterThis() {\n            ReadBookActivity.this.unregisterReceiver(batInfoReceiver);\n            batInfoReceiver = null;\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ReadStyleActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.res.AssetManager;\nimport android.content.res.Resources;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Color;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.os.Bundle;\nimport android.util.DisplayMetrics;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.BaseAdapter;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.ActionBar;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.jaredrummler.android.colorpicker.ColorPickerDialog;\nimport com.jaredrummler.android.colorpicker.ColorPickerDialogListener;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.ActivityReadStyleBinding;\nimport com.kunfei.bookshelf.help.ReadBookControl;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.utils.ActivityExtensionsKt;\nimport com.kunfei.bookshelf.utils.BitmapUtil;\nimport com.kunfei.bookshelf.utils.ContextExtensionsKt;\nimport com.kunfei.bookshelf.utils.MeUtils;\nimport com.kunfei.bookshelf.utils.RealPathUtil;\nimport com.kunfei.bookshelf.widget.filepicker.picker.FilePicker;\n\nimport java.io.IOException;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport kotlin.Unit;\n\npublic class ReadStyleActivity extends MBaseActivity<IPresenter> implements ColorPickerDialogListener {\n    private final int ResultSelectBg = 103;\n    private final int SELECT_TEXT_COLOR = 201;\n    private final int SELECT_BG_COLOR = 301;\n\n    private ActivityReadStyleBinding binding;\n    private ReadBookControl readBookControl = ReadBookControl.getInstance();\n    private int textDrawableIndex;\n    private int textColor;\n    private int bgColor;\n    private Drawable bgDrawable;\n    private int bgCustom;\n    private boolean darkStatusIcon;\n    private String bgPath;\n    private BgImgListAdapter bgImgListAdapter;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    /**\n     * 布局载入  setContentView()\n     */\n    @Override\n    protected void onCreateActivity() {\n        binding = ActivityReadStyleBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        binding.llContent.setPadding(0, ContextExtensionsKt.getStatusBarHeight(this), 0, 0);\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        setTextKind(readBookControl);\n    }\n\n    @Override\n    protected void initImmersionBar() {\n        super.initImmersionBar();\n        ActivityExtensionsKt.setLightStatusBar(this, darkStatusIcon);\n    }\n\n    /**\n     * 数据初始化\n     */\n    @Override\n    protected void initData() {\n        Intent intent = getIntent();\n        textDrawableIndex = intent.getIntExtra(\"index\", 1);\n        bgCustom = readBookControl.getBgCustom(textDrawableIndex);\n        textColor = readBookControl.getTextColor(textDrawableIndex);\n        Resources resources = this.getResources();\n        DisplayMetrics dm = resources.getDisplayMetrics();\n        int width = dm.widthPixels;\n        int height = dm.heightPixels;\n        bgDrawable = readBookControl.getBgDrawable(textDrawableIndex, getContext(), width, height);\n        bgColor = readBookControl.getBgColor(textDrawableIndex);\n        darkStatusIcon = readBookControl.getDarkStatusIcon(textDrawableIndex);\n        bgPath = readBookControl.getBgPath(textDrawableIndex);\n        upText();\n        upBg();\n    }\n\n    /**\n     * 事件触发绑定\n     */\n    @Override\n    protected void bindEvent() {\n        binding.swDarkStatusIcon.setChecked(darkStatusIcon);\n        binding.swDarkStatusIcon.setOnCheckedChangeListener((compoundButton, b) -> {\n            darkStatusIcon = b;\n            initImmersionBar();\n        });\n        //文字背景点击事件\n        binding.llContent.setOnClickListener((view) -> {\n            if (binding.llBottom.getVisibility() == View.GONE) {\n                binding.llBottom.setVisibility(View.VISIBLE);\n            } else {\n                binding.llBottom.setVisibility(View.GONE);\n            }\n        });\n        //选择文字颜色\n        binding.tvSelectTextColor.setOnClickListener(view ->\n                ColorPickerDialog.newBuilder()\n                        .setColor(textColor)\n                        .setShowAlphaSlider(false)\n                        .setDialogType(ColorPickerDialog.TYPE_CUSTOM)\n                        .setDialogId(SELECT_TEXT_COLOR)\n                        .show(ReadStyleActivity.this));\n        //选择背景颜色\n        binding.tvSelectBgColor.setOnClickListener(view ->\n                ColorPickerDialog.newBuilder()\n                        .setColor(bgColor)\n                        .setShowAlphaSlider(false)\n                        .setDialogType(ColorPickerDialog.TYPE_CUSTOM)\n                        .setDialogId(SELECT_BG_COLOR)\n                        .show(ReadStyleActivity.this));\n\n        //背景图列表\n        bgImgListAdapter = new BgImgListAdapter(this);\n        bgImgListAdapter.initList();\n        binding.bgImgList.setAdapter(bgImgListAdapter);\n        binding.bgImgList.setOnItemClickListener((adapterView, view, i, l) -> {\n            if (i == 0) {\n                selectImage();\n            } else {\n                bgPath = bgImgListAdapter.getItemAssetsFile(i - 1);\n                setAssetsBg(bgPath);\n            }\n        });\n\n        //选择背景图片\n        binding.tvSelectBgImage.setOnClickListener(view -> selectImage());\n\n        //恢复默认\n        binding.tvDefault.setOnClickListener(view -> {\n            bgCustom = 0;\n            textColor = readBookControl.getDefaultTextColor(textDrawableIndex);\n            bgDrawable = readBookControl.getDefaultBgDrawable(textDrawableIndex, this);\n            upText();\n            upBg();\n        });\n    }\n\n    private void selectImage() {\n        new PermissionsCompat.Builder(this)\n                .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                .rationale(R.string.bg_image_per)\n                .onGranted((requestCode) -> {\n                    selectImageDialog();\n                    return Unit.INSTANCE;\n                })\n                .request();\n    }\n\n    private void selectImageDialog() {\n        try {\n            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);\n            intent.addCategory(Intent.CATEGORY_OPENABLE);\n            intent.setType(\"image/*\");\n            startActivityForResult(intent, ResultSelectBg);\n        } catch (Exception e) {\n            FilePicker picker = new FilePicker(this, FilePicker.FILE);\n            picker.setBackgroundColor(getResources().getColor(R.color.background));\n            picker.setTopBackgroundColor(getResources().getColor(R.color.background));\n            picker.setItemHeight(30);\n            picker.setOnFilePickListener(this::setCustomBg);\n            picker.show();\n        }\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.read_style);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_read_style_activity, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_save) {\n            saveStyle();\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    /**\n     * 保存配置\n     */\n    private void saveStyle() {\n        readBookControl.setTextColor(textDrawableIndex, textColor);\n        readBookControl.setBgCustom(textDrawableIndex, bgCustom);\n        readBookControl.setBgColor(textDrawableIndex, bgColor);\n        readBookControl.setDarkStatusIcon(textDrawableIndex, darkStatusIcon);\n        if (bgCustom == 2 || bgCustom == 3) {\n            readBookControl.setBgPath(textDrawableIndex, bgPath);\n        }\n        readBookControl.initTextDrawableIndex();\n        RxBus.get().post(RxBusTag.UPDATE_READ, false);\n        finish();\n    }\n\n    private void setTextKind(ReadBookControl readBookControl) {\n        binding.tvContent.setTextSize(readBookControl.getTextSize());\n    }\n\n    private void upText() {\n        binding.tvContent.setTextColor(textColor);\n    }\n\n    private void upBg() {\n        binding.llContent.setBackground(bgDrawable);\n    }\n\n    /**\n     * 自定义背景\n     */\n    public void setCustomBg(String bgPath) {\n        try {\n            Resources resources = this.getResources();\n            DisplayMetrics dm = resources.getDisplayMetrics();\n            int width = dm.widthPixels;\n            int height = dm.heightPixels;\n            Bitmap bitmap = BitmapUtil.getFitSampleBitmap(bgPath, width, height);\n            bgCustom = 2;\n            bgDrawable = new BitmapDrawable(getResources(), bitmap);\n            upBg();\n        } catch (Exception e) {\n            e.printStackTrace();\n            toast(e.getMessage(), ERROR);\n        }\n    }\n\n    public void setAssetsBg(String path) {\n        try {\n            Resources resources = ReadStyleActivity.this.getResources();\n            DisplayMetrics dm = resources.getDisplayMetrics();\n            int width = dm.widthPixels;\n            int height = dm.heightPixels;\n\n            Bitmap bitmap = MeUtils.getFitAssetsSampleBitmap(ReadStyleActivity.this.getAssets(), path, width, height);\n            bgCustom = 3;\n            bgDrawable = new BitmapDrawable(getResources(), bitmap);\n            upBg();\n        } catch (Exception e) {\n            e.printStackTrace();\n            toast(e.getMessage(), ERROR);\n        }\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (requestCode == ResultSelectBg) {\n            if (resultCode == RESULT_OK && null != data) {\n                try {\n                    bgPath = RealPathUtil.getPath(this, data.getData());\n                    setCustomBg(bgPath);\n                } catch (Exception ignored) {\n                }\n            }\n        }\n    }\n\n    /**\n     * Callback that is invoked when a color is selected from the color picker dialog.\n     * @param dialogId The dialog id used to create the dialog instance.\n     * @param color    The selected color\n     */\n    @Override\n    public void onColorSelected(int dialogId, int color) {\n        switch (dialogId) {\n            case SELECT_TEXT_COLOR:\n                textColor = color;\n                upText();\n                break;\n            case SELECT_BG_COLOR:\n                bgCustom = 1;\n                bgColor = color;\n                bgDrawable = new ColorDrawable(bgColor);\n                upBg();\n        }\n    }\n\n    /**\n     * Callback that is invoked when the color picker dialog was dismissed.\n     * @param dialogId The dialog id used to create the dialog instance.\n     */\n    @Override\n    public void onDialogDismissed(int dialogId) {\n\n    }\n\n    private static class BgImgListAdapter extends BaseAdapter {\n        private final Context context;\n        private final LayoutInflater mInflater;\n        private List<String> assetsFiles;\n        final BitmapFactory.Options options = new BitmapFactory.Options();\n\n        BgImgListAdapter(Context context) {\n            this.context = context;\n            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);\n\n            options.inJustDecodeBounds = false;\n            options.inSampleSize = 4;\n        }\n\n        void initList() {\n            AssetManager am = context.getAssets();\n            String[] path;\n            try {\n                path = am.list(\"bg\");  //获取所有,填入目录获取该目录下所有资源\n            } catch (IOException e) {\n                e.printStackTrace();\n                return;\n            }\n\n            assetsFiles = new ArrayList<>();\n            Collections.addAll(assetsFiles, path);\n        }\n\n        @Override\n        public int getCount() {\n            return assetsFiles.size() + 1;\n        }\n\n        @Override\n        public Object getItem(int position) {\n            return position;\n        }\n\n        @Override\n        public long getItemId(int position) {\n            return position;\n        }\n\n        String getItemAssetsFile(int position) {\n            return \"bg/\" + assetsFiles.get(position);\n        }\n\n        @SuppressLint(\"InflateParams\")\n        @Override\n        public View getView(int position, View convertView, ViewGroup parent) {\n            ViewHolder holder;\n            if(convertView==null){\n                holder = new ViewHolder();\n                convertView = mInflater.inflate(R.layout.item_read_bg, null);\n                holder.mImage = convertView.findViewById(R.id.iv_bg);\n                holder.mTitle = convertView.findViewById(R.id.tv_desc);\n                convertView.setTag(holder);\n            } else {\n                holder = (ViewHolder)convertView.getTag();\n            }\n            if (position == 0) {\n                holder.mTitle.setText(\"选择背景\");\n                holder.mTitle.setTextColor(Color.parseColor(\"#101010\"));\n                holder.mImage.setImageBitmap(BitmapFactory.decodeResource(context.getResources(), R.drawable.icon_image));\n            } else {\n                String path = assetsFiles.get(position - 1);\n                holder.mTitle.setText(getFileName(path));\n                holder.mTitle.setTextColor(Color.parseColor(\"#909090\"));\n                try {\n                    BitmapDrawable bitmapDrawable = (BitmapDrawable) holder.mImage.getDrawable();\n                    //如果图片还未回收，先强制回收该图片\n                    if (bitmapDrawable != null && !bitmapDrawable.getBitmap().isRecycled()) {\n                        bitmapDrawable.getBitmap().recycle();\n                    }\n                    //该变现实的图片\n                    Bitmap bmp = MeUtils.getFitAssetsSampleBitmap(context.getAssets(), getItemAssetsFile(position - 1), 256, 256);\n                    holder.mImage.setImageBitmap(bmp);\n                } catch (Exception e) {\n                    e.printStackTrace();\n                    holder.mImage.setImageBitmap(null);\n                }\n            }\n            return convertView;\n        }\n\n        String getFileName(String path) {\n            int start = path.lastIndexOf(\"/\");\n            int end = path.lastIndexOf(\".\");\n            if (end < 0) end = path.length();\n            return path.substring(start + 1, end);\n        }\n\n        private static class ViewHolder {\n            private TextView mTitle ;\n            private ImageView mImage;\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ReceivingSharedActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Intent;\nimport android.os.Build;\nimport android.os.Bundle;\n\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\npublic class ReceivingSharedActivity extends AppCompatActivity {\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        String action = getIntent().getAction();\n        String type = getIntent().getType();\n\n        if (Intent.ACTION_SEND.equals(action) && type != null) {\n            if (\"text/plain\".equals(type)) {\n                String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);\n                if (openUrl(text)) {\n                    SearchBookActivity.startByKey(this, text);\n                }\n                finish();\n                return;\n            }\n        }\n        if (Intent.ACTION_PROCESS_TEXT.equals(action) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && type != null) {\n            if (\"text/plain\".equals(type)) {\n                String text = getIntent().getStringExtra(Intent.EXTRA_PROCESS_TEXT);\n                if (openUrl(text)) {\n                    SearchBookActivity.startByKey(this, text);\n                }\n                finish();\n                return;\n            }\n        }\n        finish();\n    }\n\n    private boolean openUrl(String text) {\n        if (StringUtils.isTrimEmpty(text)) {\n            return false;\n        }\n        String[] urls = text.split(\"\\\\s\");\n        StringBuilder result = new StringBuilder();\n        for (String url : urls) {\n            if (url.matches(\"http.+\"))\n                result.append(\"\\n\").append(url.trim());\n        }\n        if (result.length() > 1) {\n            MApplication.getConfigPreferences().edit()\n                    .putString(\"shared_url\", result.toString())\n                    .apply();\n\n            Intent intent = new Intent();\n            intent.setClass(ReceivingSharedActivity.this, MainActivity.class);\n            this.startActivity(intent);\n            return false;\n        } else {\n            return true;\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ReplaceRuleActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.widget.LinearLayout;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.recyclerview.widget.DividerItemDecoration;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.ActivityRecyclerVewBinding;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.model.ReplaceRuleManager;\nimport com.kunfei.bookshelf.presenter.ReplaceRulePresenter;\nimport com.kunfei.bookshelf.presenter.contract.ReplaceRuleContract;\nimport com.kunfei.bookshelf.utils.ACache;\nimport com.kunfei.bookshelf.utils.RealPathUtil;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.ReplaceRuleAdapter;\nimport com.kunfei.bookshelf.widget.filepicker.picker.FilePicker;\nimport com.kunfei.bookshelf.widget.modialog.InputDialog;\nimport com.kunfei.bookshelf.widget.modialog.MoDialogHUD;\nimport com.kunfei.bookshelf.widget.modialog.ReplaceRuleDialog;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport kotlin.Unit;\n\n/**\n * Created by GKF on 2017/12/16.\n * 书源管理\n */\n\npublic class ReplaceRuleActivity extends MBaseActivity<ReplaceRuleContract.Presenter> implements ReplaceRuleContract.View {\n    private final int IMPORT_SOURCE = 102;\n\n    private ActivityRecyclerVewBinding binding;\n    private BookShelfBean bookShelfBean;\n    private MoDialogHUD moDialogHUD;\n    private ReplaceRuleAdapter adapter;\n    private boolean selectAll = true;\n\n    public static void startThis(Context context, BookShelfBean shelfBean) {\n        String key = String.valueOf(System.currentTimeMillis());\n        Intent intent = new Intent(context, ReplaceRuleActivity.class);\n        BitIntentDataManager.getInstance().putData(key, shelfBean);\n        intent.putExtra(\"data_key\", key);\n        context.startActivity(intent);\n    }\n\n    @Override\n    protected ReplaceRuleContract.Presenter initInjector() {\n        return new ReplaceRulePresenter();\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityRecyclerVewBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void initData() {\n        String dataKey = getIntent().getStringExtra(\"data_key\");\n        if (!TextUtils.isEmpty(dataKey)) {\n            bookShelfBean = (BookShelfBean) BitIntentDataManager.getInstance().getData(dataKey);\n        }\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        initRecyclerView();\n        moDialogHUD = new MoDialogHUD(this);\n        refresh();\n    }\n\n    private void initRecyclerView() {\n        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));\n        binding.recyclerView.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));\n        adapter = new ReplaceRuleAdapter(this);\n        binding.recyclerView.setAdapter(adapter);\n        ItemTouchCallback itemTouchCallback = new ItemTouchCallback();\n        itemTouchCallback.setOnItemTouchCallbackListener(adapter.getItemTouchCallbackListener());\n        itemTouchCallback.setDragEnable(true);\n        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback);\n        itemTouchHelper.attachToRecyclerView(binding.recyclerView);\n    }\n\n    public void editReplaceRule(ReplaceRuleBean replaceRuleBean) {\n        ReplaceRuleDialog.builder(this, replaceRuleBean, bookShelfBean)\n                .setPositiveButton(replaceRuleBean1 ->\n                        ReplaceRuleManager.saveData(replaceRuleBean1)\n                                .subscribe(new MySingleObserver<Boolean>() {\n                                    @Override\n                                    public void onSuccess(Boolean aBoolean) {\n                                        refresh();\n                                    }\n                                })).show();\n    }\n\n    public void upDateSelectAll() {\n        selectAll = true;\n        for (ReplaceRuleBean replaceRuleBean : adapter.getData()) {\n            if (replaceRuleBean.getEnable() == null || !replaceRuleBean.getEnable()) {\n                selectAll = false;\n                break;\n            }\n        }\n    }\n\n    private void selectAllDataS() {\n        for (ReplaceRuleBean replaceRuleBean : adapter.getData()) {\n            replaceRuleBean.setEnable(!selectAll);\n        }\n        adapter.notifyDataSetChanged();\n        selectAll = !selectAll;\n        ReplaceRuleManager.addDataS(adapter.getData());\n    }\n\n    public void delData(ReplaceRuleBean replaceRuleBean) {\n        mPresenter.delData(replaceRuleBean);\n    }\n\n    public void saveDataS() {\n        mPresenter.saveData(adapter.getData());\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.replace_rule_title);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_replace_rule_activity, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_add_replace_rule) {\n            editReplaceRule(null);\n        } else if (id == R.id.action_select_all) {\n            selectAllDataS();\n        } else if (id == R.id.action_import) {\n            selectReplaceRuleFile();\n        } else if (id == R.id.action_import_onLine) {\n            String cacheUrl = ACache.get(this).getAsString(\"replaceUrl\");\n            String[] cacheUrls = cacheUrl == null ? new String[]{} : cacheUrl.split(\";\");\n            List<String> urlList = new ArrayList<>(Arrays.asList(cacheUrls));\n            InputDialog.builder(this)\n                    .setTitle(getString(R.string.input_replace_url))\n                    .setDefaultValue(cacheUrl)\n                    .setAdapterValues(urlList)\n                    .setCallback(new InputDialog.Callback() {\n                        @Override\n                        public void setInputText(String inputText) {\n                            inputText = StringUtils.trim(inputText);\n                            if (!urlList.contains(inputText)) {\n                                urlList.add(0, inputText);\n                                ACache.get(ReplaceRuleActivity.this).put(\"replaceUrl\", TextUtils.join(\";\", urlList));\n                            }\n                            mPresenter.importDataS(inputText);\n                        }\n\n                        @Override\n                        public void delete(String value) {\n\n                        }\n                    }).show();\n        } else if (id == R.id.action_del_all) {\n            mPresenter.delData(adapter.getData());\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    private void selectReplaceRuleFile() {\n        new PermissionsCompat.Builder(this)\n                .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                .rationale(R.string.get_storage_per)\n                .onGranted((requestCode) -> {\n                    FilePicker filePicker = new FilePicker(ReplaceRuleActivity.this, FilePicker.FILE);\n                    filePicker.setBackgroundColor(getResources().getColor(R.color.background));\n                    filePicker.setTopBackgroundColor(getResources().getColor(R.color.background));\n                    filePicker.setItemHeight(30);\n                    filePicker.setAllowExtensions(getResources().getStringArray(R.array.text_suffix));\n                    filePicker.setOnFilePickListener(s -> mPresenter.importDataSLocal(s));\n                    filePicker.show();\n                    filePicker.getSubmitButton().setText(R.string.sys_file_picker);\n                    filePicker.getSubmitButton().setOnClickListener(view -> {\n                        filePicker.dismiss();\n                        selectFileSys();\n                    });\n                    return Unit.INSTANCE;\n                })\n                .request();\n    }\n\n    private void selectFileSys() {\n        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);\n        intent.addCategory(Intent.CATEGORY_OPENABLE);\n        intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{\"text/*\", \"application/json\"});\n        intent.setType(\"*/*\");//设置类型\n        startActivityForResult(intent, IMPORT_SOURCE);\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent event) {\n        Boolean mo = moDialogHUD.onKeyDown(keyCode, event);\n        if (mo) {\n            return true;\n        } else {\n            if (keyCode == KeyEvent.KEYCODE_BACK) {\n                finish();\n                return true;\n            }\n            return super.onKeyDown(keyCode, event);\n        }\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (requestCode == IMPORT_SOURCE) {\n            if (data != null) {\n                mPresenter.importDataSLocal(RealPathUtil.getPath(this, data.getData()));\n            }\n        }\n    }\n\n    @Override\n    public void refresh() {\n        ReplaceRuleManager.getAll()\n                .subscribe(new MySingleObserver<List<ReplaceRuleBean>>() {\n                    @Override\n                    public void onSuccess(List<ReplaceRuleBean> replaceRuleBeans) {\n                        adapter.resetDataS(replaceRuleBeans);\n                    }\n                });\n    }\n\n    @Override\n    protected void onDestroy() {\n        RxBus.get().post(RxBusTag.UPDATE_READ, false);\n        super.onDestroy();\n    }\n\n    @Override\n    public Snackbar getSnackBar(String msg, int length) {\n        return Snackbar.make(binding.llContent, msg, length);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/SearchBookActivity.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.activity;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.text.TextUtils;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.google.android.flexbox.FlexboxLayoutManager;\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.bean.SearchHistoryBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.ActivitySearchBookBinding;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.presenter.BookDetailPresenter;\nimport com.kunfei.bookshelf.presenter.SearchBookPresenter;\nimport com.kunfei.bookshelf.presenter.contract.SearchBookContract;\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.Selector;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.SearchBookAdapter;\nimport com.kunfei.bookshelf.view.adapter.SearchBookshelfAdapter;\nimport com.kunfei.bookshelf.widget.explosion_field.ExplosionField;\nimport com.kunfei.bookshelf.widget.recycler.refresh.OnLoadMoreListener;\n\nimport java.util.List;\nimport java.util.Objects;\n\npublic class SearchBookActivity extends MBaseActivity<SearchBookContract.Presenter>\n        implements SearchBookContract.View, SearchBookshelfAdapter.CallBack {\n    private final int requestSource = 14;\n\n    private ActivitySearchBookBinding binding;\n    private View refreshErrorView;\n    private ExplosionField mExplosionField;\n    private SearchBookAdapter searchBookAdapter;\n    private SearchView.SearchAutoComplete mSearchAutoComplete;\n    private boolean showHistory;\n    private String searchKey;\n    private Menu menu;\n    private SearchBookshelfAdapter searchBookshelfAdapter;\n\n    public static void startByKey(Context context, String searchKey) {\n        Intent intent = new Intent(context, SearchBookActivity.class);\n        intent.putExtra(\"searchKey\", searchKey);\n        context.startActivity(intent);\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected SearchBookContract.Presenter initInjector() {\n        return new SearchBookPresenter();\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivitySearchBookBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void initData() {\n        mExplosionField = ExplosionField.attach2Window(this);\n        searchBookAdapter = new SearchBookAdapter(this);\n        searchBookshelfAdapter = new SearchBookshelfAdapter(this);\n    }\n\n    @SuppressLint(\"InflateParams\")\n    @Override\n    protected void bindView() {\n        binding.cardSearch.setCardBackgroundColor(ThemeStore.primaryColorDark(this));\n        initSearchView();\n        setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        binding.fabSearchStop.hide();\n        binding.fabSearchStop.setBackgroundTintList(Selector.colorBuild()\n                .setDefaultColor(ThemeStore.accentColor(this))\n                .setPressedColor(ColorUtils.darkenColor(ThemeStore.accentColor(this)))\n                .create());\n        binding.llSearchHistory.setOnClickListener(null);\n        binding.rfRvSearchBooks.setRefreshRecyclerViewAdapter(searchBookAdapter, new LinearLayoutManager(this));\n        refreshErrorView = LayoutInflater.from(this).inflate(R.layout.view_refresh_error, null);\n        refreshErrorView.findViewById(R.id.tv_refresh_again).setOnClickListener(v -> {\n            //刷新失败 ，重试\n            toSearch();\n        });\n        binding.rfRvSearchBooks.setNoDataAndRefreshErrorView(LayoutInflater.from(this).inflate(R.layout.view_refresh_no_data, null),\n                refreshErrorView);\n\n        searchBookAdapter.setItemClickListener((view, position) -> {\n            String dataKey = String.valueOf(System.currentTimeMillis());\n            Intent intent = new Intent(SearchBookActivity.this, BookDetailActivity.class);\n            intent.putExtra(\"openFrom\", BookDetailPresenter.FROM_SEARCH);\n            intent.putExtra(\"data_key\", dataKey);\n            BitIntentDataManager.getInstance().putData(dataKey, searchBookAdapter.getItemData(position));\n            startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out);\n        });\n\n        binding.fabSearchStop.setOnClickListener(view -> {\n            binding.fabSearchStop.hide();\n            mPresenter.stopSearch();\n        });\n        binding.rvBookshelf.setLayoutManager(new FlexboxLayoutManager(this));\n        binding.rvBookshelf.setAdapter(searchBookshelfAdapter);\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.action_search);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_book_search_activity, menu);\n        this.menu = menu;\n        initMenu();\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_book_source_manage) {\n            BookSourceActivity.startThis(this, requestSource);\n        } else if (id == android.R.id.home) {\n            SoftInputUtil.hideIMM(getCurrentFocus());\n            finish();\n        } else {\n            if (item.getGroupId() == R.id.source_group) {\n                item.setChecked(true);\n                if (Objects.equals(getString(R.string.all_source), item.getTitle().toString())) {\n                    MApplication.SEARCH_GROUP = null;\n                } else {\n                    MApplication.SEARCH_GROUP = item.getTitle().toString();\n                }\n                mPresenter.initSearchEngineS(MApplication.SEARCH_GROUP);\n            }\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    private void initSearchView() {\n        mSearchAutoComplete = binding.searchView.findViewById(R.id.search_src_text);\n        binding.searchView.setQueryHint(getString(R.string.search_book_key));\n        //获取到TextView的控件\n        mSearchAutoComplete.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);\n        mSearchAutoComplete.setPadding(15, 0, 0, 0);\n        binding.searchView.onActionViewExpanded();\n        LinearLayout editFrame = binding.searchView.findViewById(R.id.search_edit_frame);\n        ImageView closeButton = binding.searchView.findViewById(R.id.search_close_btn);\n        ImageView goButton = binding.searchView.findViewById(R.id.search_go_btn);\n        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) editFrame.getLayoutParams();\n        params.setMargins(20, 0, 10, 0);\n        editFrame.setLayoutParams(params);\n        closeButton.setScaleX(0.9f);\n        closeButton.setScaleY(0.9f);\n        closeButton.setPadding(0, 0, 0, 0);\n        goButton.setPadding(0, 0, 0, 0);\n        binding.searchView.setSubmitButtonEnabled(true);\n        binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                if (TextUtils.isEmpty(query))\n                    return false;\n                searchKey = query.trim();\n                if (!searchKey.toLowerCase().startsWith(\"set:\")) {\n                    toSearch();\n                    binding.searchView.clearFocus();\n                } else {\n                    parseSecretCode(searchKey);\n                    finish();\n                }\n                return false;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                if (newText != null) {\n                    List<BookInfoBean> beans = BookshelfHelp.searchBookInfo(newText);\n                    searchBookshelfAdapter.setItems(beans);\n                    if (beans.size() > 0) {\n                        binding.tvBookshelf.setVisibility(View.VISIBLE);\n                        binding.rvBookshelf.setVisibility(View.VISIBLE);\n                    } else {\n                        binding.tvBookshelf.setVisibility(View.GONE);\n                        binding.rvBookshelf.setVisibility(View.GONE);\n                    }\n                } else {\n                    binding.tvBookshelf.setVisibility(View.GONE);\n                    binding.rvBookshelf.setVisibility(View.GONE);\n                }\n                if (!newText.toLowerCase().startsWith(\"set\")) {\n                    mPresenter.querySearchHistory(newText);\n                } else {\n                    showHideSetting();\n                }\n                return false;\n            }\n        });\n        binding.searchView.setOnQueryTextFocusChangeListener((view, b) -> {\n            showHistory = b;\n            if (!b && binding.searchView.getQuery().toString().trim().equals(\"\")) {\n                finish();\n            }\n            if (showHistory) {\n                binding.fabSearchStop.hide();\n                mPresenter.stopSearch();\n            }\n            openOrCloseHistory(showHistory);\n        });\n    }\n\n    @Override\n    protected void bindEvent() {\n        binding.tvSearchHistoryClean.setOnClickListener(v -> {\n            mExplosionField.explode(binding.tflSearchHistory, true);\n            mPresenter.cleanSearchHistory();\n        });\n\n        binding.rfRvSearchBooks.setLoadMoreListener(new OnLoadMoreListener() {\n            @Override\n            public void startLoadMore() {\n                binding.fabSearchStop.show();\n                mPresenter.toSearchBooks(null, false);\n            }\n\n            @Override\n            public void loadMoreErrorTryAgain() {\n                binding.fabSearchStop.show();\n                mPresenter.toSearchBooks(null, true);\n            }\n        });\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        Intent intent = this.getIntent();\n        searchBook(intent.getStringExtra(\"searchKey\"));\n    }\n\n    @Override\n    public void onPause() {\n        super.onPause();\n        showHistory = binding.llSearchHistory.getVisibility() == View.VISIBLE;\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        openOrCloseHistory(showHistory);\n    }\n\n    @Override\n    public void searchBook(String searchKey) {\n        if (!TextUtils.isEmpty(searchKey)) {\n            binding.searchView.setQuery(searchKey, true);\n            showHistory = false;\n        } else {\n            showHistory = true;\n            mPresenter.querySearchHistory(\"\");\n        }\n        openOrCloseHistory(showHistory);\n    }\n\n    private void initMenu() {\n        if (menu == null) return;\n        menu.removeGroup(R.id.source_group);\n        menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source);\n        List<String> groupList = BookSourceManager.getEnableGroupList();\n        for (String groupName : groupList) {\n            menu.add(R.id.source_group, Menu.NONE, Menu.NONE, groupName);\n        }\n        menu.setGroupCheckable(R.id.source_group, true, true);\n        if (MApplication.SEARCH_GROUP != null) {\n            boolean hasGroup = false;\n            for (int i = 0; i < menu.size(); i++) {\n                if (menu.getItem(i).getTitle().toString().equals(MApplication.SEARCH_GROUP)) {\n                    menu.getItem(i).setChecked(true);\n                    hasGroup = true;\n                    break;\n                }\n            }\n            if (!hasGroup) {\n                menu.getItem(1).setChecked(true);\n            }\n        } else {\n            menu.getItem(1).setChecked(true);\n        }\n    }\n\n    private void showHideSetting() {\n        binding.tflSearchHistory.removeAllViews();\n        TextView tagView;\n        String[] hideSettings = {\"show_nav_shelves\", \"fade_tts\", \"use_regex_in_new_rule\", \"blur_sim_back\", \"async_draw\", \"disable_scroll_click_turn\"};\n\n        for (String text : hideSettings) {\n            tagView = (TextView) getLayoutInflater().inflate(R.layout.item_search_history, binding.tflSearchHistory, false);\n            tagView.setTag(text);\n            tagView.setText(text);\n            tagView.setOnClickListener(view -> {\n                String key = \"set:\" + view.getTag();\n                binding.searchView.setQuery(key, false);\n            });\n            binding.tflSearchHistory.addView(tagView);\n        }\n    }\n\n    private void parseSecretCode(String code) {\n        code = code.toLowerCase().replaceAll(\"^\\\\s*set:\", \"\").trim();\n        String[] param = code.split(\"\\\\s+\");\n        String msg = null;\n        boolean enable = param.length == 1 || !param[1].equals(\"false\");\n        switch (param[0]) {\n            case \"show_nav_shelves\":\n                MApplication.getConfigPreferences().edit().putBoolean(\"showNavShelves\", enable).apply();\n                msg = \"已\" + (enable ? \"启\" : \"禁\") + \"用侧边栏书架！\";\n                RxBus.get().post(RxBusTag.RECREATE, true);\n                break;\n            case \"fade_tts\":\n                MApplication.getConfigPreferences().edit().putBoolean(\"fadeTTS\", enable).apply();\n                msg = \"已\" + (enable ? \"启\" : \"禁\") + \"用朗读时淡入淡出！\";\n                break;\n            case \"use_regex_in_new_rule\":\n                MApplication.getConfigPreferences().edit().putBoolean(\"useRegexInNewRule\", enable).apply();\n                msg = \"已\" + (enable ? \"启\" : \"禁\") + \"用新建替换规则时默认使用正则表达式！\";\n                break;\n            case \"blur_sim_back\":\n                MApplication.getConfigPreferences().edit().putBoolean(\"blurSimBack\", enable).apply();\n                msg = \"已\" + (enable ? \"启\" : \"禁\") + \"用仿真翻页背景虚化！\";\n                break;\n            case \"async_draw\":\n                MApplication.getConfigPreferences().edit().putBoolean(\"asyncDraw\", enable).apply();\n                msg = \"已\" + (enable ? \"启\" : \"禁\") + \"用异步加载！\";\n                break;\n            case \"disable_scroll_click_turn\":\n                MApplication.getConfigPreferences().edit().putBoolean(\"disableScrollClickTurn\", enable).apply();\n                msg = \"已\" + (enable ? \"禁\" : \"启\") + \"用滚动模式点击翻页！\";\n                break;\n        }\n        if (msg == null) {\n            toast(\"无法识别设置密码: \" + code, 0, -1);\n        } else {\n            toast(msg, 0, 1);\n        }\n    }\n\n    /**\n     * 开始搜索\n     */\n    private void toSearch() {\n        if (!TextUtils.isEmpty(searchKey)) {\n            mPresenter.insertSearchHistory();\n            //执行搜索请求\n            new Handler().postDelayed(() -> {\n                mPresenter.initPage();\n                binding.rfRvSearchBooks.startRefresh();\n                binding.fabSearchStop.show();\n                mPresenter.toSearchBooks(searchKey, false);\n            }, 300);\n        }\n    }\n\n    private void openOrCloseHistory(Boolean open) {\n        if (open) {\n            if (binding.llSearchHistory.getVisibility() != View.VISIBLE) {\n                binding.llSearchHistory.setVisibility(View.VISIBLE);\n            }\n        } else {\n            if (binding.llSearchHistory.getVisibility() == View.VISIBLE) {\n                binding.llSearchHistory.setVisibility(View.GONE);\n            }\n        }\n    }\n\n    private void addNewHistories(List<SearchHistoryBean> historyBeans) {\n        binding.tflSearchHistory.removeAllViews();\n        if (historyBeans != null) {\n            TextView tagView;\n            for (SearchHistoryBean searchHistoryBean : historyBeans) {\n                tagView = (TextView) getLayoutInflater().inflate(R.layout.item_search_history, binding.tflSearchHistory, false);\n                tagView.setTag(searchHistoryBean);\n                tagView.setText(searchHistoryBean.getContent());\n                tagView.setOnClickListener(view -> {\n                    SearchHistoryBean historyBean = (SearchHistoryBean) view.getTag();\n                    List<BookInfoBean> beans = BookshelfHelp.searchBookInfo(historyBean.getContent());\n                    binding.searchView.setQuery(historyBean.getContent(), beans.isEmpty());\n                });\n                tagView.setOnLongClickListener(view -> {\n                    SearchHistoryBean historyBean = (SearchHistoryBean) view.getTag();\n                    mExplosionField.explode(view);\n                    view.setOnLongClickListener(null);\n                    mPresenter.cleanSearchHistory(historyBean);\n                    return true;\n                });\n                binding.tflSearchHistory.addView(tagView);\n            }\n        }\n    }\n\n    @Override\n    public void insertSearchHistorySuccess(SearchHistoryBean searchHistoryBean) {\n        //搜索历史插入或者修改成功\n        mPresenter.querySearchHistory(searchKey);\n    }\n\n    @Override\n    public void querySearchHistorySuccess(List<SearchHistoryBean> data) {\n        addNewHistories(data);\n        if (binding.tflSearchHistory.getChildCount() > 0) {\n            binding.tvSearchHistoryClean.setVisibility(View.VISIBLE);\n        } else {\n            binding.tvSearchHistoryClean.setVisibility(View.INVISIBLE);\n        }\n    }\n\n    @Override\n    public void refreshSearchBook() {\n        searchBookAdapter.upData(SearchBookAdapter.DataAction.CLEAR, null);\n    }\n\n    @Override\n    public void refreshFinish(Boolean isAll) {\n        binding.fabSearchStop.hide();\n        binding.rfRvSearchBooks.finishRefresh(isAll, true);\n    }\n\n    @Override\n    public void loadMoreFinish(Boolean isAll) {\n        binding.fabSearchStop.hide();\n        binding.rfRvSearchBooks.finishLoadMore(isAll, true);\n    }\n\n    @Override\n    public void searchBookError(Throwable throwable) {\n        if (searchBookAdapter.getICount() == 0) {\n            ((TextView) refreshErrorView.findViewById(R.id.tv_error_msg)).setText(throwable.getMessage());\n            binding.rfRvSearchBooks.refreshError();\n        } else {\n            binding.rfRvSearchBooks.loadMoreError();\n        }\n    }\n\n    @Override\n    public void loadMoreSearchBook(final List<SearchBookBean> books) {\n        searchBookAdapter.addAll(books, mSearchAutoComplete.getText().toString().trim());\n    }\n\n    @Override\n    protected void onDestroy() {\n        mPresenter.stopSearch();\n        mExplosionField.clear();\n        super.onDestroy();\n    }\n\n    @Override\n    public EditText getEdtContent() {\n        return mSearchAutoComplete;\n    }\n\n    @Override\n    public SearchBookAdapter getSearchBookAdapter() {\n        return searchBookAdapter;\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (resultCode == RESULT_OK) {\n            if (requestCode == requestSource) {\n                initMenu();\n                mPresenter.initSearchEngineS(MApplication.SEARCH_GROUP);\n            }\n        }\n    }\n\n    @Override\n    public void finish() {\n        super.finish();\n        overridePendingTransition(0, android.R.anim.fade_out);\n    }\n\n    @Override\n    public void openBookInfo(BookInfoBean bookInfoBean) {\n        Intent intent = new Intent(this, BookDetailActivity.class);\n        intent.putExtra(\"noteUrl\", bookInfoBean.getNoteUrl());\n        startActivity(intent);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/SettingActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.MenuItem;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.ActionBar;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.databinding.ActivitySettingsBinding;\nimport com.kunfei.bookshelf.help.storage.BackupRestoreUi;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.fragment.SettingsFragment;\n\n/**\n * Created by GKF on 2017/12/16.\n * 设置\n */\n\npublic class SettingActivity extends MBaseActivity<IPresenter> {\n\n    private ActivitySettingsBinding binding;\n    private final SettingsFragment settingsFragment = new SettingsFragment();\n\n    public static void startThis(Context context) {\n        context.startActivity(new Intent(context, SettingActivity.class));\n    }\n\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivitySettingsBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar(getString(R.string.setting));\n\n        getFragmentManager().beginTransaction()\n                .replace(R.id.settingsFrameLayout, settingsFragment, \"settings\")\n                .commit();\n\n    }\n\n    @Override\n    protected void initData() {\n\n    }\n\n    //设置ToolBar\n    public void setupActionBar(String title) {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(title);\n        }\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    public void finish() {\n        if (getFragmentManager().findFragmentByTag(\"settings\") == null) {\n            getFragmentManager().beginTransaction()\n                    .replace(R.id.settingsFrameLayout, settingsFragment, \"settings\")\n                    .commit();\n        } else {\n            super.finish();\n        }\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n\n    @Override\n    public void onBackPressed() {\n        super.onBackPressed();\n    }\n\n    @Override\n    public void initImmersionBar() {\n        super.initImmersionBar();\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        BackupRestoreUi.INSTANCE.onActivityResult(requestCode, resultCode, data);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/SourceDebugActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.Menu;\nimport android.view.MenuItem;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.ActivitySourceDebugBinding;\nimport com.kunfei.bookshelf.model.content.Debug;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.SourceDebugAdapter;\n\nimport io.reactivex.disposables.CompositeDisposable;\n\npublic class SourceDebugActivity extends MBaseActivity<IPresenter> {\n    private final int REQUEST_QR = 202;\n\n    private ActivitySourceDebugBinding binding;\n    private SourceDebugAdapter adapter;\n    private CompositeDisposable compositeDisposable;\n    private String sourceTag;\n\n    public static void startThis(Context context, String sourceUrl) {\n        if (TextUtils.isEmpty(sourceUrl)) return;\n        Intent intent = new Intent(context, SourceDebugActivity.class);\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        intent.putExtra(\"sourceUrl\", sourceUrl);\n        context.startActivity(intent);\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        RxBus.get().register(this);\n    }\n\n    @Override\n    protected void onDestroy() {\n        Debug.SOURCE_DEBUG_TAG = null;\n        RxBus.get().unregister(this);\n        if (compositeDisposable != null) {\n            compositeDisposable.dispose();\n        }\n        super.onDestroy();\n    }\n\n    /**\n     * 布局载入  setContentView()\n     */\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivitySourceDebugBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    /**\n     * 数据初始化\n     */\n    @Override\n    protected void initData() {\n        sourceTag = getIntent().getStringExtra(\"sourceUrl\");\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        initSearchView();\n        adapter = new SourceDebugAdapter(this);\n        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));\n        binding.recyclerView.setAdapter(adapter);\n    }\n\n    private void initSearchView() {\n        binding.searchView.setQueryHint(getString(R.string.debug_hint));\n        binding.searchView.onActionViewExpanded();\n        binding.searchView.setSubmitButtonEnabled(true);\n        binding.searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                if (TextUtils.isEmpty(query))\n                    return false;\n                startDebug(query);\n                SoftInputUtil.hideIMM(binding.searchView);\n                return true;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                return false;\n            }\n        });\n    }\n\n    private void startDebug(String key) {\n        if (TextUtils.isEmpty(sourceTag) || TextUtils.isEmpty(key)) {\n            toast(R.string.cannot_empty);\n            return;\n        }\n        if (compositeDisposable != null) {\n            compositeDisposable.dispose();\n        }\n        compositeDisposable = new CompositeDisposable();\n        binding.loading.start();\n        adapter.clearData();\n        Debug.newDebug(sourceTag, key, compositeDisposable);\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_debug_activity, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_scan) {\n            scan();\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    private void scan() {\n        Intent intent = new Intent(this, QRCodeScanActivity.class);\n        startActivityForResult(intent, REQUEST_QR);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (resultCode == RESULT_OK) {\n            if (requestCode == REQUEST_QR) {\n                String result = data.getStringExtra(\"result\");\n                if (!StringUtils.isTrimEmpty(result)) {\n                    binding.searchView.setQuery(result, true);\n                }\n            }\n        }\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.PRINT_DEBUG_LOG)})\n    public void printDebugLog(String msg) {\n        adapter.addData(msg);\n        if (msg.equals(\"finish\")) {\n            binding.loading.stop();\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/SourceEditActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport static android.text.TextUtils.isEmpty;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.text.Editable;\nimport android.view.Gravity;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.ViewTreeObserver;\nimport android.widget.EditText;\nimport android.widget.PopupWindow;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.core.content.FileProvider;\nimport androidx.fragment.app.Fragment;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.zxing.EncodeHintType;\nimport com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.BuildConfig;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.constant.BookType;\nimport com.kunfei.bookshelf.databinding.ActivitySourceEditBinding;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.presenter.SourceEditPresenter;\nimport com.kunfei.bookshelf.presenter.contract.SourceEditContract;\nimport com.kunfei.bookshelf.service.ShareService;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.SourceEditAdapter;\nimport com.kunfei.bookshelf.view.dialog.SourceLoginDialog;\nimport com.kunfei.bookshelf.view.popupwindow.KeyboardToolPop;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\n\nimport cn.bingoogolapple.qrcode.zxing.QRCodeEncoder;\nimport io.reactivex.Observable;\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\n/**\n * Created by GKF on 2018/1/26.\n * 编辑书源\n */\n\npublic class SourceEditActivity extends MBaseActivity<SourceEditContract.Presenter> implements SourceEditContract.View, KeyboardToolPop.CallBack {\n    public final static int EDIT_SOURCE = 1101;\n    private final int REQUEST_QR = 202;\n\n    private ActivitySourceEditBinding binding;\n    private SourceEditAdapter adapter;\n    private final List<SourceEdit> sourceEditList = new ArrayList<>();\n    private final List<SourceEdit> findEditList = new ArrayList<>();\n    private BookSourceBean bookSourceBean;\n    private int serialNumber;\n    private boolean enable;\n    private String title;\n    private PopupWindow mSoftKeyboardTool;\n    private boolean mIsSoftKeyBoardShowing = false;\n    private boolean showFind;\n    private String[] keyHelp = {\"@\", \"&\", \"|\", \"%\", \"/\", \":\", \"[\", \"]\", \"(\", \")\", \"{\", \"}\", \"<\", \">\", \"\\\\\", \"$\", \"#\", \"!\", \".\",\n            \"href\", \"src\", \"textNodes\", \"xpath\", \"json\", \"css\", \"id\", \"class\", \"tag\"};\n\n    public static void startThis(Object object, BookSourceBean sourceBean) {\n        String key = String.valueOf(System.currentTimeMillis());\n        BitIntentDataManager.getInstance().putData(key, sourceBean.clone());\n        if (object instanceof Activity) {\n            Activity activity = (Activity) object;\n            Intent intent = new Intent(activity, SourceEditActivity.class);\n            intent.putExtra(\"data_key\", key);\n            activity.startActivityForResult(intent, EDIT_SOURCE);\n        } else if (object instanceof Fragment) {\n            Fragment fragment = (Fragment) object;\n            Intent intent = new Intent(fragment.getContext(), SourceEditActivity.class);\n            intent.putExtra(\"data_key\", key);\n            fragment.startActivityForResult(intent, EDIT_SOURCE);\n        } else if (object instanceof Context) {\n            Context context = (Context) object;\n            Intent intent = new Intent(context, SourceEditActivity.class);\n            intent.putExtra(\"data_key\", key);\n            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n            context.startActivity(intent);\n        }\n    }\n\n    @Override\n    protected SourceEditContract.Presenter initInjector() {\n        return new SourceEditPresenter();\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            title = savedInstanceState.getString(\"title\");\n            serialNumber = savedInstanceState.getInt(\"serialNumber\");\n            enable = savedInstanceState.getBoolean(\"enable\");\n        }\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected void onSaveInstanceState(@NonNull Bundle outState) {\n        super.onSaveInstanceState(outState);\n        outState.putString(\"title\", title);\n        outState.putInt(\"serialNumber\", serialNumber);\n        outState.putBoolean(\"enable\", enable);\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivitySourceEditBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void initData() {\n        String key = this.getIntent().getStringExtra(\"data_key\");\n        if (title == null) {\n            if (isEmpty(key)) {\n                title = getString(R.string.add_book_source);\n                bookSourceBean = new BookSourceBean();\n            } else {\n                title = getString(R.string.edit_book_source);\n                bookSourceBean = (BookSourceBean) BitIntentDataManager.getInstance().getData(key);\n                serialNumber = bookSourceBean.getSerialNumber();\n                enable = bookSourceBean.getEnable();\n            }\n        }\n\n    }\n\n    @Override\n    protected void bindView() {\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        mSoftKeyboardTool = new KeyboardToolPop(this, Arrays.asList(keyHelp), this);\n        getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new KeyboardOnGlobalChangeListener());\n        adapter = new SourceEditAdapter(this);\n        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));\n        binding.recyclerView.setAdapter(adapter);\n        adapter.reSetData(sourceEditList);\n        setText(bookSourceBean);\n    }\n\n    @Override\n    protected void bindEvent() {\n        super.bindEvent();\n        binding.tvEditFind.setOnClickListener(v -> {\n            binding.recyclerView.clearFocus();\n            if (showFind) {\n                adapter.reSetData(sourceEditList);\n                binding.tvEditFind.setText(R.string.edit_find);\n            } else {\n                adapter.reSetData(findEditList);\n                binding.tvEditFind.setText(R.string.back);\n            }\n            showFind = !showFind;\n            binding.recyclerView.scrollToPosition(0);\n        });\n    }\n\n    private boolean canSaveBookSource() {\n        SoftInputUtil.hideIMM(binding.recyclerView);\n        binding.recyclerView.clearFocus();\n        BookSourceBean bookSourceBean = getBookSource(true);\n        if (isEmpty(bookSourceBean.getBookSourceName()) || isEmpty(bookSourceBean.getBookSourceUrl())) {\n            toast(R.string.non_null_source_name_url, ERROR);\n            return false;\n        }\n        return true;\n    }\n\n    @Override\n    public String getBookSourceStr(boolean hasFind) {\n        Gson gson = new GsonBuilder()\n                .disableHtmlEscaping()\n                .setPrettyPrinting()\n                .create();\n        return gson.toJson(getBookSource(hasFind));\n    }\n\n    @Override\n    public void setText(BookSourceBean bookSourceBean) {\n        sourceEditList.clear();\n        findEditList.clear();\n        adapter.notifyDataSetChanged();\n        sourceEditList.add(new SourceEdit(\"bookSourceUrl\", bookSourceBean.getBookSourceUrl(), R.string.book_source_url));\n        sourceEditList.add(new SourceEdit(\"bookSourceName\", bookSourceBean.getBookSourceName(), R.string.book_source_name));\n        sourceEditList.add(new SourceEdit(\"bookSourceGroup\", bookSourceBean.getBookSourceGroup(), R.string.book_source_group));\n        sourceEditList.add(new SourceEdit(\"loginUrl\", bookSourceBean.getLoginUrl(), R.string.book_source_login_url));\n        sourceEditList.add(new SourceEdit(\"loginUi\", bookSourceBean.getLoginUi(), R.string.login_ui));\n        sourceEditList.add(new SourceEdit(\"loginCheckJs\", bookSourceBean.getLoginCheckJs(), R.string.login_check_js));\n        //搜索\n        sourceEditList.add(new SourceEdit(\"ruleSearchUrl\", bookSourceBean.getRuleSearchUrl(), R.string.rule_search_url));\n        sourceEditList.add(new SourceEdit(\"ruleSearchList\", bookSourceBean.getRuleSearchList(), R.string.rule_search_list));\n        sourceEditList.add(new SourceEdit(\"ruleSearchName\", bookSourceBean.getRuleSearchName(), R.string.rule_search_name));\n        sourceEditList.add(new SourceEdit(\"ruleSearchAuthor\", bookSourceBean.getRuleSearchAuthor(), R.string.rule_search_author));\n        sourceEditList.add(new SourceEdit(\"ruleSearchKind\", bookSourceBean.getRuleSearchKind(), R.string.rule_search_kind));\n        sourceEditList.add(new SourceEdit(\"ruleSearchLastChapter\", bookSourceBean.getRuleSearchLastChapter(), R.string.rule_search_last_chapter));\n        sourceEditList.add(new SourceEdit(\"ruleSearchIntroduce\", bookSourceBean.getRuleSearchIntroduce(), R.string.rule_search_introduce));\n        sourceEditList.add(new SourceEdit(\"ruleSearchCoverUrl\", bookSourceBean.getRuleSearchCoverUrl(), R.string.rule_search_cover_url));\n        sourceEditList.add(new SourceEdit(\"ruleSearchNoteUrl\", bookSourceBean.getRuleSearchNoteUrl(), R.string.rule_search_note_url));\n        //详情页\n        sourceEditList.add(new SourceEdit(\"ruleBookUrlPattern\", bookSourceBean.getRuleBookUrlPattern(), R.string.book_url_pattern));\n        sourceEditList.add(new SourceEdit(\"ruleBookInfoInit\", bookSourceBean.getRuleBookInfoInit(), R.string.rule_book_info_init));\n        sourceEditList.add(new SourceEdit(\"ruleBookName\", bookSourceBean.getRuleBookName(), R.string.rule_book_name));\n        sourceEditList.add(new SourceEdit(\"ruleBookAuthor\", bookSourceBean.getRuleBookAuthor(), R.string.rule_book_author));\n        sourceEditList.add(new SourceEdit(\"ruleCoverUrl\", bookSourceBean.getRuleCoverUrl(), R.string.rule_cover_url));\n        sourceEditList.add(new SourceEdit(\"ruleIntroduce\", bookSourceBean.getRuleIntroduce(), R.string.rule_introduce));\n        sourceEditList.add(new SourceEdit(\"ruleBookKind\", bookSourceBean.getRuleBookKind(), R.string.rule_book_kind));\n        sourceEditList.add(new SourceEdit(\"ruleBookLastChapter\", bookSourceBean.getRuleBookLastChapter(), R.string.rule_book_last_chapter));\n        sourceEditList.add(new SourceEdit(\"ruleChapterUrl\", bookSourceBean.getRuleChapterUrl(), R.string.rule_chapter_list_url));\n        //目录页\n        sourceEditList.add(new SourceEdit(\"ruleChapterUrlNext\", bookSourceBean.getRuleChapterUrlNext(), R.string.rule_chapter_list_url_next));\n        sourceEditList.add(new SourceEdit(\"ruleChapterList\", bookSourceBean.getRuleChapterList(), R.string.rule_chapter_list));\n        sourceEditList.add(new SourceEdit(\"ruleChapterName\", bookSourceBean.getRuleChapterName(), R.string.rule_chapter_name));\n        sourceEditList.add(new SourceEdit(\"ruleContentUrl\", bookSourceBean.getRuleContentUrl(), R.string.rule_content_url));\n        sourceEditList.add(new SourceEdit(\"ruleChapterVip\", bookSourceBean.getRuleChapterVip(), R.string.rule_vip));\n        sourceEditList.add(new SourceEdit(\"ruleChapterPay\", bookSourceBean.getRuleChapterPay(), R.string.rule_pay));\n        //正文页\n        sourceEditList.add(new SourceEdit(\"ruleContentUrlNext\", bookSourceBean.getRuleContentUrlNext(), R.string.rule_content_url_next));\n        sourceEditList.add(new SourceEdit(\"ruleBookContent\", bookSourceBean.getRuleBookContent(), R.string.rule_book_content));\n        sourceEditList.add(new SourceEdit(\"ruleBookContentReplace\", bookSourceBean.getRuleBookContentReplace(), R.string.rule_book_content_replace));\n        sourceEditList.add(new SourceEdit(\"httpUserAgent\", bookSourceBean.getHttpUserAgent(), R.string.source_user_agent));\n\n        //发现\n        findEditList.add(new SourceEdit(\"ruleFindUrl\", bookSourceBean.getRuleFindUrl(), R.string.rule_find_url));\n        findEditList.add(new SourceEdit(\"ruleFindList\", bookSourceBean.getRuleFindList(), R.string.rule_find_list));\n        findEditList.add(new SourceEdit(\"ruleFindName\", bookSourceBean.getRuleFindName(), R.string.rule_find_name));\n        findEditList.add(new SourceEdit(\"ruleFindAuthor\", bookSourceBean.getRuleFindAuthor(), R.string.rule_find_author));\n        findEditList.add(new SourceEdit(\"ruleFindKind\", bookSourceBean.getRuleFindKind(), R.string.rule_find_kind));\n        findEditList.add(new SourceEdit(\"ruleFindIntroduce\", bookSourceBean.getRuleFindIntroduce(), R.string.rule_find_introduce));\n        findEditList.add(new SourceEdit(\"ruleFindLastChapter\", bookSourceBean.getRuleFindLastChapter(), R.string.rule_find_last_chapter));\n        findEditList.add(new SourceEdit(\"ruleFindCoverUrl\", bookSourceBean.getRuleFindCoverUrl(), R.string.rule_find_cover_url));\n        findEditList.add(new SourceEdit(\"ruleFindNoteUrl\", bookSourceBean.getRuleFindNoteUrl(), R.string.rule_find_note_url));\n\n        if (showFind) {\n            adapter.reSetData(findEditList);\n        } else {\n            adapter.reSetData(sourceEditList);\n        }\n        binding.cbIsAudio.setChecked(Objects.equals(bookSourceBean.getBookSourceType(), BookType.AUDIO));\n        binding.cbIsEnable.setChecked(bookSourceBean.getEnable());\n    }\n\n    private void scanBookSource() {\n        Intent intent = new Intent(this, QRCodeScanActivity.class);\n        startActivityForResult(intent, REQUEST_QR);\n    }\n\n    private BookSourceBean getBookSource(boolean hasFind) {\n        BookSourceBean bookSourceBeanN = new BookSourceBean();\n        for (SourceEdit sourceEdit : sourceEditList) {\n            switch (sourceEdit.getKey()) {\n                case \"bookSourceUrl\":\n                    bookSourceBeanN.setBookSourceUrl(sourceEdit.value);\n                    break;\n                case \"bookSourceName\":\n                    bookSourceBeanN.setBookSourceName(sourceEdit.value);\n                    break;\n                case \"bookSourceGroup\":\n                    bookSourceBeanN.setBookSourceGroup(sourceEdit.value);\n                    break;\n                case \"loginUrl\":\n                    bookSourceBeanN.setLoginUrl(sourceEdit.value);\n                    break;\n                case \"loginUi\":\n                    bookSourceBeanN.setLoginUi(sourceEdit.value);\n                    break;\n                case \"loginCheckJs\":\n                    bookSourceBeanN.setLoginCheckJs(sourceEdit.value);\n                    break;\n                case \"ruleSearchUrl\":\n                    bookSourceBeanN.setRuleSearchUrl(sourceEdit.value);\n                    break;\n                case \"ruleSearchList\":\n                    bookSourceBeanN.setRuleSearchList(sourceEdit.value);\n                    break;\n                case \"ruleSearchName\":\n                    bookSourceBeanN.setRuleSearchName(sourceEdit.value);\n                    break;\n                case \"ruleSearchAuthor\":\n                    bookSourceBeanN.setRuleSearchAuthor(sourceEdit.value);\n                    break;\n                case \"ruleSearchKind\":\n                    bookSourceBeanN.setRuleSearchKind(sourceEdit.value);\n                    break;\n                case \"ruleSearchIntroduce\":\n                    bookSourceBeanN.setRuleSearchIntroduce(sourceEdit.value);\n                    break;\n                case \"ruleSearchLastChapter\":\n                    bookSourceBeanN.setRuleSearchLastChapter(sourceEdit.value);\n                    break;\n                case \"ruleSearchCoverUrl\":\n                    bookSourceBeanN.setRuleSearchCoverUrl(sourceEdit.value);\n                    break;\n                case \"ruleSearchNoteUrl\":\n                    bookSourceBeanN.setRuleSearchNoteUrl(sourceEdit.value);\n                    break;\n                case \"ruleBookUrlPattern\":\n                    bookSourceBeanN.setRuleBookUrlPattern(sourceEdit.value);\n                    break;\n                case \"ruleBookInfoInit\":\n                    bookSourceBeanN.setRuleBookInfoInit(sourceEdit.value);\n                    break;\n                case \"ruleBookName\":\n                    bookSourceBeanN.setRuleBookName(sourceEdit.value);\n                    break;\n                case \"ruleBookAuthor\":\n                    bookSourceBeanN.setRuleBookAuthor(sourceEdit.value);\n                    break;\n                case \"ruleCoverUrl\":\n                    bookSourceBeanN.setRuleCoverUrl(sourceEdit.value);\n                    break;\n                case \"ruleIntroduce\":\n                    bookSourceBeanN.setRuleIntroduce(sourceEdit.value);\n                    break;\n                case \"ruleBookKind\":\n                    bookSourceBeanN.setRuleBookKind(sourceEdit.value);\n                    break;\n                case \"ruleBookLastChapter\":\n                    bookSourceBeanN.setRuleBookLastChapter(sourceEdit.value);\n                    break;\n                case \"ruleChapterUrl\":\n                    bookSourceBeanN.setRuleChapterUrl(sourceEdit.value);\n                    break;\n                case \"ruleChapterUrlNext\":\n                    bookSourceBeanN.setRuleChapterUrlNext(sourceEdit.value);\n                    break;\n                case \"ruleChapterList\":\n                    bookSourceBeanN.setRuleChapterList(sourceEdit.value);\n                    break;\n                case \"ruleChapterName\":\n                    bookSourceBeanN.setRuleChapterName(sourceEdit.value);\n                    break;\n                case \"ruleVip\":\n                    bookSourceBeanN.setRuleChapterVip(sourceEdit.value);\n                    break;\n                case \"rulePay\":\n                    bookSourceBeanN.setRuleChapterPay(sourceEdit.value);\n                    break;\n                case \"ruleContentUrl\":\n                    bookSourceBeanN.setRuleContentUrl(sourceEdit.value);\n                    break;\n                case \"ruleContentUrlNext\":\n                    bookSourceBeanN.setRuleContentUrlNext(sourceEdit.value);\n                    break;\n                case \"ruleBookContent\":\n                    bookSourceBeanN.setRuleBookContent(sourceEdit.value);\n                    break;\n                case \"ruleBookContentReplace\":\n                    bookSourceBeanN.setRuleBookContentReplace(sourceEdit.value);\n                    break;\n                case \"httpUserAgent\":\n                    bookSourceBeanN.setHttpUserAgent(sourceEdit.value);\n                    break;\n            }\n        }\n        if (hasFind) {\n            for (SourceEdit sourceEdit : findEditList) {\n                switch (sourceEdit.getKey()) {\n                    case \"ruleFindUrl\":\n                        bookSourceBeanN.setRuleFindUrl(sourceEdit.value);\n                        break;\n                    case \"ruleFindList\":\n                        bookSourceBeanN.setRuleFindList(sourceEdit.value);\n                        break;\n                    case \"ruleFindName\":\n                        bookSourceBeanN.setRuleFindName(sourceEdit.value);\n                        break;\n                    case \"ruleFindAuthor\":\n                        bookSourceBeanN.setRuleFindAuthor(sourceEdit.value);\n                        break;\n                    case \"ruleFindKind\":\n                        bookSourceBeanN.setRuleFindKind(sourceEdit.value);\n                        break;\n                    case \"ruleFindIntroduce\":\n                        bookSourceBeanN.setRuleFindIntroduce(sourceEdit.value);\n                        break;\n                    case \"ruleFindLastChapter\":\n                        bookSourceBeanN.setRuleFindLastChapter(sourceEdit.value);\n                        break;\n                    case \"ruleFindCoverUrl\":\n                        bookSourceBeanN.setRuleFindCoverUrl(sourceEdit.value);\n                        break;\n                    case \"ruleFindNoteUrl\":\n                        bookSourceBeanN.setRuleFindNoteUrl(sourceEdit.value);\n                        break;\n                }\n            }\n        }\n        bookSourceBeanN.setSerialNumber(serialNumber);\n        bookSourceBeanN.setEnable(binding.cbIsEnable.isChecked());\n        bookSourceBeanN.setBookSourceType(binding.cbIsAudio.isChecked() ? BookType.AUDIO : null);\n        return bookSourceBeanN;\n    }\n\n    @SuppressLint(\"SetWorldReadable\")\n    private void shareBookSource() {\n        Single.create((SingleOnSubscribe<Bitmap>) emitter -> {\n            QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);\n            Bitmap bitmap = QRCodeEncoder.syncEncodeQRCode(getBookSourceStr(true), 600);\n            QRCodeEncoder.HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);\n            emitter.onSuccess(bitmap);\n        }).compose(RxUtils::toSimpleSingle)\n                .subscribe(new MySingleObserver<Bitmap>() {\n\n                    @Override\n                    public void onSuccess(Bitmap bitmap) {\n                        if (bitmap == null) {\n                            toast(\"书源文字太多,生成二维码失败\");\n                            return;\n                        }\n                        try {\n                            File file = new File(SourceEditActivity.this.getExternalCacheDir(), \"bookSource.png\");\n                            FileOutputStream fOut = new FileOutputStream(file);\n                            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);\n                            fOut.flush();\n                            fOut.close();\n                            //noinspection ResultOfMethodCallIgnored\n                            file.setReadable(true, false);\n                            Uri contentUri = FileProvider.getUriForFile(SourceEditActivity.this, BuildConfig.APPLICATION_ID + \".fileProvider\", file);\n                            final Intent intent = new Intent(Intent.ACTION_SEND);\n                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                            intent.putExtra(Intent.EXTRA_STREAM, contentUri);\n                            intent.setType(\"image/png\");\n                            startActivity(Intent.createChooser(intent, \"分享书源\"));\n                        } catch (Exception e) {\n                            toast(e.getLocalizedMessage());\n                        }\n                    }\n                });\n\n    }\n\n    private void openRuleSummary() {\n        try {\n            Intent intent = new Intent(Intent.ACTION_VIEW);\n            intent.setData(Uri.parse(getString(R.string.source_rule_url)));\n            startActivity(intent);\n        } catch (Exception e) {\n            toast(R.string.can_not_open, ERROR);\n        }\n    }\n\n    private void shareText(String title, String text) {\n        try {\n            Intent textIntent = new Intent(Intent.ACTION_SEND);\n            textIntent.setType(\"text/plain\");\n            textIntent.putExtra(Intent.EXTRA_TEXT, text);\n            startActivity(Intent.createChooser(textIntent, title));\n        } catch (Exception e) {\n            toast(R.string.can_not_share, ERROR);\n        }\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(title);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_book_source_edit, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_save) {\n            if (canSaveBookSource()) {\n                mPresenter.saveSource(getBookSource(true), bookSourceBean)\n                        .subscribe(new MyObserver<Boolean>() {\n                            @Override\n                            public void onNext(Boolean aBoolean) {\n                                bookSourceBean = getBookSource(true);\n                                toast(\"保存成功\");\n                                setResult(RESULT_OK);\n                                finish();\n                            }\n\n                            @Override\n                            public void onError(Throwable e) {\n                                toast(e.getLocalizedMessage());\n                            }\n                        });\n            }\n        } else if (id == R.id.action_login) {\n            BookSourceBean bookSourceBean = getBookSource(true);\n            if (!isEmpty(bookSourceBean.getLoginUrl())) {\n                if (isEmpty(bookSourceBean.getLoginUi())) {\n                    SourceLoginActivity.startThis(this, getBookSource(true));\n                } else {\n                    SourceLoginDialog.Companion.start(\n                            getSupportFragmentManager(),\n                            bookSourceBean.getBookSourceUrl()\n                    );\n                }\n            } else {\n                toast(R.string.source_no_login);\n            }\n        } else if (id == R.id.action_copy_source) {\n            mPresenter.copySource(getBookSourceStr(true));\n        } else if (id == R.id.action_copy_source_no_find) {\n            mPresenter.copySource(getBookSourceStr(false));\n        } else if (id == R.id.action_paste_source) {\n            mPresenter.pasteSource();\n        } else if (id == R.id.action_qr_code_camera) {\n            scanBookSource();\n        } else if (id == R.id.action_share_it) {\n            shareBookSource();\n        } else if (id == R.id.action_share_str) {\n            shareText(\"Source Share\", getBookSourceStr(true));\n        } else if (id == R.id.action_share_wifi) {\n            ShareService.startThis(this, Collections.singletonList(getBookSource(true)));\n        } else if (id == R.id.action_rule_summary) {\n            openRuleSummary();\n        } else if (id == R.id.action_debug_source) {\n            if (canSaveBookSource()) {\n                mPresenter.saveSource(getBookSource(true), bookSourceBean)\n                        .subscribe(new MyObserver<Boolean>() {\n                            @Override\n                            public void onNext(Boolean aBoolean) {\n                                bookSourceBean = getBookSource(true);\n                                setResult(RESULT_OK);\n                                SourceDebugActivity.startThis(SourceEditActivity.this, getBookSource(true).getBookSourceUrl());\n                            }\n\n                            @Override\n                            public void onError(Throwable e) {\n                                toast(e.getLocalizedMessage());\n                            }\n                        });\n            }\n        } else if (id == android.R.id.home) {\n            SoftInputUtil.hideIMM(getCurrentFocus());\n            if (back()) {\n                return true;\n            }\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (requestCode == REQUEST_QR && resultCode == RESULT_OK && null != data) {\n            String result = data.getStringExtra(\"result\");\n            Observable<List<BookSourceBean>> observable = BookSourceManager.importSource(result);\n            if (observable != null) {\n                observable.subscribe(new MyObserver<List<BookSourceBean>>() {\n                    @SuppressLint(\"DefaultLocale\")\n                    @Override\n                    public void onNext(List<BookSourceBean> bookSourceBeans) {\n                        if (bookSourceBeans.size() > 1) {\n                            toast(String.format(\"导入成功%d个书源, 显示第一个\", bookSourceBeans.size()));\n                            setText(bookSourceBeans.get(0));\n                        } else if (bookSourceBeans.size() == 1) {\n                            setText(bookSourceBeans.get(0));\n                        } else {\n                            toast(\"未导入\");\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        toast(e.getLocalizedMessage());\n                    }\n                });\n            } else {\n                toast(\"导入失败\");\n            }\n        }\n    }\n\n    @Override\n    public boolean onKeyDown(int keyCode, KeyEvent keyEvent) {\n        if (keyCode == KeyEvent.KEYCODE_BACK) {\n            if (back()) {\n                return true;\n            }\n        }\n        return super.onKeyDown(keyCode, keyEvent);\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        if (mSoftKeyboardTool != null) {\n            mSoftKeyboardTool.dismiss();\n        }\n    }\n\n    private boolean back() {\n        if (bookSourceBean == null) {\n            bookSourceBean = new BookSourceBean();\n        }\n        if (!getBookSource(true).equals(bookSourceBean)) {\n            new AlertDialog.Builder(this)\n                    .setTitle(getString(R.string.exit))\n                    .setMessage(getString(R.string.exit_no_save))\n                    .setPositiveButton(\"是\", (DialogInterface dialogInterface, int which) -> {\n                    })\n                    .setNegativeButton(\"否\", (DialogInterface dialogInterface, int which) -> finish())\n                    .show();\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void sendText(@NotNull String txt) {\n        if (isEmpty(txt)) return;\n        View view = getWindow().getDecorView().findFocus();\n        if (view instanceof EditText) {\n            EditText editText = (EditText) view;\n            int start = editText.getSelectionStart();\n            int end = editText.getSelectionEnd();\n            Editable edit = editText.getEditableText();//获取EditText的文字\n            if (start < 0 || start >= edit.length()) {\n                edit.append(txt);\n            } else {\n                edit.replace(start, end, txt);//光标所在位置插入文字\n            }\n        }\n    }\n\n    private void showKeyboardTopPopupWindow() {\n        if (isFinishing()) return;\n        if (mSoftKeyboardTool != null && mSoftKeyboardTool.isShowing()) {\n            return;\n        }\n        if (mSoftKeyboardTool != null & !this.isFinishing()) {\n            mSoftKeyboardTool.showAtLocation(binding.llContent, Gravity.BOTTOM, 0, 0);\n        }\n    }\n\n    private void closePopupWindow() {\n        if (mSoftKeyboardTool != null && mSoftKeyboardTool.isShowing()) {\n            mSoftKeyboardTool.dismiss();\n        }\n    }\n\n    private class KeyboardOnGlobalChangeListener implements ViewTreeObserver.OnGlobalLayoutListener {\n        @Override\n        public void onGlobalLayout() {\n            Rect rect = new Rect();\n            // 获取当前页面窗口的显示范围\n            getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);\n            int screenHeight = SoftInputUtil.getScreenHeight(SourceEditActivity.this);\n            int keyboardHeight = screenHeight - rect.bottom; // 输入法的高度\n            boolean preShowing = mIsSoftKeyBoardShowing;\n            if (Math.abs(keyboardHeight) > screenHeight / 5) {\n                mIsSoftKeyBoardShowing = true; // 超过屏幕五分之一则表示弹出了输入法\n                binding.recyclerView.setPadding(0, 0, 0, 100);\n                showKeyboardTopPopupWindow();\n            } else {\n                mIsSoftKeyBoardShowing = false;\n                binding.recyclerView.setPadding(0, 0, 0, 0);\n                if (preShowing) {\n                    closePopupWindow();\n                }\n            }\n        }\n    }\n\n    public class SourceEdit {\n        private String key;\n        private String value;\n        private final int hint;\n\n        SourceEdit(String key, String value, int hint) {\n            this.key = key;\n            this.value = value;\n            this.hint = hint;\n        }\n\n        public String getKey() {\n            return key;\n        }\n\n        public void setKey(String key) {\n            this.key = key;\n        }\n\n        public String getValue() {\n            return value;\n        }\n\n        public void setValue(String value) {\n            this.value = value;\n        }\n\n        public int getHint() {\n            return hint;\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/SourceLoginActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.os.Bundle;\nimport android.text.TextUtils;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.webkit.CookieManager;\nimport android.webkit.WebSettings;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport androidx.appcompat.app.ActionBar;\n\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.CookieBean;\nimport com.kunfei.bookshelf.databinding.ActivitySourceLoginBinding;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\npublic class SourceLoginActivity extends MBaseActivity<IPresenter> {\n\n    private ActivitySourceLoginBinding binding;\n    private BookSourceBean bookSourceBean;\n    private boolean checking = false;\n\n    public static void startThis(Context context, BookSourceBean bookSourceBean) {\n        if (TextUtils.isEmpty(bookSourceBean.getLoginUrl())) {\n            return;\n        }\n        Intent intent = new Intent(context, SourceLoginActivity.class);\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        String key = String.valueOf(System.currentTimeMillis());\n        intent.putExtra(\"data_key\", key);\n        BitIntentDataManager.getInstance().putData(key, bookSourceBean.clone());\n        context.startActivity(intent);\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    /**\n     * 布局载入  setContentView()\n     */\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivitySourceLoginBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n    }\n\n    /**\n     * 数据初始化\n     */\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    @Override\n    protected void initData() {\n        String key = this.getIntent().getStringExtra(\"data_key\");\n        bookSourceBean = (BookSourceBean) BitIntentDataManager.getInstance().getData(key);\n        WebSettings settings = binding.webView.getSettings();\n        settings.setSupportZoom(true);\n        settings.setBuiltInZoomControls(true);\n        settings.setDefaultTextEncodingName(\"UTF-8\");\n        settings.setJavaScriptEnabled(true);\n        CookieManager cookieManager = CookieManager.getInstance();\n        binding.webView.setWebViewClient(new WebViewClient() {\n            @Override\n            public void onPageStarted(WebView view, String url, Bitmap favicon) {\n                String cookie = cookieManager.getCookie(url);\n                DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(bookSourceBean.getBookSourceUrl(), cookie));\n                super.onPageStarted(view, url, favicon);\n            }\n\n            @Override\n            public void onPageFinished(WebView view, String url) {\n                String cookie = cookieManager.getCookie(url);\n                DbHelper.getDaoSession().getCookieBeanDao().insertOrReplace(new CookieBean(bookSourceBean.getBookSourceUrl(), cookie));\n                if (checking)\n                    finish();\n                else\n                    showSnackBar(binding.toolbar, getString(R.string.click_check_after_success));\n                super.onPageFinished(view, url);\n            }\n        });\n        binding.webView.loadUrl(bookSourceBean.getLoginUrl());\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(getString(R.string.login));\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_source_login, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_check) {\n            if (checking) return super.onOptionsItemSelected(item);\n            checking = true;\n            showSnackBar(binding.toolbar, getString(R.string.check_host_cookie));\n            binding.webView.loadUrl(bookSourceBean.getBookSourceUrl());\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/ThemeSettingActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.MenuItem;\n\nimport androidx.appcompat.app.ActionBar;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.databinding.ActivitySettingsBinding;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.fragment.ThemeSettingsFragment;\n\n/**\n * Created by GKF on 2017/12/16.\n * 设置\n */\n\npublic class ThemeSettingActivity extends MBaseActivity<IPresenter> {\n\n    private ActivitySettingsBinding binding;\n\n    public static void startThis(Context context) {\n        context.startActivity(new Intent(context, ThemeSettingActivity.class));\n    }\n\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivitySettingsBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        ThemeSettingsFragment settingsFragment = new ThemeSettingsFragment();\n        getFragmentManager().beginTransaction()\n                .replace(R.id.settingsFrameLayout, settingsFragment)\n                .commit();\n\n    }\n\n    @Override\n    protected void initData() {\n\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.theme_setting);\n        }\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n    }\n\n    @Override\n    public void onBackPressed() {\n        super.onBackPressed();\n    }\n\n    @Override\n    public void initImmersionBar() {\n        super.initImmersionBar();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/TxtChapterRuleActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.Menu;\nimport android.view.MenuItem;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.google.android.material.snackbar.Snackbar;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.bean.TxtChapterRuleBean;\nimport com.kunfei.bookshelf.databinding.ActivityRecyclerVewBinding;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.help.permission.Permissions;\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat;\nimport com.kunfei.bookshelf.model.TxtChapterRuleManager;\nimport com.kunfei.bookshelf.presenter.TxtChapterRulePresenter;\nimport com.kunfei.bookshelf.presenter.contract.TxtChapterRuleContract;\nimport com.kunfei.bookshelf.utils.RealPathUtil;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.TxtChapterRuleAdapter;\nimport com.kunfei.bookshelf.widget.filepicker.picker.FilePicker;\nimport com.kunfei.bookshelf.widget.modialog.TxtChapterRuleDialog;\n\nimport kotlin.Unit;\n\npublic class TxtChapterRuleActivity extends MBaseActivity<TxtChapterRuleContract.Presenter> implements TxtChapterRuleContract.View {\n    private final int requestImport = 102;\n\n    private ActivityRecyclerVewBinding binding;\n    private TxtChapterRuleAdapter adapter;\n    private boolean selectAll = true;\n\n    public static void startThis(Context context) {\n        Intent intent = new Intent(context, TxtChapterRuleActivity.class);\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n        context.startActivity(intent);\n    }\n\n    @Override\n    protected TxtChapterRuleContract.Presenter initInjector() {\n        return new TxtChapterRulePresenter();\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        getWindow().getDecorView().setBackgroundColor(ThemeStore.backgroundColor(this));\n        binding = ActivityRecyclerVewBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n    }\n\n    @Override\n    protected void initData() {\n        this.setSupportActionBar(binding.toolbar);\n        setupActionBar();\n        initRecyclerView();\n        refresh();\n    }\n\n    //设置ToolBar\n    private void setupActionBar() {\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            actionBar.setTitle(R.string.txt_chapter_regex);\n        }\n    }\n\n    // 添加菜单\n    @Override\n    public boolean onCreateOptionsMenu(Menu menu) {\n        getMenuInflater().inflate(R.menu.menu_replace_rule_activity, menu);\n        return super.onCreateOptionsMenu(menu);\n    }\n\n    //菜单\n    @Override\n    public boolean onOptionsItemSelected(MenuItem item) {\n        int id = item.getItemId();\n        if (id == R.id.action_add_replace_rule) {\n            editChapterRule(null);\n        } else if (id == R.id.action_select_all) {\n            selectAllDataS();\n        } else if (id == R.id.action_import) {\n            selectReplaceRuleFile();\n        } else if (id == R.id.action_import_onLine) {\n        } else if (id == R.id.action_del_all) {\n            mPresenter.delData(adapter.getData());\n        } else if (id == android.R.id.home) {\n            finish();\n        }\n        return super.onOptionsItemSelected(item);\n    }\n\n    private void initRecyclerView() {\n        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));\n        adapter = new TxtChapterRuleAdapter(this);\n        binding.recyclerView.setAdapter(adapter);\n        ItemTouchCallback itemTouchCallback = new ItemTouchCallback();\n        itemTouchCallback.setOnItemTouchCallbackListener(adapter.getItemTouchCallbackListener());\n        itemTouchCallback.setDragEnable(true);\n        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback);\n        itemTouchHelper.attachToRecyclerView(binding.recyclerView);\n    }\n\n    public void editChapterRule(TxtChapterRuleBean txtChapterRuleBean) {\n        TxtChapterRuleDialog.builder(this, txtChapterRuleBean)\n                .setPositiveButton(txtChapterRuleBean1 -> {\n                    if (txtChapterRuleBean != null) {\n                        TxtChapterRuleManager.del(txtChapterRuleBean);\n                    }\n                    TxtChapterRuleManager.save(txtChapterRuleBean1);\n                    refresh();\n                })\n                .show();\n    }\n\n    public void upDateSelectAll() {\n        selectAll = true;\n        for (TxtChapterRuleBean ruleBean : adapter.getData()) {\n            if (ruleBean.getEnable() == null || !ruleBean.getEnable()) {\n                selectAll = false;\n                break;\n            }\n        }\n    }\n\n    private void selectAllDataS() {\n        for (TxtChapterRuleBean ruleBean : adapter.getData()) {\n            ruleBean.setEnable(!selectAll);\n        }\n        adapter.notifyDataSetChanged();\n        selectAll = !selectAll;\n        TxtChapterRuleManager.save(adapter.getData());\n    }\n\n    public void delData(TxtChapterRuleBean ruleBean) {\n        mPresenter.delData(ruleBean);\n    }\n\n    public void saveDataS() {\n        mPresenter.saveData(adapter.getData());\n    }\n\n    @Override\n    public void refresh() {\n        adapter.resetDataS(TxtChapterRuleManager.getAll());\n    }\n\n    @Override\n    public Snackbar getSnackBar(String msg, int length) {\n        return Snackbar.make(binding.llContent, msg, length);\n    }\n\n    private void selectReplaceRuleFile() {\n        new PermissionsCompat.Builder(this)\n                .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                .rationale(R.string.get_storage_per)\n                .onGranted((requestCode) -> {\n                    FilePicker filePicker = new FilePicker(TxtChapterRuleActivity.this, FilePicker.FILE);\n                    filePicker.setBackgroundColor(getResources().getColor(R.color.background));\n                    filePicker.setTopBackgroundColor(getResources().getColor(R.color.background));\n                    filePicker.setItemHeight(30);\n                    filePicker.setAllowExtensions(getResources().getStringArray(R.array.text_suffix));\n                    filePicker.setOnFilePickListener(s -> mPresenter.importDataSLocal(s));\n                    filePicker.show();\n                    filePicker.getSubmitButton().setText(R.string.sys_file_picker);\n                    filePicker.getSubmitButton().setOnClickListener(view -> {\n                        filePicker.dismiss();\n                        selectFileSys();\n                    });\n                    return Unit.INSTANCE;\n                })\n                .request();\n    }\n\n    private void selectFileSys() {\n        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);\n        intent.addCategory(Intent.CATEGORY_OPENABLE);\n        intent.setType(\"text/*\");//设置类型\n        startActivityForResult(intent, requestImport);\n    }\n\n    @Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        switch (requestCode) {\n            case requestImport:\n                if (data != null) {\n                    mPresenter.importDataSLocal(RealPathUtil.getPath(this, data.getData()));\n                }\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/WebViewActivity.kt",
    "content": "package com.kunfei.bookshelf.view.activity\n\nimport android.R\nimport android.annotation.SuppressLint\nimport android.view.MenuItem\nimport com.kunfei.basemvplib.BitIntentDataManager\nimport com.kunfei.basemvplib.impl.IPresenter\nimport com.kunfei.bookshelf.base.MBaseActivity\nimport com.kunfei.bookshelf.databinding.ActivityWebViewBinding\nimport com.kunfei.bookshelf.utils.theme.ThemeStore\n\nclass WebViewActivity : MBaseActivity<IPresenter>() {\n\n    val binding by lazy {\n        ActivityWebViewBinding.inflate(layoutInflater)\n    }\n\n    override fun initInjector(): IPresenter? {\n        return null\n    }\n\n    override fun onCreateActivity() {\n        window.decorView.setBackgroundColor(ThemeStore.backgroundColor(this))\n        setContentView(binding.root)\n        setSupportActionBar(binding.toolbar)\n        setupActionBar()\n    }\n\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    override fun initData() {\n        val settings = binding.webView.settings\n        settings.setSupportZoom(true)\n        settings.builtInZoomControls = true\n        settings.defaultTextEncodingName = \"UTF-8\"\n        settings.javaScriptEnabled = true\n        val url = intent.getStringExtra(\"url\")\n        val header = BitIntentDataManager.getInstance().getData(url) as? Map<String, String>\n        url?.let {\n            if (header == null) {\n                binding.webView.loadUrl(url)\n            } else {\n                binding.webView.loadUrl(url, header)\n            }\n        }\n    }\n\n    //设置ToolBar\n    private fun setupActionBar() {\n        val actionBar = supportActionBar\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(true)\n            actionBar.title = intent.getStringExtra(\"title\")\n        }\n    }\n\n    //菜单\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        val id = item.itemId\n        if (id == R.id.home) {\n            finish()\n        }\n        return super.onOptionsItemSelected(item)\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/WelcomeActivity.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.activity;\n\nimport android.animation.Animator;\nimport android.animation.ValueAnimator;\nimport android.content.Intent;\nimport android.os.AsyncTask;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseActivity;\nimport com.kunfei.bookshelf.databinding.ActivityWelcomeBinding;\nimport com.kunfei.bookshelf.presenter.ReadBookPresenter;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\npublic class WelcomeActivity extends MBaseActivity<IPresenter> {\n\n    private ActivityWelcomeBinding binding;\n\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void onCreateActivity() {\n        // 避免从桌面启动程序后，会重新实例化入口类的activity\n        if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {\n            finish();\n            return;\n        }\n        binding = ActivityWelcomeBinding.inflate(getLayoutInflater());\n        setContentView(binding.getRoot());\n        AsyncTask.execute(DbHelper::getDaoSession);\n        binding.tvGzh.setTextColor(ThemeStore.accentColor(this));\n        binding.ivBg.setColorFilter(ThemeStore.accentColor(this));\n        ValueAnimator welAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(800);\n        welAnimator.setStartDelay(500);\n        welAnimator.addUpdateListener(animation -> {\n            float alpha = (Float) animation.getAnimatedValue();\n            binding.ivBg.setAlpha(alpha);\n        });\n        welAnimator.addListener(new Animator.AnimatorListener() {\n            @Override\n            public void onAnimationStart(Animator animation) {\n                if (preferences.getBoolean(getString(R.string.pk_default_read), false)) {\n                    startReadActivity();\n                } else {\n                    startBookshelfActivity();\n                }\n                finish();\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animation) {\n\n            }\n\n            @Override\n            public void onAnimationCancel(Animator animation) {\n\n            }\n\n            @Override\n            public void onAnimationRepeat(Animator animation) {\n\n            }\n        });\n        welAnimator.start();\n    }\n\n    private void startBookshelfActivity() {\n        startActivityByAnim(new Intent(this, MainActivity.class), android.R.anim.fade_in, android.R.anim.fade_out);\n    }\n\n    private void startReadActivity() {\n        Intent intent = new Intent(this, ReadBookActivity.class);\n        intent.putExtra(\"openFrom\", ReadBookPresenter.OPEN_FROM_APP);\n        startActivity(intent);\n    }\n\n    @Override\n    protected void initData() {\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/activity/WelcomeBookActivity.java",
    "content": "package com.kunfei.bookshelf.view.activity;\n\npublic class WelcomeBookActivity extends WelcomeActivity {\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/BookShelfAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo;\n\nimport java.util.HashSet;\nimport java.util.List;\n\npublic interface BookShelfAdapter {\n\n    void setArrange(boolean isArrange);\n\n    void selectAll();\n\n    ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener();\n\n    List<BookShelfBean> getBooks();\n\n    void replaceAll(List<BookShelfBean> newDataS, String bookshelfPx);\n\n    void refreshBook(String noteUrl);\n\n    void setItemClickListener(OnItemClickListenerTwo itemClickListener);\n\n    HashSet<String> getSelected();\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/BookShelfGridAdapter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.adapter;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.graphics.Color;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo;\nimport com.kunfei.bookshelf.widget.BadgeView;\nimport com.kunfei.bookshelf.widget.RotateLoading;\nimport com.kunfei.bookshelf.widget.image.CoverImageView;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class BookShelfGridAdapter extends RecyclerView.Adapter<BookShelfGridAdapter.MyViewHolder> implements BookShelfAdapter {\n    private boolean isArrange;\n    private List<BookShelfBean> books;\n    private OnItemClickListenerTwo itemClickListener;\n    private String bookshelfPx;\n    private Activity activity;\n    private HashSet<String> selectList = new HashSet<>();\n\n    private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() {\n        @Override\n        public void onSwiped(int adapterPosition) {\n\n        }\n\n        @Override\n        public boolean onMove(int srcPosition, int targetPosition) {\n            BookShelfBean shelfBean = books.get(srcPosition);\n            books.remove(srcPosition);\n            books.add(targetPosition, shelfBean);\n            notifyItemMoved(srcPosition, targetPosition);\n            int start = srcPosition;\n            int end = targetPosition;\n            if (start > end) {\n                start = targetPosition;\n                end = srcPosition;\n            }\n            notifyItemRangeChanged(start, end - start + 1);\n            return true;\n        }\n    };\n\n    public BookShelfGridAdapter(Activity activity) {\n        this.activity = activity;\n        books = new ArrayList<>();\n    }\n\n    @Override\n    public void setArrange(boolean isArrange) {\n        selectList.clear();\n        this.isArrange = isArrange;\n        notifyDataSetChanged();\n    }\n\n    @Override\n    public void selectAll() {\n        if (selectList.size() == books.size()) {\n            selectList.clear();\n        } else {\n            for (BookShelfBean bean : books) {\n                selectList.add(bean.getNoteUrl());\n            }\n        }\n        notifyDataSetChanged();\n        itemClickListener.onClick(null, 0);\n    }\n\n    @Override\n    public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() {\n        return itemTouchCallbackListener;\n    }\n\n    @Override\n    public void refreshBook(String noteUrl) {\n        for (int i = 0; i < books.size(); i++) {\n            if (Objects.equals(books.get(i).getNoteUrl(), noteUrl)) {\n                notifyItemChanged(i);\n            }\n        }\n    }\n\n    @Override\n    public int getItemCount() {\n        //如果不为0，按正常的流程跑\n        return books.size();\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bookshelf_grid, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, @SuppressLint(\"RecyclerView\") int index) {\n        BookShelfBean bookShelfBean = books.get(index);\n        BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean();\n\n        if (isArrange) {\n            holder.vwSelect.setVisibility(View.VISIBLE);\n            if (selectList.contains(bookShelfBean.getNoteUrl())) {\n                holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light);\n            } else {\n                holder.vwSelect.setBackgroundColor(Color.TRANSPARENT);\n            }\n            holder.vwSelect.setOnClickListener(v -> {\n                if (selectList.contains(bookShelfBean.getNoteUrl())) {\n                    selectList.remove(bookShelfBean.getNoteUrl());\n                    holder.vwSelect.setBackgroundColor(Color.TRANSPARENT);\n                } else {\n                    selectList.add(bookShelfBean.getNoteUrl());\n                    holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light);\n                }\n                itemClickListener.onClick(v, index);\n            });\n        } else {\n            holder.vwSelect.setVisibility(View.VISIBLE);\n        }\n        holder.tvName.setText(bookInfoBean.getName());\n        holder.tvName.setBackgroundColor(ThemeStore.backgroundColor(activity));\n\n        if (!activity.isFinishing()) {\n            holder.ivCover.load(bookShelfBean.getCoverPath(), bookShelfBean.getName(), bookShelfBean.getAuthor());\n        }\n\n        holder.ivCover.setOnClickListener(v -> {\n            if (itemClickListener != null)\n                itemClickListener.onClick(v, index);\n        });\n        holder.tvName.setOnClickListener(view -> {\n            if (itemClickListener != null) {\n                itemClickListener.onLongClick(view, index);\n            }\n        });\n        if (!Objects.equals(bookshelfPx, \"2\")) {\n            holder.ivCover.setOnLongClickListener(v -> {\n                if (itemClickListener != null) {\n                    itemClickListener.onLongClick(v, index);\n                }\n                return true;\n            });\n        } else if (bookShelfBean.getSerialNumber() != index) {\n            bookShelfBean.setSerialNumber(index);\n            new Thread() {\n                public void run() {\n                    DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean);\n                }\n            }.start();\n        }\n        if (bookShelfBean.isLoading()) {\n            holder.bvUnread.setVisibility(View.INVISIBLE);\n            holder.rotateLoading.setVisibility(View.VISIBLE);\n            holder.rotateLoading.start();\n        } else {\n            holder.bvUnread.setBadgeCount(bookShelfBean.getUnreadChapterNum());\n            holder.bvUnread.setHighlight(bookShelfBean.getHasUpdate());\n            holder.rotateLoading.setVisibility(View.INVISIBLE);\n            holder.rotateLoading.stop();\n        }\n    }\n\n    @Override\n    public void setItemClickListener(OnItemClickListenerTwo itemClickListener) {\n        this.itemClickListener = itemClickListener;\n    }\n\n    @Override\n    public synchronized void replaceAll(List<BookShelfBean> newDataS, String bookshelfPx) {\n        this.bookshelfPx = bookshelfPx;\n        selectList.clear();\n        if (null != newDataS && newDataS.size() > 0) {\n            BookshelfHelp.order(newDataS, bookshelfPx);\n            books = newDataS;\n        } else {\n            books.clear();\n        }\n        notifyDataSetChanged();\n        if (isArrange) {\n            itemClickListener.onClick(null, 0);\n        }\n    }\n\n    @Override\n    public List<BookShelfBean> getBooks() {\n        return books;\n    }\n\n    @Override\n    public HashSet<String> getSelected() {\n        return selectList;\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        CoverImageView ivCover;\n        TextView tvName;\n        BadgeView bvUnread;\n        RotateLoading rotateLoading;\n        View vwSelect;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            ivCover = itemView.findViewById(R.id.iv_cover);\n            tvName = itemView.findViewById(R.id.tv_name);\n            bvUnread = itemView.findViewById(R.id.bv_unread);\n            rotateLoading = itemView.findViewById(R.id.rl_loading);\n            rotateLoading.setLoadingColor(ThemeStore.accentColor(itemView.getContext()));\n            vwSelect = itemView.findViewById(R.id.vw_select);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/BookShelfListAdapter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.adapter;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.graphics.Color;\nimport android.os.AsyncTask;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo;\nimport com.kunfei.bookshelf.widget.BadgeView;\nimport com.kunfei.bookshelf.widget.RotateLoading;\nimport com.kunfei.bookshelf.widget.image.CoverImageView;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class BookShelfListAdapter extends RecyclerView.Adapter<BookShelfListAdapter.MyViewHolder> implements BookShelfAdapter {\n    private boolean isArrange;\n    private Activity activity;\n    private List<BookShelfBean> books;\n    private OnItemClickListenerTwo itemClickListener;\n    private String bookshelfPx;\n    private HashSet<String> selectList = new HashSet<>();\n\n    private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() {\n        @Override\n        public void onSwiped(int adapterPosition) {\n\n        }\n\n        @Override\n        public boolean onMove(int srcPosition, int targetPosition) {\n            Collections.swap(books, srcPosition, targetPosition);\n            notifyItemMoved(srcPosition, targetPosition);\n            notifyItemChanged(srcPosition);\n            notifyItemChanged(targetPosition);\n            return true;\n        }\n    };\n\n\n    public BookShelfListAdapter(Activity activity) {\n        this.activity = activity;\n        books = new ArrayList<>();\n    }\n\n\n    @Override\n    public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() {\n        return itemTouchCallbackListener;\n    }\n\n    @Override\n    public void setArrange(boolean isArrange) {\n        selectList.clear();\n        this.isArrange = isArrange;\n        notifyDataSetChanged();\n    }\n\n    @Override\n    public void selectAll() {\n        if (selectList.size() == books.size()) {\n            selectList.clear();\n        } else {\n            for (BookShelfBean bean : books) {\n                selectList.add(bean.getNoteUrl());\n            }\n        }\n        notifyDataSetChanged();\n        itemClickListener.onClick(null, 0);\n    }\n\n    @Override\n    public void refreshBook(String noteUrl) {\n        for (int i = 0; i < books.size(); i++) {\n            if (Objects.equals(books.get(i).getNoteUrl(), noteUrl)) {\n                notifyItemChanged(i);\n            }\n        }\n    }\n\n    @Override\n    public int getItemCount() {\n        //如果不为0，按正常的流程跑\n        return books.size();\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bookshelf_list, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, @SuppressLint(\"RecyclerView\") int index) {\n        final BookShelfBean bookShelfBean = books.get(index);\n        holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity));\n        if (isArrange) {\n            if (selectList.contains(bookShelfBean.getNoteUrl())) {\n                holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light);\n            } else {\n                holder.vwSelect.setBackgroundColor(Color.TRANSPARENT);\n            }\n            holder.vwSelect.setVisibility(View.VISIBLE);\n            holder.vwSelect.setOnClickListener(v -> {\n                if (selectList.contains(bookShelfBean.getNoteUrl())) {\n                    selectList.remove(bookShelfBean.getNoteUrl());\n                    holder.vwSelect.setBackgroundColor(Color.TRANSPARENT);\n                } else {\n                    selectList.add(bookShelfBean.getNoteUrl());\n                    holder.vwSelect.setBackgroundResource(R.color.ate_button_disabled_light);\n                }\n                itemClickListener.onClick(v, index);\n            });\n        } else {\n            holder.vwSelect.setVisibility(View.GONE);\n        }\n        BookInfoBean bookInfoBean = bookShelfBean.getBookInfoBean();\n        if (!activity.isFinishing()) {\n            holder.ivCover.load(bookShelfBean.getCoverPath(), bookShelfBean.getName(), bookShelfBean.getAuthor());\n        }\n        holder.tvName.setText(bookInfoBean.getName());\n        holder.tvAuthor.setText(bookInfoBean.getAuthor());\n        holder.tvRead.setText(bookShelfBean.getDurChapterName());\n        holder.tvLast.setText(bookShelfBean.getLastChapterName());\n        holder.ivCover.setOnClickListener(v -> {\n            if (itemClickListener != null)\n                itemClickListener.onClick(v, index);\n        });\n        holder.ivCover.setOnLongClickListener(v -> {\n            if (itemClickListener != null) {\n                itemClickListener.onLongClick(v, index);\n            }\n            return true;\n        });\n        holder.flContent.setOnClickListener(v -> {\n            if (itemClickListener != null)\n                itemClickListener.onClick(v, index);\n        });\n        if (!Objects.equals(bookshelfPx, \"2\")) {\n            holder.flContent.setOnLongClickListener(view -> {\n                if (itemClickListener != null) {\n                    itemClickListener.onLongClick(view, index);\n                }\n                return true;\n            });\n        } else {\n            holder.ivCover.setOnClickListener(view -> {\n                if (itemClickListener != null) {\n                    itemClickListener.onLongClick(view, index);\n                }\n            });\n        }\n        if (Objects.equals(bookshelfPx, \"2\") && bookShelfBean.getSerialNumber() != index) {\n            bookShelfBean.setSerialNumber(index);\n            AsyncTask.execute(() -> DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean));\n        }\n        if (bookShelfBean.isLoading()) {\n            holder.bvUnread.setVisibility(View.INVISIBLE);\n            holder.rotateLoading.setVisibility(View.VISIBLE);\n            holder.rotateLoading.start();\n        } else {\n            holder.bvUnread.setBadgeCount(bookShelfBean.getUnreadChapterNum());\n            holder.bvUnread.setHighlight(bookShelfBean.getHasUpdate());\n            holder.rotateLoading.setVisibility(View.INVISIBLE);\n            holder.rotateLoading.stop();\n        }\n    }\n\n    @Override\n    public void setItemClickListener(OnItemClickListenerTwo itemClickListener) {\n        this.itemClickListener = itemClickListener;\n    }\n\n    @Override\n    public synchronized void replaceAll(List<BookShelfBean> newDataS, String bookshelfPx) {\n        this.bookshelfPx = bookshelfPx;\n        selectList.clear();\n        if (null != newDataS && newDataS.size() > 0) {\n            BookshelfHelp.order(newDataS, bookshelfPx);\n            books = newDataS;\n        } else {\n            books.clear();\n        }\n        notifyDataSetChanged();\n        if (isArrange) {\n            itemClickListener.onClick(null, 0);\n        }\n    }\n\n    @Override\n    public List<BookShelfBean> getBooks() {\n        return books;\n    }\n\n    @Override\n    public HashSet<String> getSelected() {\n        return selectList;\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        ViewGroup flContent;\n        CoverImageView ivCover;\n        BadgeView bvUnread;\n        TextView tvName;\n        TextView tvAuthor;\n        TextView tvRead;\n        TextView tvLast;\n        RotateLoading rotateLoading;\n        View vwSelect;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            flContent = itemView.findViewById(R.id.cv_content);\n            ivCover = itemView.findViewById(R.id.iv_cover);\n            bvUnread = itemView.findViewById(R.id.bv_unread);\n            tvName = itemView.findViewById(R.id.tv_name);\n            tvRead = itemView.findViewById(R.id.tv_read);\n            tvLast = itemView.findViewById(R.id.tv_last);\n            tvAuthor = itemView.findViewById(R.id.tv_author);\n            rotateLoading = itemView.findViewById(R.id.rl_loading);\n            rotateLoading.setLoadingColor(ThemeStore.accentColor(itemView.getContext()));\n            vwSelect = itemView.findViewById(R.id.vw_select);\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/BookSourceAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.text.TextUtils;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CheckBox;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.activity.BookSourceActivity;\nimport com.kunfei.bookshelf.view.activity.SourceEditActivity;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Created by GKF on 2017/12/22.\n * 书源Adapter\n */\n\npublic class BookSourceAdapter extends RecyclerView.Adapter<BookSourceAdapter.MyViewHolder> {\n    private List<BookSourceBean> dataList;\n    private List<BookSourceBean> allDataList;\n    private BookSourceActivity activity;\n    private int index;\n    private int sort;\n\n    private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() {\n        @Override\n        public void onSwiped(int adapterPosition) {\n\n        }\n\n        @Override\n        public boolean onMove(int srcPosition, int targetPosition) {\n            Collections.swap(dataList, srcPosition, targetPosition);\n            notifyItemMoved(srcPosition, targetPosition);\n            notifyItemChanged(srcPosition);\n            notifyItemChanged(targetPosition);\n            activity.saveDate(dataList);\n            return true;\n        }\n    };\n\n    public BookSourceAdapter(BookSourceActivity activity) {\n        this.activity = activity;\n        dataList = new ArrayList<>();\n    }\n\n    public void resetDataS(List<BookSourceBean> bookSourceBeanList) {\n        this.dataList = bookSourceBeanList;\n        notifyDataSetChanged();\n        activity.upDateSelectAll();\n        activity.upSearchView(dataList.size());\n        activity.upGroupMenu();\n    }\n\n    private void setAllDataList(List<BookSourceBean> bookSourceBeanList) {\n        this.allDataList = bookSourceBeanList;\n        notifyDataSetChanged();\n        activity.upDateSelectAll();\n    }\n\n    public List<BookSourceBean> getDataList() {\n        return dataList;\n    }\n\n    public List<BookSourceBean> getSelectDataList() {\n        List<BookSourceBean> selectDataS = new ArrayList<>();\n        for (BookSourceBean data : dataList) {\n            if (data.getEnable()) {\n                selectDataS.add(data);\n            }\n        }\n        return selectDataS;\n    }\n\n    public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() {\n        return itemTouchCallbackListener;\n    }\n\n    public void setSort(int sort) {\n        this.sort = sort;\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_book_source, parent, false);\n        return new MyViewHolder(view);\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {\n        holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity));\n        if (sort != 2) {\n            holder.topView.setVisibility(View.VISIBLE);\n        } else {\n            holder.topView.setVisibility(View.GONE);\n        }\n        if (TextUtils.isEmpty(dataList.get(position).getBookSourceGroup())) {\n            holder.cbView.setText(dataList.get(position).getBookSourceName());\n        } else {\n            holder.cbView.setText(String.format(\"%s (%s)\", dataList.get(position).getBookSourceName(), dataList.get(position).getBookSourceGroup()));\n        }\n        holder.cbView.setChecked(dataList.get(position).getEnable());\n        holder.cbView.setOnClickListener((View view) -> {\n            dataList.get(position).setEnable(holder.cbView.isChecked());\n            activity.saveDate(dataList.get(position));\n            activity.upDateSelectAll();\n        });\n        holder.editView.setOnClickListener(view -> SourceEditActivity.startThis(activity, dataList.get(position)));\n        holder.delView.setOnClickListener(view -> {\n            activity.delBookSource(dataList.get(position));\n            dataList.remove(position);\n            activity.upSearchView(dataList.size());\n            notifyDataSetChanged();\n        });\n        holder.topView.setOnClickListener(view -> {\n            setAllDataList(BookSourceManager.getAllBookSource());\n            BookSourceBean moveData = dataList.get(position);\n            dataList.remove(position);\n            notifyItemRemoved(position);\n            dataList.add(0, moveData);\n            notifyItemInserted(0);\n            if (sort == 1) {\n                int maxWeight = allDataList.get(0).getWeight();\n                moveData.setWeight(maxWeight + 1);\n            }\n            if (dataList.size() != allDataList.size()) {\n                index = allDataList.indexOf(moveData);\n                allDataList.remove(index);\n                allDataList.add(0, moveData);\n                activity.saveDate(allDataList);\n            } else {\n                activity.saveDate(dataList);\n            }\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        return dataList.size();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        CheckBox cbView;\n        ImageView editView;\n        ImageView delView;\n        ImageView topView;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            cbView = itemView.findViewById(R.id.cb_book_source);\n            editView = itemView.findViewById(R.id.iv_edit_source);\n            delView = itemView.findViewById(R.id.iv_del_source);\n            topView = itemView.findViewById(R.id.iv_top_source);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/BookmarkAdapter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookmarkBean;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class BookmarkAdapter extends RecyclerView.Adapter<BookmarkAdapter.ThisViewHolder> {\n\n    private BookShelfBean bookShelfBean;\n    private OnItemClickListener itemClickListener;\n    private List<BookmarkBean> allBookmark = new ArrayList<>();\n    private List<BookmarkBean> bookmarkBeans = new ArrayList<>();\n    private boolean isSearch = false;\n\n    public BookmarkAdapter(BookShelfBean bookShelfBean, @NonNull OnItemClickListener itemClickListener) {\n        this.bookShelfBean = bookShelfBean;\n        this.itemClickListener = itemClickListener;\n    }\n\n    public void setAllBookmark(List<BookmarkBean> allBookmark) {\n        this.allBookmark = allBookmark;\n        notifyDataSetChanged();\n    }\n\n    public void search(final String key) {\n        bookmarkBeans.clear();\n        if (Objects.equals(key, \"\")) {\n            isSearch = false;\n            notifyDataSetChanged();\n        } else {\n            Observable.create((ObservableOnSubscribe<Boolean>) emitter -> {\n                for (BookmarkBean bookmarkBean : allBookmark) {\n                    if (bookmarkBean.getChapterName().contains(key)) {\n                        bookmarkBeans.add(bookmarkBean);\n                    } else if (bookmarkBean.getContent().contains(key)) {\n                        bookmarkBeans.add(bookmarkBean);\n                    }\n                }\n                emitter.onNext(true);\n                emitter.onComplete();\n            }).subscribeOn(Schedulers.io())\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .subscribe(new MyObserver<Boolean>() {\n                        @Override\n                        public void onNext(Boolean aBoolean) {\n                            isSearch = true;\n                            notifyDataSetChanged();\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n\n                        }\n                    });\n        }\n    }\n\n    @NonNull\n    @Override\n    public ThisViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new ThisViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chapter_list, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull ThisViewHolder holder, final int position) {\n\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull ThisViewHolder holder, int position, @NonNull List<Object> payloads) {\n        int realPosition = holder.getLayoutPosition();\n        if (realPosition == getItemCount() - 1) {\n            holder.line.setVisibility(View.GONE);\n        } else {\n            holder.line.setVisibility(View.VISIBLE);\n        }\n\n        BookmarkBean bookmarkBean = isSearch ? bookmarkBeans.get(realPosition) : allBookmark.get(realPosition);\n        holder.tvName.setText(StringUtils.isTrimEmpty(bookmarkBean.getContent()) ? bookmarkBean.getChapterName() : bookmarkBean.getContent());\n        holder.llName.setOnClickListener(v -> {\n            if (itemClickListener != null) {\n                itemClickListener.itemClick(bookmarkBean.getChapterIndex(), bookmarkBean.getPageIndex());\n            }\n        });\n        holder.llName.setOnLongClickListener(view -> {\n            if (itemClickListener != null) {\n                itemClickListener.itemLongClick(bookmarkBean);\n            }\n            return true;\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        if (bookShelfBean == null)\n            return 0;\n        else {\n            if (isSearch) {\n                return bookmarkBeans.size();\n            }\n            return allBookmark.size();\n        }\n    }\n\n    static class ThisViewHolder extends RecyclerView.ViewHolder {\n        private TextView tvName;\n        private View line;\n        private View llName;\n\n        ThisViewHolder(View itemView) {\n            super(itemView);\n            tvName = itemView.findViewById(R.id.tv_name);\n            line = itemView.findViewById(R.id.v_line);\n            llName = itemView.findViewById(R.id.ll_name);\n        }\n    }\n\n    public interface OnItemClickListener {\n        void itemClick(int index, int page);\n\n        void itemLongClick(BookmarkBean bookmarkBean);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/ChangeSourceAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static android.text.TextUtils.isEmpty;\n\n/**\n * Created by GKF on 2017/12/22.\n * 书源Adapter\n */\n\npublic class ChangeSourceAdapter extends RefreshRecyclerViewAdapter {\n    private List<SearchBookBean> allBookBeans;\n    private CallBack callBack;\n\n    public ChangeSourceAdapter(Boolean needLoadMore) {\n        super(needLoadMore);\n        allBookBeans = new ArrayList<>();\n    }\n\n    public void addSourceAdapter(SearchBookBean value) {\n        allBookBeans.add(value);\n        notifyDataSetChanged();\n    }\n\n    public void addAllSourceAdapter(List<SearchBookBean> value) {\n        allBookBeans.addAll(value);\n        notifyDataSetChanged();\n    }\n\n    public void reSetSourceAdapter() {\n        allBookBeans.clear();\n        notifyDataSetChanged();\n    }\n\n    public void removeData(SearchBookBean searchBookBean) {\n        DbHelper.getDaoSession().getSearchBookBeanDao().delete(searchBookBean);\n        allBookBeans.remove(searchBookBean);\n        notifyDataSetChanged();\n    }\n\n    public void setCallBack(CallBack callBack) {\n        this.callBack = callBack;\n    }\n\n    public List<SearchBookBean> getSearchBookBeans() {\n        return allBookBeans;\n    }\n\n    @Override\n    public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_change_source, parent, false));\n    }\n\n    @Override\n    public void onBindIViewHolder(RecyclerView.ViewHolder holder, int position) {\n        MyViewHolder myViewHolder = (MyViewHolder) holder;\n        myViewHolder.bind(allBookBeans.get(position), callBack);\n    }\n\n    @Override\n    public int getIViewType(int position) {\n        return 0;\n    }\n\n    @Override\n    public int getICount() {\n        return allBookBeans.size();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        LinearLayout llContent;\n        TextView tvBookSource;\n        TextView tvLastChapter;\n        ImageView ivChecked;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            llContent = itemView.findViewById(R.id.ll_content);\n            tvBookSource = itemView.findViewById(R.id.tv_source_name);\n            tvLastChapter = itemView.findViewById(R.id.tv_lastChapter);\n            ivChecked = itemView.findViewById(R.id.iv_checked);\n        }\n\n        public void bind(SearchBookBean searchBookBean, CallBack callBack) {\n            tvBookSource.setText(searchBookBean.getOrigin());\n            if (isEmpty(searchBookBean.getLastChapter())) {\n                tvLastChapter.setText(R.string.no_last_chapter);\n            } else {\n                tvLastChapter.setText(searchBookBean.getLastChapter());\n            }\n            if (searchBookBean.getIsCurrentSource()) {\n                ivChecked.setVisibility(View.VISIBLE);\n            } else {\n                ivChecked.setVisibility(View.INVISIBLE);\n            }\n            llContent.setOnClickListener(view -> {\n                if (callBack != null) {\n                    callBack.changeTo(searchBookBean);\n                }\n            });\n            llContent.setOnLongClickListener(view -> {\n                if (callBack != null) {\n                    callBack.showMenu(view, searchBookBean);\n                }\n                return true;\n            });\n        }\n    }\n\n    public interface CallBack {\n        void changeTo(SearchBookBean searchBookBean);\n\n        void showMenu(View view, SearchBookBean searchBookBean);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/ChapterListAdapter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class ChapterListAdapter extends RecyclerView.Adapter<ChapterListAdapter.ThisViewHolder> {\n\n    private BookShelfBean bookShelfBean;\n    private OnItemClickListener itemClickListener;\n    private List<BookChapterBean> allChapter;\n    private List<BookChapterBean> bookChapterBeans = new ArrayList<>();\n    private int index = 0;\n    private boolean isSearch = false;\n    private int normalColor;\n    private int highlightColor;\n\n    public ChapterListAdapter(BookShelfBean bookShelfBean, List<BookChapterBean> allChapter, @NonNull OnItemClickListener itemClickListener) {\n        this.bookShelfBean = bookShelfBean;\n        this.allChapter = allChapter;\n        this.itemClickListener = itemClickListener;\n        highlightColor = ThemeStore.accentColor(MApplication.getInstance());\n    }\n\n    public void upChapter(int index) {\n        if (allChapter.size() > index) {\n            notifyItemChanged(index, 0);\n        }\n    }\n\n    public void search(final String key) {\n        bookChapterBeans.clear();\n        if (Objects.equals(key, \"\")) {\n            isSearch = false;\n            notifyDataSetChanged();\n        } else {\n            Observable.create((ObservableOnSubscribe<Boolean>) emitter -> {\n                for (BookChapterBean bookChapterBean : allChapter) {\n                    if (bookChapterBean.getDurChapterName().contains(key)) {\n                        bookChapterBeans.add(bookChapterBean);\n                    }\n                }\n                emitter.onNext(true);\n                emitter.onComplete();\n            }).subscribeOn(Schedulers.io())\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .subscribe(new MyObserver<Boolean>() {\n                        @Override\n                        public void onNext(Boolean aBoolean) {\n                            isSearch = true;\n                            notifyDataSetChanged();\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n\n                        }\n                    });\n        }\n    }\n\n    @NonNull\n    @Override\n    public ThisViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        normalColor = ThemeStore.textColorSecondary(parent.getContext());\n        return new ThisViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chapter_list, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull ThisViewHolder holder, final int position) {\n\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull ThisViewHolder holder, int position, @NonNull List<Object> payloads) {\n        int realPosition = holder.getLayoutPosition();\n        if (realPosition == getItemCount() - 1) {\n            holder.line.setVisibility(View.GONE);\n        } else {\n            holder.line.setVisibility(View.VISIBLE);\n        }\n        if (payloads.size() > 0) {\n            holder.tvName.setSelected(true);\n            holder.tvName.getPaint().setFakeBoldText(true);\n            return;\n        }\n        BookChapterBean bookChapterBean = isSearch ? bookChapterBeans.get(realPosition) : allChapter.get(realPosition);\n        if (bookChapterBean.getDurChapterIndex() == index) {\n            holder.tvName.setTextColor(highlightColor);\n        } else {\n            holder.tvName.setTextColor(normalColor);\n        }\n\n        holder.tvName.setText(bookChapterBean.getDisplayTitle(holder.tvName.getContext()));\n        if (Objects.equals(bookShelfBean.getTag(), BookShelfBean.LOCAL_TAG) || bookChapterBean.getHasCache(bookShelfBean.getBookInfoBean())) {\n            holder.tvName.setSelected(true);\n            holder.tvName.getPaint().setFakeBoldText(true);\n        } else {\n            holder.tvName.setSelected(false);\n            holder.tvName.getPaint().setFakeBoldText(false);\n        }\n\n        holder.llName.setOnClickListener(v -> {\n            setIndex(realPosition);\n            itemClickListener.itemClick(bookChapterBean.getDurChapterIndex(), 0);\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        if (bookShelfBean == null || allChapter == null)\n            return 0;\n        else {\n            if (isSearch) {\n                return bookChapterBeans.size();\n            }\n            return allChapter.size();\n        }\n    }\n\n    public int getIndex() {\n        return index;\n    }\n\n    public void setIndex(int index) {\n        this.index = index;\n        notifyItemChanged(this.index, 0);\n    }\n\n    static class ThisViewHolder extends RecyclerView.ViewHolder {\n        private TextView tvName;\n        private View line;\n        private View llName;\n\n        ThisViewHolder(View itemView) {\n            super(itemView);\n            tvName = itemView.findViewById(R.id.tv_name);\n            line = itemView.findViewById(R.id.v_line);\n            llName = itemView.findViewById(R.id.ll_name);\n        }\n    }\n\n    public interface OnItemClickListener {\n        void itemClick(int index, int page);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/ChoiceBookAdapter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.adapter;\n\nimport android.app.Activity;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookKindBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.widget.image.CoverImageView;\nimport com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.kunfei.bookshelf.utils.StringUtils.isTrimEmpty;\n\npublic class ChoiceBookAdapter extends RefreshRecyclerViewAdapter {\n    private Activity activity;\n    private ArrayList<SearchBookBean> searchBooks;\n    private Callback callback;\n\n    public ChoiceBookAdapter(Activity activity) {\n        super(true);\n        this.activity = activity;\n        searchBooks = new ArrayList<>();\n    }\n\n    @Override\n    public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_search_book, parent, false));\n    }\n\n    @Override\n    public void onBindIViewHolder(final RecyclerView.ViewHolder holder, final int position) {\n        MyViewHolder myViewHolder = (MyViewHolder) holder;\n        if (position >= searchBooks.size()) return;\n        SearchBookBean book = searchBooks.get(position);\n        if (!activity.isFinishing()) {\n            myViewHolder.ivCover.load(book.getCoverUrl(), book.getName(), book.getCoverUrl());\n        }\n        String title = book.getName();\n        String author = book.getAuthor();\n        if (author != null && author.trim().length() > 0)\n            title = String.format(\"%s (%s)\", title, author);\n        myViewHolder.tvName.setText(title);\n        BookKindBean bookKindBean = new BookKindBean(book.getKind());\n        if (isTrimEmpty(bookKindBean.getKind())) {\n            myViewHolder.tvKind.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvKind.setVisibility(View.VISIBLE);\n            myViewHolder.tvKind.setText(bookKindBean.getKind());\n        }\n        if (isTrimEmpty(bookKindBean.getWordsS())) {\n            myViewHolder.tvWords.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvWords.setVisibility(View.VISIBLE);\n            myViewHolder.tvWords.setText(bookKindBean.getWordsS());\n        }\n        if (isTrimEmpty(bookKindBean.getState())) {\n            myViewHolder.tvState.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvState.setVisibility(View.VISIBLE);\n            myViewHolder.tvState.setText(bookKindBean.getState());\n        }\n        //来源\n        if (isTrimEmpty(book.getOrigin())) {\n            myViewHolder.tvOrigin.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvOrigin.setVisibility(View.VISIBLE);\n            myViewHolder.tvOrigin.setText(activity.getString(R.string.origin_format, searchBooks.get(position).getOrigin()));\n        }\n        //最新章节\n        if (isTrimEmpty(book.getLastChapter())) {\n            myViewHolder.tvLasted.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvLasted.setText(book.getLastChapter());\n            myViewHolder.tvLasted.setVisibility(View.VISIBLE);\n        }\n        //简介\n        if (isTrimEmpty(book.getIntroduce())) {\n            myViewHolder.tvIntroduce.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvIntroduce.setText(StringUtils.formatHtml(searchBooks.get(position).getIntroduce()));\n            myViewHolder.tvIntroduce.setVisibility(View.VISIBLE);\n        }\n\n        myViewHolder.flContent.setOnClickListener(v -> {\n            if (callback != null)\n                callback.clickItem(myViewHolder.ivCover, position, book);\n        });\n    }\n\n    @Override\n    public int getIViewType(int position) {\n        return 0;\n    }\n\n    @Override\n    public int getICount() {\n        return searchBooks.size();\n    }\n\n    public void setCallback(Callback callback) {\n        this.callback = callback;\n    }\n\n    public void addAll(List<SearchBookBean> newData) {\n        if (newData != null && newData.size() > 0) {\n            int position = getICount();\n            if (newData.size() > 0) {\n                searchBooks.addAll(newData);\n            }\n            notifyItemInserted(position);\n            notifyItemRangeChanged(position, newData.size());\n        }\n    }\n\n    public void replaceAll(List<SearchBookBean> newData) {\n        searchBooks.clear();\n        if (newData != null && newData.size() > 0) {\n            searchBooks.addAll(newData);\n        }\n        notifyDataSetChanged();\n    }\n\n    public interface Callback {\n        void clickItem(View animView, int position, SearchBookBean searchBookBean);\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        ViewGroup flContent;\n        CoverImageView ivCover;\n        TextView tvName;\n        TextView tvState;\n        TextView tvWords;\n        TextView tvKind;\n        TextView tvLasted;\n        TextView tvOrigin;\n        TextView tvIntroduce;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            flContent = itemView.findViewById(R.id.fl_content);\n            ivCover = itemView.findViewById(R.id.iv_cover);\n            tvName = itemView.findViewById(R.id.tv_name);\n            tvState = itemView.findViewById(R.id.tv_state);\n            tvWords = itemView.findViewById(R.id.tv_words);\n            tvLasted = itemView.findViewById(R.id.tv_lasted);\n            tvKind = itemView.findViewById(R.id.tv_kind);\n            tvOrigin = itemView.findViewById(R.id.tv_origin);\n            tvIntroduce = itemView.findViewById(R.id.tv_introduce);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/DownloadAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.graphics.PorterDuff;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.DownloadBookBean;\nimport com.kunfei.bookshelf.service.DownloadService;\nimport com.kunfei.bookshelf.view.activity.DownloadActivity;\nimport com.kunfei.bookshelf.widget.image.CoverImageView;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\n\npublic class DownloadAdapter extends RecyclerView.Adapter<DownloadAdapter.MyViewHolder> {\n    private DownloadActivity activity;\n    private List<DownloadBookBean> data;\n    private final Object mLock = new Object();\n\n    public DownloadAdapter(DownloadActivity activity) {\n        this.activity = activity;\n        data = new ArrayList<>();\n    }\n\n\n    public void upDataS(List<DownloadBookBean> dataS) {\n        synchronized (mLock) {\n            this.data.clear();\n            if (dataS != null) {\n                this.data.addAll(dataS);\n                Collections.sort(this.data);\n            }\n        }\n        if (dataS != null) {\n            notifyDataSetChanged();\n        }\n    }\n\n    public void upData(DownloadBookBean data) {\n        int index = -1;\n        synchronized (mLock) {\n            if (data != null && !this.data.isEmpty()) {\n                index = this.data.indexOf(data);\n                if (index >= 0) {\n                    this.data.set(index, data);\n                }\n            }\n        }\n        if (index >= 0) {\n            notifyItemChanged(index, data.getWaitingCount());\n        }\n    }\n\n    public void removeData(DownloadBookBean data) {\n        int index = -1;\n        synchronized (mLock) {\n            if (data != null && !this.data.isEmpty()) {\n                index = this.data.indexOf(data);\n                if (index >= 0) {\n                    this.data.remove(index);\n                }\n            }\n        }\n        if (index >= 0) {\n            notifyItemRemoved(index);\n        }\n    }\n\n    public void addData(DownloadBookBean data) {\n        synchronized (mLock) {\n            if (data != null) {\n                this.data.add(data);\n            }\n        }\n        if (data != null) {\n            notifyItemInserted(this.data.size() - 1);\n        }\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_download, parent, false);\n        return new MyViewHolder(view);\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, int i) {\n\n    }\n\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position, @NonNull List<Object> payloads) {\n        final DownloadBookBean item = data.get(holder.getLayoutPosition());\n        if (!payloads.isEmpty()) {\n            holder.tvName.setText(String.format(Locale.getDefault(), \"%s(正在下载)\", item.getName()));\n            holder.tvDownload.setText(activity.getString(R.string.un_download, (Integer) payloads.get(0)));\n        } else {\n            holder.ivDel.getDrawable().mutate();\n            holder.ivDel.getDrawable().setColorFilter(activity.getResources().getColor(R.color.tv_text_default), PorterDuff.Mode.SRC_ATOP);\n            holder.ivCover.load(item.getCoverUrl(), item.getName(), null);\n            if (item.getSuccessCount() > 0) {\n                holder.tvName.setText(String.format(Locale.getDefault(), \"%s(正在下载)\", item.getName()));\n            } else {\n                holder.tvName.setText(String.format(Locale.getDefault(), \"%s(等待下载)\", item.getName()));\n            }\n            holder.tvDownload.setText(activity.getString(R.string.un_download, item.getDownloadCount() - item.getSuccessCount()));\n            holder.ivDel.setOnClickListener(view -> DownloadService.removeDownload(activity, item.getNoteUrl()));\n        }\n    }\n\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        CoverImageView ivCover;\n        TextView tvName;\n        TextView tvDownload;\n        ImageView ivDel;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            ivCover = itemView.findViewById(R.id.iv_cover);\n            tvName = itemView.findViewById(R.id.tv_name);\n            tvDownload = itemView.findViewById(R.id.tv_download);\n            ivDel = itemView.findViewById(R.id.iv_delete);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/FileSystemAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.view.adapter.base.BaseListAdapter;\nimport com.kunfei.bookshelf.view.adapter.base.IViewHolder;\nimport com.kunfei.bookshelf.view.adapter.view.FileHolder;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\n/**\n * Created by newbiechen on 17-5-27.\n */\n\npublic class FileSystemAdapter extends BaseListAdapter<File> {\n    //记录item是否被选中的Map\n    private HashMap<File, Boolean> mCheckMap = new HashMap<>();\n    private int mCheckedCount = 0;\n\n    @Override\n    protected IViewHolder<File> createViewHolder(int viewType) {\n        return new FileHolder(mCheckMap);\n    }\n\n    @Override\n    public void refreshItems(List<File> list) {\n        mCheckMap.clear();\n        for (File file : list) {\n            mCheckMap.put(file, false);\n        }\n        super.refreshItems(list);\n    }\n\n    @Override\n    public void addItem(File value) {\n        mCheckMap.put(value, false);\n        super.addItem(value);\n    }\n\n    @Override\n    public void addItem(int index, File value) {\n        mCheckMap.put(value, false);\n        super.addItem(index, value);\n    }\n\n    @Override\n    public void addItems(List<File> values) {\n        for (File file : values) {\n            mCheckMap.put(file, false);\n        }\n        super.addItems(values);\n    }\n\n    @Override\n    public void removeItem(File value) {\n        mCheckMap.remove(value);\n        super.removeItem(value);\n    }\n\n    @Override\n    public void removeItems(List<File> value) {\n        //删除在HashMap中的文件\n        for (File file : value) {\n            mCheckMap.remove(file);\n            //因为，能够被移除的文件，肯定是选中的\n            --mCheckedCount;\n        }\n        //删除列表中的文件\n        super.removeItems(value);\n    }\n\n    //设置点击切换\n    public void setCheckedItem(int pos) {\n        File file = getItem(pos);\n        if (isFileLoaded(file.getAbsolutePath())) return;\n\n        boolean isSelected = mCheckMap.get(file);\n        if (isSelected) {\n            mCheckMap.put(file, false);\n            --mCheckedCount;\n        } else {\n            mCheckMap.put(file, true);\n            ++mCheckedCount;\n        }\n        notifyDataSetChanged();\n    }\n\n    public void setCheckedAll(boolean isChecked) {\n        Set<Map.Entry<File, Boolean>> entrys = mCheckMap.entrySet();\n        mCheckedCount = 0;\n        for (Map.Entry<File, Boolean> entry : entrys) {\n            //必须是文件，必须没有被收藏\n            if (entry.getKey().isFile() && !isFileLoaded(entry.getKey().getAbsolutePath())) {\n                entry.setValue(isChecked);\n                //如果选中，则增加点击的数量\n                if (isChecked) {\n                    ++mCheckedCount;\n                }\n            }\n        }\n        notifyDataSetChanged();\n    }\n\n    private boolean isFileLoaded(String id) {\n        //如果是已加载的文件，则点击事件无效。\n        return BookshelfHelp.getBook(id) != null;\n    }\n\n    public int getCheckableCount() {\n        List<File> files = getItems();\n        int count = 0;\n        for (File file : files) {\n            if (!isFileLoaded(file.getAbsolutePath()) && file.isFile())\n                ++count;\n        }\n        return count;\n    }\n\n    public boolean getItemIsChecked(int pos) {\n        File file = getItem(pos);\n        return mCheckMap.get(file);\n    }\n\n    public List<File> getCheckedFiles() {\n        List<File> files = new ArrayList<>();\n        Set<Map.Entry<File, Boolean>> entrys = mCheckMap.entrySet();\n        for (Map.Entry<File, Boolean> entry : entrys) {\n            if (entry.getValue()) {\n                files.add(entry.getKey());\n            }\n        }\n        return files;\n    }\n\n    public int getCheckedCount() {\n        return mCheckedCount;\n    }\n\n    public HashMap<File, Boolean> getCheckMap() {\n        return mCheckMap;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/FindKindAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.appcompat.widget.AppCompatImageView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.FindKindBean;\nimport com.kunfei.bookshelf.bean.FindKindGroupBean;\nimport com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandAbleViewHolder;\nimport com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandableRecyclerAdapter;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.GroupItem;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData;\n\nimport java.util.List;\n\n/**\n * Created by GKF on 2017/12/22.\n * 书源Adapter\n */\n\npublic class FindKindAdapter extends BaseExpandableRecyclerAdapter<FindKindGroupBean, FindKindBean, FindKindAdapter.MyViewHolder> {\n\n    public FindKindAdapter(Context ctx, List<RecyclerViewData> datas) {\n        super(ctx, datas);\n    }\n\n    /**\n     * return groupView\n     */\n    @Override\n    public View getGroupView(ViewGroup parent) {\n        return LayoutInflater.from(parent.getContext()).inflate(R.layout.item_find1_group, parent, false);\n    }\n\n    /**\n     * return childView\n     */\n    @Override\n    public View getChildView(ViewGroup parent) {\n        return LayoutInflater.from(parent.getContext()).inflate(R.layout.item_find1_kind, parent, false);\n    }\n\n    /**\n     * return <VH extends BaseViewHolder> instance\n     */\n    @Override\n    public MyViewHolder createRealViewHolder(Context ctx, View view, int viewType) {\n        return new MyViewHolder(ctx, view, viewType);\n    }\n\n    /**\n     * onBind groupData to groupView\n     */\n    @Override\n    public void onBindGroupHolder(MyViewHolder holder, int groupPos, int position, FindKindGroupBean groupData) {\n        holder.textView.setText(groupData.getGroupName());\n        GroupItem item = getAllDatas().get(groupPos).getGroupItem();\n        if (item.isExpand()) {\n            holder.imageView.setImageResource(R.drawable.ic_expand_less_24dp);\n        } else {\n            holder.imageView.setImageResource(R.drawable.ic_expand_more_24dp);\n        }\n    }\n\n    /**\n     * onBind childData to childView\n     */\n    @Override\n    public void onBindChildpHolder(MyViewHolder holder, int groupPos, int childPos, int position, FindKindBean childData) {\n        holder.textView.setText(childData.getKindName());\n    }\n\n    public class MyViewHolder extends BaseExpandAbleViewHolder {\n        TextView textView;\n        AppCompatImageView imageView;\n\n        public MyViewHolder(Context ctx, View itemView, int viewType) {\n            super(ctx, itemView, viewType);\n            textView = itemView.findViewById(R.id.tv_kind_name);\n            if (viewType == VIEW_TYPE_PARENT) {\n                imageView = itemView.findViewById(R.id.iv_group);\n            }\n        }\n\n        /**\n         * return ChildView root layout id\n         */\n        @Override\n        public int getChildViewResId() {\n            return R.id.ll_content;\n        }\n\n        /**\n         * return GroupView root layout id\n         */\n        @Override\n        public int getGroupViewResId() {\n            return R.id.ll_content;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/FindLeftAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.FindKindGroupBean;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class FindLeftAdapter extends RecyclerView.Adapter<FindLeftAdapter.MyViewHolder> {\n    private Context context;\n    private int showIndex = 0;\n    private List<RecyclerViewData> data = new ArrayList<>();\n    private OnClickListener onClickListener;\n\n    public FindLeftAdapter(Context context, OnClickListener onClickListener) {\n        this.context = context;\n        this.onClickListener = onClickListener;\n    }\n\n    public void setData(List<RecyclerViewData> data) {\n        this.data.clear();\n        this.data.addAll(data);\n        notifyDataSetChanged();\n    }\n\n    public void upShowIndex(int showIndex) {\n        if (showIndex != this.showIndex) {\n            int oldIndex = this.showIndex;\n            this.showIndex = showIndex;\n            notifyItemChanged(oldIndex);\n            notifyItemChanged(this.showIndex);\n        }\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {\n        return new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_find_left, viewGroup, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder myViewHolder, @SuppressLint(\"RecyclerView\") int i) {\n        FindKindGroupBean groupBean = (FindKindGroupBean) data.get(i).getGroupData();\n        myViewHolder.tvSourceName.setText(groupBean.getGroupName());\n        if (i == showIndex) {\n            myViewHolder.tvSourceName.setBackgroundColor(context.getResources().getColor(R.color.transparent30));\n        } else {\n            myViewHolder.tvSourceName.setBackgroundColor(Color.TRANSPARENT);\n        }\n        myViewHolder.tvSourceName.setOnClickListener(v -> {\n            if (onClickListener != null) {\n                int oldIndex = showIndex;\n                showIndex = i;\n                notifyItemChanged(oldIndex);\n                notifyItemChanged(showIndex);\n                onClickListener.click(showIndex);\n            }\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        TextView tvSourceName;\n\n        public MyViewHolder(@NonNull View itemView) {\n            super(itemView);\n            tvSourceName = itemView.findViewById(R.id.tv_source_name);\n        }\n    }\n\n    public interface OnClickListener {\n        void click(int pos);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/FindRightAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.FindKindBean;\nimport com.kunfei.bookshelf.bean.FindKindGroupBean;\nimport com.kunfei.bookshelf.view.activity.ChoiceBookActivity;\nimport com.kunfei.bookshelf.widget.recycler.expandable.OnRecyclerViewListener;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData;\nimport com.kunfei.bookshelf.widget.recycler.sectioned.SectionedRecyclerViewAdapter;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class FindRightAdapter extends SectionedRecyclerViewAdapter<FindRightAdapter.HeaderHolder, FindRightAdapter.DescHolder, RecyclerView.ViewHolder> {\n    private List<RecyclerViewData> data = new ArrayList<>();\n    private LayoutInflater inflater;\n    private Context context;\n    private OnRecyclerViewListener.OnItemLongClickListener onItemLongClickListener;\n\n    public FindRightAdapter(Context context, OnRecyclerViewListener.OnItemLongClickListener onItemLongClickListener) {\n        this.context = context;\n        this.onItemLongClickListener = onItemLongClickListener;\n        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);\n    }\n\n    public void setData(List<RecyclerViewData> data) {\n        this.data.clear();\n        this.data.addAll(data);\n        notifyDataSetChanged();\n    }\n\n    @Override\n    protected int getSectionCount() {\n        return data.size();\n    }\n\n    @Override\n    protected int getItemCountForSection(int section) {\n        return data.get(section).getChildList().size();\n    }\n\n    @Override\n    protected boolean hasFooterInSection(int section) {\n        return false;\n    }\n\n    @Override\n    protected HeaderHolder onCreateSectionHeaderViewHolder(ViewGroup parent, int viewType) {\n        return new HeaderHolder(inflater.inflate(R.layout.item_find2_header_view, parent, false));\n    }\n\n    @Override\n    protected RecyclerView.ViewHolder onCreateSectionFooterViewHolder(ViewGroup parent, int viewType) {\n        return null;\n    }\n\n    @Override\n    protected DescHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {\n        return new DescHolder(inflater.inflate(R.layout.item_find2_childer_view, parent, false));\n    }\n\n    @Override\n    protected void onBindSectionHeaderViewHolder(HeaderHolder holder, int section) {\n        RecyclerViewData recyclerViewData = data.get(section);\n        holder.tv_source_name.setText(((FindKindGroupBean) recyclerViewData.getGroupData()).getGroupName());\n        holder.tv_source_name.setOnLongClickListener(v -> {\n            if (onItemLongClickListener != null) {\n                onItemLongClickListener.onGroupItemLongClick(section, section, holder.tv_source_name);\n            }\n            return true;\n        });\n    }\n\n    @Override\n    protected void onBindSectionFooterViewHolder(RecyclerView.ViewHolder holder, int section) {\n\n    }\n\n    @Override\n    protected void onBindItemViewHolder(DescHolder holder, int section, int position) {\n        try {\n            FindKindBean kindBean = (FindKindBean) data.get(section).getChild(position);\n            holder.tv_item.setHorizontallyScrolling(false);\n            holder.tv_item.setText(kindBean.getKindName());\n            holder.tv_item.setOnClickListener(view -> {\n                Intent intent = new Intent(context, ChoiceBookActivity.class);\n                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n                intent.putExtra(\"url\", kindBean.getKindUrl());\n                intent.putExtra(\"title\", kindBean.getKindName());\n                intent.putExtra(\"tag\", kindBean.getTag());\n                context.startActivity(intent);\n            });\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    public List<RecyclerViewData> getData() {\n        return data;\n    }\n\n    static class HeaderHolder extends RecyclerView.ViewHolder {\n        TextView tv_source_name;\n\n        HeaderHolder(View itemView) {\n            super(itemView);\n            tv_source_name = itemView.findViewById(R.id.tv_source_name);\n        }\n    }\n\n    static class DescHolder extends RecyclerView.ViewHolder {\n        TextView tv_item;\n\n        DescHolder(View view) {\n            super(view);\n            tv_item = view.findViewById(R.id.tv_item);\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/ReplaceRuleAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CheckBox;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.activity.ReplaceRuleActivity;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Created by GKF on 2017/12/22.\n * 书源Adapter\n */\n\npublic class ReplaceRuleAdapter extends RecyclerView.Adapter<ReplaceRuleAdapter.MyViewHolder> {\n    private List<ReplaceRuleBean> data;\n    private ReplaceRuleActivity activity;\n    private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() {\n        @Override\n        public void onSwiped(int adapterPosition) {\n\n        }\n\n        @Override\n        public boolean onMove(int srcPosition, int targetPosition) {\n            Collections.swap(data, srcPosition, targetPosition);\n            notifyItemMoved(srcPosition, targetPosition);\n            notifyItemChanged(srcPosition);\n            notifyItemChanged(targetPosition);\n            activity.saveDataS();\n            return true;\n        }\n    };\n\n    public ReplaceRuleAdapter(ReplaceRuleActivity activity) {\n        this.activity = activity;\n        data = new ArrayList<>();\n    }\n\n    public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() {\n        return itemTouchCallbackListener;\n    }\n\n    public void resetDataS(List<ReplaceRuleBean> dataList) {\n        this.data.clear();\n        this.data.addAll(dataList);\n        notifyDataSetChanged();\n        activity.upDateSelectAll();\n    }\n\n    public List<ReplaceRuleBean> getData() {\n        return data;\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_replace_rule, parent, false);\n        return new MyViewHolder(view);\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {\n        holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity));\n        holder.checkBox.setText(data.get(position).getReplaceSummary());\n        holder.checkBox.setChecked(data.get(position).getEnable());\n        holder.checkBox.setOnClickListener((View view) -> {\n            data.get(position).setEnable(holder.checkBox.isChecked());\n            activity.upDateSelectAll();\n            activity.saveDataS();\n        });\n        holder.editView.setOnClickListener(view -> activity.editReplaceRule(data.get(position)));\n        holder.delView.setOnClickListener(view -> {\n            activity.delData(data.get(position));\n            data.remove(position);\n            notifyDataSetChanged();\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        CheckBox checkBox;\n        ImageView editView;\n        ImageView delView;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            checkBox = itemView.findViewById(R.id.cb_replace_rule);\n            editView = itemView.findViewById(R.id.iv_edit);\n            delView = itemView.findViewById(R.id.iv_del);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/SearchBookAdapter.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.adapter;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.os.AsyncTask;\nimport android.text.TextUtils;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.bumptech.glide.Glide;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookKindBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.adapter.base.BaseListAdapter;\nimport com.kunfei.bookshelf.widget.image.CoverImageView;\nimport com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerViewAdapter;\n\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.kunfei.bookshelf.utils.StringUtils.isTrimEmpty;\n\n\npublic class SearchBookAdapter extends RefreshRecyclerViewAdapter {\n    private WeakReference<Activity> activityRef;\n    private List<SearchBookBean> searchBooks;\n    private BaseListAdapter.OnItemClickListener itemClickListener;\n\n    public SearchBookAdapter(Activity activity) {\n        super(true);\n        this.activityRef = new WeakReference<>(activity);\n        searchBooks = new ArrayList<>();\n    }\n\n    @Override\n    public RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_search_book, parent, false));\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    @Override\n    public void onBindIViewHolder(final RecyclerView.ViewHolder holder, final int position) {\n        Activity activity = activityRef.get();\n        holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity));\n        MyViewHolder myViewHolder = (MyViewHolder) holder;\n        myViewHolder.flContent.setOnClickListener(v -> {\n            if (itemClickListener != null)\n                itemClickListener.onItemClick(v, position);\n        });\n        SearchBookBean book = searchBooks.get(position);\n        if (!activity.isFinishing()) {\n            myViewHolder.ivCover.load(book.getCoverUrl(), book.getName(), book.getAuthor());\n        }\n        myViewHolder.tvName.setText(String.format(\"%s (%s)\", book.getName(), book.getAuthor()));\n        BookKindBean bookKindBean = new BookKindBean(book.getKind());\n        if (isTrimEmpty(bookKindBean.getKind())) {\n            myViewHolder.tvKind.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvKind.setVisibility(View.VISIBLE);\n            myViewHolder.tvKind.setText(bookKindBean.getKind());\n        }\n        if (isTrimEmpty(bookKindBean.getWordsS())) {\n            myViewHolder.tvWords.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvWords.setVisibility(View.VISIBLE);\n            myViewHolder.tvWords.setText(bookKindBean.getWordsS());\n        }\n        if (isTrimEmpty(bookKindBean.getState())) {\n            myViewHolder.tvState.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvState.setVisibility(View.VISIBLE);\n            myViewHolder.tvState.setText(bookKindBean.getState());\n        }\n        //来源\n        if (isTrimEmpty(book.getOrigin())) {\n            myViewHolder.tvOrigin.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvOrigin.setVisibility(View.VISIBLE);\n            myViewHolder.tvOrigin.setText(activity.getString(R.string.origin_format, book.getOrigin()));\n        }\n        //最新章节\n        if (isTrimEmpty(book.getLastChapter())) {\n            myViewHolder.tvLasted.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvLasted.setText(book.getLastChapter());\n            myViewHolder.tvLasted.setVisibility(View.VISIBLE);\n        }\n        //简介\n        if (isTrimEmpty(book.getIntroduce())) {\n            myViewHolder.tvIntroduce.setVisibility(View.GONE);\n        } else {\n            myViewHolder.tvIntroduce.setText(StringUtils.formatHtml(book.getIntroduce()));\n            myViewHolder.tvIntroduce.setVisibility(View.VISIBLE);\n        }\n        myViewHolder.tvOriginNum.setText(String.format(\"共%d个源\", book.getOriginNum()));\n    }\n\n    @Override\n    public int getIViewType(int position) {\n        return 0;\n    }\n\n    @Override\n    public int getICount() {\n        return searchBooks.size();\n    }\n\n    public void setItemClickListener(BaseListAdapter.OnItemClickListener itemClickListener) {\n        this.itemClickListener = itemClickListener;\n    }\n\n    public synchronized void upData(DataAction action, List<SearchBookBean> newDataS) {\n        switch (action) {\n            case ADD:\n                searchBooks = newDataS;\n                notifyDataSetChanged();\n                break;\n            case CLEAR:\n                if (!searchBooks.isEmpty()) {\n                    try {\n                        Glide.with(activityRef.get()).onDestroy();\n                    } catch (Exception ignored) {\n                    }\n                    searchBooks.clear();\n                    notifyDataSetChanged();\n                }\n                break;\n        }\n    }\n\n    public synchronized void addAll(List<SearchBookBean> newDataS, String keyWord) {\n        List<SearchBookBean> copyDataS = new ArrayList<>(searchBooks);\n        if (newDataS != null && newDataS.size() > 0) {\n            saveData(newDataS);\n            List<SearchBookBean> searchBookBeansAdd = new ArrayList<>();\n            if (copyDataS.size() == 0) {\n                copyDataS.addAll(newDataS);\n            } else {\n                //存在\n                for (SearchBookBean temp : newDataS) {\n                    boolean hasSame = false;\n                    for (int i = 0, size = copyDataS.size(); i < size; i++) {\n                        SearchBookBean searchBook = copyDataS.get(i);\n                        if (TextUtils.equals(temp.getName(), searchBook.getName())\n                                && TextUtils.equals(temp.getAuthor(), searchBook.getAuthor())) {\n                            hasSame = true;\n                            searchBook.addOriginUrl(temp.getTag());\n                            break;\n                        }\n                    }\n\n                    if (!hasSame) {\n                        searchBookBeansAdd.add(temp);\n                    }\n                }\n                //添加\n                for (SearchBookBean temp : searchBookBeansAdd) {\n                    if (TextUtils.equals(keyWord, temp.getName())) {\n                        for (int i = 0; i < copyDataS.size(); i++) {\n                            SearchBookBean searchBook = copyDataS.get(i);\n                            if (!TextUtils.equals(keyWord, searchBook.getName())) {\n                                copyDataS.add(i, temp);\n                                break;\n                            }\n                        }\n                    } else if (TextUtils.equals(keyWord, temp.getAuthor())) {\n                        for (int i = 0; i < copyDataS.size(); i++) {\n                            SearchBookBean searchBook = copyDataS.get(i);\n                            if (!TextUtils.equals(keyWord, searchBook.getName()) && !TextUtils.equals(keyWord, searchBook.getAuthor())) {\n                                copyDataS.add(i, temp);\n                                break;\n                            }\n                        }\n                    } else {\n                        copyDataS.add(temp);\n                    }\n                }\n            }\n            Activity activity = activityRef.get();\n            if (activity != null) {\n                activity.runOnUiThread(() -> upData(DataAction.ADD, copyDataS));\n            }\n        }\n    }\n\n    private void saveData(List<SearchBookBean> data) {\n        AsyncTask.execute(() -> DbHelper.getDaoSession().getSearchBookBeanDao().insertOrReplaceInTx(data));\n    }\n\n    public SearchBookBean getItemData(int pos) {\n        return searchBooks.get(pos);\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        ViewGroup flContent;\n        CoverImageView ivCover;\n        TextView tvName;\n        TextView tvState;\n        TextView tvWords;\n        TextView tvKind;\n        TextView tvLasted;\n        TextView tvOrigin;\n        TextView tvOriginNum;\n        TextView tvIntroduce;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            flContent = itemView.findViewById(R.id.fl_content);\n            ivCover = itemView.findViewById(R.id.iv_cover);\n            tvName = itemView.findViewById(R.id.tv_name);\n            tvState = itemView.findViewById(R.id.tv_state);\n            tvWords = itemView.findViewById(R.id.tv_words);\n            tvLasted = itemView.findViewById(R.id.tv_lasted);\n            tvKind = itemView.findViewById(R.id.tv_kind);\n            tvOrigin = itemView.findViewById(R.id.tv_origin);\n            tvOriginNum = itemView.findViewById(R.id.tv_origin_num);\n            tvIntroduce = itemView.findViewById(R.id.tv_introduce);\n        }\n    }\n\n    public enum DataAction {\n        ADD, CLEAR\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/SearchBookshelfAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookInfoBean;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class SearchBookshelfAdapter extends RecyclerView.Adapter<SearchBookshelfAdapter.MyViewHolder> {\n\n    private List<BookInfoBean> beans = new ArrayList<>();\n    private CallBack callBack;\n\n    public SearchBookshelfAdapter(CallBack callBack) {\n        this.callBack = callBack;\n    }\n\n    public void setItems(List<BookInfoBean> beans) {\n        this.beans.clear();\n        this.beans.addAll(beans);\n        notifyDataSetChanged();\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_search_history, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {\n        holder.textView.setText(beans.get(position).getName());\n        holder.itemView.setOnClickListener(v -> callBack.openBookInfo(beans.get(position)));\n    }\n\n    @Override\n    public int getItemCount() {\n        return beans.size();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        TextView textView;\n\n        public MyViewHolder(@NonNull View itemView) {\n            super(itemView);\n            textView = itemView.findViewById(R.id.tv);\n        }\n    }\n\n    public interface CallBack {\n        void openBookInfo(BookInfoBean bookInfoBean);\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/SourceDebugAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.RecyclerView.Adapter;\n\nimport com.kunfei.bookshelf.R;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class SourceDebugAdapter extends Adapter<SourceDebugAdapter.MyViewHolder> {\n    private Context context;\n    private List<String> data = new ArrayList<>();\n\n    public SourceDebugAdapter(Context context) {\n        this.context = context;\n    }\n\n    public void clearData() {\n        data.clear();\n        notifyDataSetChanged();\n    }\n\n    public void addData(String msg) {\n        data.add(msg);\n        notifyItemChanged(data.size() - 1);\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_source_debug, parent, false));\n    }\n\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {\n        if (holder.textView.getTag(R.id.tag1) == null) {\n            View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() {\n                @Override\n                public void onViewAttachedToWindow(View v) {\n                    holder.textView.setCursorVisible(false);\n                    holder.textView.setCursorVisible(true);\n                }\n\n                @Override\n                public void onViewDetachedFromWindow(View v) {\n\n                }\n            };\n            holder.textView.addOnAttachStateChangeListener(listener);\n            holder.textView.setTag(R.id.tag1, listener);\n        }\n        holder.textView.setText(data.get(position));\n    }\n\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        TextView textView;\n\n        public MyViewHolder(@NonNull View itemView) {\n            super(itemView);\n            textView = itemView.findViewById(R.id.tv);\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/SourceEditAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.content.Context;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.AppCompatEditText;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.RecyclerView.Adapter;\n\nimport com.google.android.material.textfield.TextInputLayout;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.view.activity.SourceEditActivity;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class SourceEditAdapter extends Adapter<SourceEditAdapter.MyViewHolder> {\n    private Context context;\n    private List<SourceEditActivity.SourceEdit> data = new ArrayList<>();\n\n    public SourceEditAdapter(Context context) {\n        this.context = context;\n    }\n\n    public void reSetData(List<SourceEditActivity.SourceEdit> data) {\n        this.data = data;\n        notifyDataSetChanged();\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_source_edit, parent, false));\n    }\n\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {\n        if (holder.editText.getTag(R.id.tag1) == null) {\n            View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() {\n                @Override\n                public void onViewAttachedToWindow(View v) {\n                    holder.editText.setCursorVisible(false);\n                    holder.editText.setCursorVisible(true);\n                    holder.editText.setFocusable(true);\n                    holder.editText.setFocusableInTouchMode(true);\n                }\n\n                @Override\n                public void onViewDetachedFromWindow(View v) {\n\n                }\n            };\n            holder.editText.addOnAttachStateChangeListener(listener);\n            holder.editText.setTag(R.id.tag1, listener);\n        }\n        if (holder.editText.getTag(R.id.tag2) != null && holder.editText.getTag(R.id.tag2) instanceof TextWatcher) {\n            holder.editText.removeTextChangedListener((TextWatcher) holder.editText.getTag(R.id.tag2));\n        }\n        holder.editText.setText(data.get(position).getValue());\n        holder.textInputLayout.setHint(context.getString(data.get(position).getHint()));\n        TextWatcher textWatcher = new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n\n            }\n\n            @Override\n            public void onTextChanged(CharSequence s, int start, int before, int count) {\n\n            }\n\n            @Override\n            public void afterTextChanged(Editable s) {\n                data.get(position).setValue(s == null ? null : s.toString());\n            }\n        };\n        holder.editText.addTextChangedListener(textWatcher);\n        holder.editText.setTag(R.id.tag2, textWatcher);\n    }\n\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        TextInputLayout textInputLayout;\n        AppCompatEditText editText;\n\n        public MyViewHolder(@NonNull View itemView) {\n            super(itemView);\n            textInputLayout = itemView.findViewById(R.id.textInputLayout);\n            editText = itemView.findViewById(R.id.editText);\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/TxtChapterRuleAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CheckBox;\nimport android.widget.ImageView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.TxtChapterRuleBean;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.activity.TxtChapterRuleActivity;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Created by GKF on 2017/12/22.\n * 书源Adapter\n */\n\npublic class TxtChapterRuleAdapter extends RecyclerView.Adapter<TxtChapterRuleAdapter.MyViewHolder> {\n    private List<TxtChapterRuleBean> data;\n    private TxtChapterRuleActivity activity;\n    private ItemTouchCallback.OnItemTouchCallbackListener itemTouchCallbackListener = new ItemTouchCallback.OnItemTouchCallbackListener() {\n        @Override\n        public void onSwiped(int adapterPosition) {\n\n        }\n\n        @Override\n        public boolean onMove(int srcPosition, int targetPosition) {\n            Collections.swap(data, srcPosition, targetPosition);\n            notifyItemMoved(srcPosition, targetPosition);\n            notifyItemChanged(srcPosition);\n            notifyItemChanged(targetPosition);\n            activity.saveDataS();\n            return true;\n        }\n    };\n\n    public TxtChapterRuleAdapter(TxtChapterRuleActivity activity) {\n        this.activity = activity;\n        data = new ArrayList<>();\n    }\n\n    public ItemTouchCallback.OnItemTouchCallbackListener getItemTouchCallbackListener() {\n        return itemTouchCallbackListener;\n    }\n\n    public void resetDataS(List<TxtChapterRuleBean> dataList) {\n        this.data.clear();\n        this.data.addAll(dataList);\n        notifyDataSetChanged();\n        activity.upDateSelectAll();\n    }\n\n    public List<TxtChapterRuleBean> getData() {\n        return data;\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_replace_rule, parent, false);\n        return new MyViewHolder(view);\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {\n        holder.itemView.setBackgroundColor(ThemeStore.backgroundColor(activity));\n        holder.checkBox.setText(data.get(position).getName());\n        holder.checkBox.setChecked(data.get(position).getEnable());\n        holder.checkBox.setOnClickListener((View view) -> {\n            data.get(position).setEnable(holder.checkBox.isChecked());\n            activity.upDateSelectAll();\n            activity.saveDataS();\n        });\n        holder.editView.setOnClickListener(view -> activity.editChapterRule(data.get(position)));\n        holder.delView.setOnClickListener(view -> {\n            activity.delData(data.get(position));\n            data.remove(position);\n            notifyDataSetChanged();\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        CheckBox checkBox;\n        ImageView editView;\n        ImageView delView;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            checkBox = itemView.findViewById(R.id.cb_replace_rule);\n            editView = itemView.findViewById(R.id.iv_edit);\n            delView = itemView.findViewById(R.id.iv_del);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/base/BaseListAdapter.java",
    "content": "package com.kunfei.bookshelf.view.adapter.base;\n\nimport android.annotation.SuppressLint;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * Created by newbiechen on 17-3-21.\n */\n\npublic abstract class BaseListAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {\n\n    private static final String TAG = \"BaseListAdapter\";\n    /*common statement*/\n    protected final List<T> mList = new ArrayList<>();\n    protected OnItemClickListener mClickListener;\n    protected OnItemLongClickListener mLongClickListener;\n\n    /************************abstract area************************/\n    protected abstract IViewHolder<T> createViewHolder(int viewType);\n\n    /*************************rewrite logic area***************************************/\n    @NonNull\n    @Override\n    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        IViewHolder<T> viewHolder = createViewHolder(viewType);\n\n        View view = viewHolder.createItemView(parent);\n        //初始化\n        return new BaseViewHolder<>(view, viewHolder);\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {\n        //防止别人直接使用RecyclerView.ViewHolder调用该方法\n        if (!(holder instanceof BaseViewHolder))\n            throw new IllegalArgumentException(\"The ViewHolder item must extend BaseViewHolder\");\n\n        @SuppressWarnings({\"rawtypes\", \"unchecked\"})\n        IViewHolder<T> iHolder = ((BaseViewHolder) holder).holder;\n        iHolder.onBind(getItem(position), position);\n\n        //设置点击事件\n        holder.itemView.setOnClickListener((v) -> {\n            if (mClickListener != null) {\n                mClickListener.onItemClick(v, position);\n            }\n            //adapter监听点击事件\n            iHolder.onClick();\n            onItemClick(v, position);\n        });\n        //设置长点击事件\n        holder.itemView.setOnLongClickListener(\n                (v) -> {\n                    boolean isClicked = false;\n                    if (mLongClickListener != null) {\n                        isClicked = mLongClickListener.onItemLongClick(v, position);\n                    }\n                    //Adapter监听长点击事件\n                    onItemLongClick(v, position);\n                    return isClicked;\n                }\n        );\n    }\n\n    @Override\n    public int getItemCount() {\n        return mList.size();\n    }\n\n    protected void onItemClick(View v, int pos) {\n    }\n\n    protected void onItemLongClick(View v, int pos) {\n    }\n\n    /******************************public area***********************************/\n\n    public void setOnItemClickListener(OnItemClickListener mListener) {\n        this.mClickListener = mListener;\n    }\n\n    public void setOnItemLongClickListener(OnItemLongClickListener mListener) {\n        this.mLongClickListener = mListener;\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    public void addItem(T value) {\n        mList.add(value);\n        notifyDataSetChanged();\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    public void addItem(int index, T value) {\n        mList.add(index, value);\n        notifyDataSetChanged();\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    public void addItems(List<T> values) {\n        mList.addAll(values);\n        notifyDataSetChanged();\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    public void removeItem(T value) {\n        mList.remove(value);\n        notifyDataSetChanged();\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    public void removeItems(List<T> value) {\n        mList.removeAll(value);\n        notifyDataSetChanged();\n    }\n\n    public T getItem(int position) {\n        return mList.get(position);\n    }\n\n    public List<T> getItems() {\n        return Collections.unmodifiableList(mList);\n    }\n\n    public int getItemSize() {\n        return mList.size();\n    }\n\n    public void refreshItems(List<T> list) {\n        mList.clear();\n        mList.addAll(list);\n        notifyDataSetChanged();\n    }\n\n    public void clear() {\n        mList.clear();\n    }\n\n    /***************************inner class area***********************************/\n    public interface OnItemClickListener {\n        void onItemClick(View view, int pos);\n    }\n\n    public interface OnItemLongClickListener {\n        boolean onItemLongClick(View view, int pos);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/base/BaseViewHolder.java",
    "content": "package com.kunfei.bookshelf.view.adapter.base;\n\nimport android.view.View;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by newbiechen on 17-5-17.\n */\n\npublic class BaseViewHolder<T> extends RecyclerView.ViewHolder {\n    public IViewHolder<T> holder;\n\n    public BaseViewHolder(View itemView, IViewHolder<T> holder) {\n        super(itemView);\n        this.holder = holder;\n        holder.initView();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/base/IViewHolder.java",
    "content": "package com.kunfei.bookshelf.view.adapter.base;\n\nimport android.view.View;\nimport android.view.ViewGroup;\n\n/**\n * Created by newbiechen on 17-5-17.\n */\n\npublic interface IViewHolder<T> {\n    View createItemView(ViewGroup parent);\n\n    void initView();\n\n    void onBind(T data, int pos);\n\n    void onClick();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/base/OnItemClickListenerTwo.java",
    "content": "package com.kunfei.bookshelf.view.adapter.base;\n\nimport android.view.View;\n\npublic interface OnItemClickListenerTwo {\n    void onClick(View view, int index);\n\n    void onLongClick(View view, int index);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/base/ViewHolderImpl.java",
    "content": "package com.kunfei.bookshelf.view.adapter.base;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\n/**\n * Created by newbiechen on 17-5-17.\n */\n\npublic abstract class ViewHolderImpl<T> implements IViewHolder<T> {\n    private View view;\n    private Context context;\n\n    /****************************************************/\n    protected abstract int getItemLayoutId();\n\n\n    @Override\n    public View createItemView(ViewGroup parent) {\n        view = LayoutInflater.from(parent.getContext())\n                .inflate(getItemLayoutId(), parent, false);\n        context = parent.getContext();\n        return view;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    protected <V extends View> V findById(int id) {\n        return (V) view.findViewById(id);\n    }\n\n    protected Context getContext() {\n        return context;\n    }\n\n    protected View getItemView() {\n        return view;\n    }\n\n    @Override\n    public void onClick() {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/adapter/view/FileHolder.java",
    "content": "package com.kunfei.bookshelf.view.adapter.view;\n\nimport android.view.View;\nimport android.widget.CheckBox;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.constant.AppConstant;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.FileHelp;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.view.adapter.base.ViewHolderImpl;\n\nimport java.io.File;\nimport java.util.HashMap;\n\n/**\n * Created by newbiechen on 17-5-27.\n */\n\npublic class FileHolder extends ViewHolderImpl<File> {\n    private ImageView mIvIcon;\n    private CheckBox mCbSelect;\n    private TextView mTvName;\n    private LinearLayout mLlBrief;\n    private TextView mTvTag;\n    private TextView mTvSize;\n    private TextView mTvDate;\n    private TextView mTvSubCount;\n\n    private HashMap<File, Boolean> mSelectedMap;\n\n    public FileHolder(HashMap<File, Boolean> selectedMap) {\n        mSelectedMap = selectedMap;\n    }\n\n    @Override\n    public void initView() {\n        mIvIcon = findById(R.id.file_iv_icon);\n        mCbSelect = findById(R.id.file_cb_select);\n        mTvName = findById(R.id.file_tv_name);\n        mLlBrief = findById(R.id.file_ll_brief);\n        mTvTag = findById(R.id.file_tv_tag);\n        mTvSize = findById(R.id.file_tv_size);\n        mTvDate = findById(R.id.file_tv_date);\n        mTvSubCount = findById(R.id.file_tv_sub_count);\n    }\n\n    @Override\n    public void onBind(File data, int pos) {\n        //判断是文件还是文件夹\n        if (data.isDirectory()) {\n            setFolder(data);\n        } else {\n            setFile(data);\n        }\n        mCbSelect.setClickable(false);\n    }\n\n    private void setFile(File file) {\n        //选择\n\n        if (BookshelfHelp.getBook(file.getAbsolutePath()) != null) {\n            mIvIcon.setImageResource(R.drawable.ic_book_has);\n            mIvIcon.setVisibility(View.VISIBLE);\n            mCbSelect.setVisibility(View.GONE);\n        } else {\n            boolean isSelected = mSelectedMap.get(file);\n            mCbSelect.setChecked(isSelected);\n            mIvIcon.setVisibility(View.GONE);\n            mCbSelect.setVisibility(View.VISIBLE);\n        }\n\n        mLlBrief.setVisibility(View.VISIBLE);\n        mTvSubCount.setVisibility(View.GONE);\n\n        mTvName.setText(file.getName());\n        mTvTag.setText(file.getName().substring(file.getName().lastIndexOf(\".\") + 1).toUpperCase());\n        mTvSize.setText(FileHelp.getFileSize(file.length()));\n        mTvDate.setText(StringUtils.dateConvert(file.lastModified(), AppConstant.FORMAT_FILE_DATE));\n    }\n\n    public void setFolder(File folder) {\n        //图片\n        mIvIcon.setVisibility(View.VISIBLE);\n        mCbSelect.setVisibility(View.GONE);\n        mIvIcon.setImageResource(R.drawable.ic_folder);\n        //名字\n        mTvName.setText(folder.getName());\n        //介绍\n        mLlBrief.setVisibility(View.GONE);\n        mTvSubCount.setVisibility(View.VISIBLE);\n\n        mTvSubCount.setText(getContext().getString(R.string.nb_file_sub_count, folder.list().length));\n    }\n\n    @Override\n    protected int getItemLayoutId() {\n        return R.layout.item_file;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/dialog/SourceLoginDialog.kt",
    "content": "package com.kunfei.bookshelf.view.dialog\n\nimport android.os.Bundle\nimport android.text.InputType\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.EditText\nimport android.widget.TextView\nimport androidx.core.os.bundleOf\nimport androidx.core.view.setPadding\nimport androidx.fragment.app.DialogFragment\nimport androidx.fragment.app.FragmentManager\nimport com.google.android.material.textfield.TextInputLayout\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.databinding.DialogLoginBinding\nimport com.kunfei.bookshelf.model.BookSourceManager\nimport com.kunfei.bookshelf.utils.*\nimport com.kunfei.bookshelf.utils.theme.ThemeStore\nimport com.kunfei.bookshelf.utils.viewbindingdelegate.viewBinding\nimport io.reactivex.Single\nimport io.reactivex.SingleObserver\nimport io.reactivex.disposables.Disposable\nimport org.jetbrains.anko.sdk27.listeners.onClick\n\nclass SourceLoginDialog : DialogFragment() {\n\n    companion object {\n        fun start(fragmentManager: FragmentManager, sourceUrl: String) {\n            SourceLoginDialog().apply {\n                arguments = bundleOf(\n                    Pair(\"sourceUrl\", sourceUrl)\n                )\n            }.show(fragmentManager, \"sourceLoginDialog\")\n        }\n    }\n\n    val binding by viewBinding(DialogLoginBinding::bind)\n\n    override fun onStart() {\n        super.onStart()\n        dialog?.window?.setLayout(\n            ViewGroup.LayoutParams.MATCH_PARENT,\n            ViewGroup.LayoutParams.WRAP_CONTENT\n        )\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?\n    ): View? {\n        return inflater.inflate(R.layout.dialog_login, container)\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        binding.toolBar.setBackgroundColor(ThemeStore.primaryColor(requireContext()))\n        binding.toolBar.title = getString(R.string.login)\n        val sourceUrl = arguments?.getString(\"sourceUrl\")\n        val source = BookSourceManager.getBookSourceByUrl(sourceUrl)\n        source ?: let {\n            dismiss()\n            return\n        }\n        binding.toolBar.title = getString(R.string.login_source, source.bookSourceName)\n        val loginInfo = source.loginInfoMap\n        val loginUi = GSON.fromJsonArray<RowUi>(source.loginUi)\n        loginUi?.forEachIndexed { index, rowUi ->\n            when (rowUi.type) {\n                \"text\" -> layoutInflater.inflate(R.layout.item_source_edit, binding.root, false)\n                    .let {\n                        binding.listView.addView(it)\n                        it.id = index\n                        (it as TextInputLayout).hint = rowUi.name\n                        it.findViewById<EditText>(R.id.editText).apply {\n                            setText(loginInfo?.get(rowUi.name))\n                        }\n                    }\n                \"password\" -> layoutInflater.inflate(R.layout.item_source_edit, binding.root, false)\n                    .let {\n                        binding.listView.addView(it)\n                        it.id = index\n                        (it as TextInputLayout).hint = rowUi.name\n                        it.findViewById<EditText>(R.id.editText).apply {\n                            inputType =\n                                InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT\n                            setText(loginInfo?.get(rowUi.name))\n                        }\n                    }\n                \"button\" -> layoutInflater.inflate(\n                    R.layout.item_find2_childer_view,\n                    binding.root,\n                    false\n                )\n                    .let {\n                        binding.listView.addView(it)\n                        it.id = index\n                        (it as TextView).let { textView ->\n                            textView.text = rowUi.name\n                            textView.setPadding(DensityUtil.dp2px(requireContext(), 16f))\n                        }\n                        it.onClick {\n                            if (rowUi.action.isAbsUrl()) {\n                                context?.openUrl(rowUi.action!!)\n                            }\n                        }\n                    }\n            }\n        }\n        binding.toolBar.inflateMenu(R.menu.menu_source_login)\n        binding.toolBar.setOnMenuItemClickListener {\n            when (it.itemId) {\n                R.id.action_check -> {\n                    val loginData = hashMapOf<String, String?>()\n                    loginUi?.forEachIndexed { index, rowUi ->\n                        when (rowUi.type) {\n                            \"text\", \"password\" -> {\n                                val value = binding.listView.findViewById<TextInputLayout>(index)\n                                    .findViewById<EditText>(R.id.editText).text?.toString()\n                                loginData[rowUi.name] = value\n                            }\n\n                        }\n                    }\n                    source.putLoginInfo(loginData)\n                    Single.create<String> { emitter ->\n                        source.loginUrl?.let { loginUrl ->\n                            emitter.onSuccess(source.evalJS(loginUrl).toString())\n                        } ?: let {\n                            emitter.onError(Throwable(\"\"))\n                        }\n                    }.compose(RxUtils::toSimpleSingle)\n                        .subscribe(object : SingleObserver<String> {\n\n                            override fun onSubscribe(d: Disposable) {\n\n                            }\n\n                            override fun onSuccess(t: String) {\n                                dismiss()\n                            }\n\n                            override fun onError(e: Throwable) {\n\n                            }\n                        })\n                }\n            }\n            return@setOnMenuItemClickListener true\n        }\n    }\n\n    data class RowUi(\n        var name: String,\n        var type: String,\n        var action: String?\n    )\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/BaseFileFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.base.MBaseFragment;\nimport com.kunfei.bookshelf.view.adapter.FileSystemAdapter;\n\nimport java.io.File;\nimport java.util.List;\n\n/**\n * Created by newbiechen on 17-7-10.\n * FileSystemActivity的基础Fragment类\n */\n\npublic abstract class BaseFileFragment extends MBaseFragment<IPresenter> {\n\n    protected FileSystemAdapter mAdapter;\n    protected OnFileCheckedListener mListener;\n    protected boolean isCheckedAll;\n\n    public void setChecked(boolean checked) {\n        isCheckedAll = checked;\n    }\n\n    //当前fragment是否全选\n    public boolean isCheckedAll() {\n        return isCheckedAll;\n    }\n\n    //设置当前列表为全选\n    public void setCheckedAll(boolean checkedAll) {\n        if (mAdapter == null) return;\n\n        isCheckedAll = checkedAll;\n        mAdapter.setCheckedAll(checkedAll);\n    }\n\n    //获取被选中的数量\n    public int getCheckedCount() {\n        if (mAdapter == null) return 0;\n        return mAdapter.getCheckedCount();\n    }\n\n    //获取被选中的文件列表\n    public List<File> getCheckedFiles() {\n        return mAdapter != null ? mAdapter.getCheckedFiles() : null;\n    }\n\n    //获取文件的总数\n    public int getFileCount() {\n        return mAdapter != null ? mAdapter.getItemCount() : 0;\n    }\n\n    //获取可点击的文件的数量\n    public int getCheckableCount() {\n        if (mAdapter == null) return 0;\n        return mAdapter.getCheckableCount();\n    }\n\n    /**\n     * 删除选中的文件\n     */\n    public void deleteCheckedFiles() {\n        //删除选中的文件\n        List<File> files = getCheckedFiles();\n        //删除显示的文件列表\n        mAdapter.removeItems(files);\n        //删除选中的文件\n        for (File file : files) {\n            if (file.exists()) {\n                //noinspection ResultOfMethodCallIgnored\n                file.delete();\n            }\n        }\n    }\n\n    //设置文件点击监听事件\n    public void setOnFileCheckedListener(OnFileCheckedListener listener) {\n        mListener = listener;\n    }\n\n    //文件点击监听\n    public interface OnFileCheckedListener {\n        void onItemCheckedChange(boolean isChecked);\n\n        void onCategoryChanged();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/BookListFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport android.annotation.SuppressLint;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.viewpager.widget.ViewPager;\n\nimport com.kunfei.basemvplib.BitIntentDataManager;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseFragment;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.databinding.FragmentBookListBinding;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.ItemTouchCallback;\nimport com.kunfei.bookshelf.presenter.BookDetailPresenter;\nimport com.kunfei.bookshelf.presenter.BookListPresenter;\nimport com.kunfei.bookshelf.presenter.ReadBookPresenter;\nimport com.kunfei.bookshelf.presenter.contract.BookListContract;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.activity.BookDetailActivity;\nimport com.kunfei.bookshelf.view.activity.ReadBookActivity;\nimport com.kunfei.bookshelf.view.adapter.BookShelfAdapter;\nimport com.kunfei.bookshelf.view.adapter.BookShelfGridAdapter;\nimport com.kunfei.bookshelf.view.adapter.BookShelfListAdapter;\nimport com.kunfei.bookshelf.view.adapter.base.OnItemClickListenerTwo;\n\nimport java.util.List;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\npublic class BookListFragment extends MBaseFragment<BookListContract.Presenter> implements BookListContract.View {\n\n    private CallbackValue callbackValue;\n    private FragmentBookListBinding binding;\n    private String bookPx;\n    private boolean resumed = false;\n    private boolean isRecreate;\n    private int group;\n\n    private BookShelfAdapter bookShelfAdapter;\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        if (savedInstanceState != null) {\n            resumed = savedInstanceState.getBoolean(\"resumed\");\n        }\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    protected View createView(LayoutInflater inflater, ViewGroup container) {\n        binding = FragmentBookListBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n    @Override\n    protected BookListContract.Presenter initInjector() {\n        return new BookListPresenter();\n    }\n\n    @Override\n    protected void initData() {\n        callbackValue = (CallbackValue) getActivity();\n        bookPx = preferences.getString(getString(R.string.pk_bookshelf_px), \"0\");\n        isRecreate = callbackValue != null && callbackValue.isRecreate();\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        int bookshelfLayout = preferences.getInt(\"bookshelfLayout\", 0);\n        if (bookshelfLayout == 0) {\n            binding.rvBookshelf.setLayoutManager(new LinearLayoutManager(getContext()));\n            bookShelfAdapter = new BookShelfListAdapter(getActivity());\n        } else {\n            binding.rvBookshelf.setLayoutManager(new GridLayoutManager(getContext(), bookshelfLayout + 2));\n            bookShelfAdapter = new BookShelfGridAdapter(getActivity());\n        }\n        binding.rvBookshelf.setAdapter((RecyclerView.Adapter) bookShelfAdapter);\n        binding.refreshLayout.setColorSchemeColors(ThemeStore.accentColor(MApplication.getInstance()));\n    }\n\n    @Override\n    protected void firstRequest() {\n        group = preferences.getInt(\"bookshelfGroup\", 0);\n        boolean needRefresh = preferences.getBoolean(getString(R.string.pk_auto_refresh), false)\n                && !isRecreate && NetworkUtils.isNetWorkAvailable() && group != 2;\n        mPresenter.queryBookShelf(needRefresh, group);\n    }\n\n    @Override\n    protected void bindEvent() {\n        binding.refreshLayout.setOnRefreshListener(() -> {\n            mPresenter.queryBookShelf(NetworkUtils.isNetWorkAvailable(), group);\n            if (!NetworkUtils.isNetWorkAvailable()) {\n                Toast.makeText(getContext(), R.string.network_connection_unavailable, Toast.LENGTH_SHORT).show();\n            }\n            binding.refreshLayout.setRefreshing(false);\n        });\n        ItemTouchCallback itemTouchCallback = new ItemTouchCallback();\n        itemTouchCallback.setSwipeRefreshLayout(binding.refreshLayout);\n        itemTouchCallback.setViewPager(callbackValue.getViewPager());\n        if (bookPx.equals(\"2\")) {\n            itemTouchCallback.setDragEnable(true);\n            ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback);\n            itemTouchHelper.attachToRecyclerView(binding.rvBookshelf);\n        } else {\n            itemTouchCallback.setDragEnable(false);\n            ItemTouchHelper itemTouchHelper = new ItemTouchHelper(itemTouchCallback);\n            itemTouchHelper.attachToRecyclerView(binding.rvBookshelf);\n        }\n        bookShelfAdapter.setItemClickListener(getAdapterListener());\n        itemTouchCallback.setOnItemTouchCallbackListener(bookShelfAdapter.getItemTouchCallbackListener());\n        binding.ivBack.setOnClickListener(v -> setArrange(false));\n        binding.ivDel.setOnClickListener(v -> {\n            if (bookShelfAdapter.getSelected().size() == bookShelfAdapter.getBooks().size()) {\n                AlertDialog alertDialog = new AlertDialog.Builder(requireContext())\n                        .setTitle(R.string.delete)\n                        .setMessage(getString(R.string.sure_del_all_book))\n                        .setPositiveButton(R.string.yes, (dialog, which) -> delSelect())\n                        .setNegativeButton(R.string.no, null)\n                        .show();\n                ATH.setAlertDialogTint(alertDialog);\n            } else {\n                delSelect();\n            }\n        });\n        binding.ivSelectAll.setOnClickListener(v -> bookShelfAdapter.selectAll());\n    }\n\n    private OnItemClickListenerTwo getAdapterListener() {\n        return new OnItemClickListenerTwo() {\n            @Override\n            public void onClick(View view, int index) {\n                if (binding.actionBar.getVisibility() == View.VISIBLE) {\n                    upSelectCount();\n                    return;\n                }\n                BookShelfBean bookShelfBean = bookShelfAdapter.getBooks().get(index);\n                Intent intent = new Intent(getContext(), ReadBookActivity.class);\n                intent.putExtra(\"openFrom\", ReadBookPresenter.OPEN_FROM_APP);\n                String key = String.valueOf(System.currentTimeMillis());\n                String bookKey = \"book\" + key;\n                intent.putExtra(\"bookKey\", bookKey);\n                BitIntentDataManager.getInstance().putData(bookKey, bookShelfBean.clone());\n                startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out);\n            }\n\n            @Override\n            public void onLongClick(View view, int index) {\n                BookShelfBean bookShelfBean = bookShelfAdapter.getBooks().get(index);\n                String key = String.valueOf(System.currentTimeMillis());\n                BitIntentDataManager.getInstance().putData(key, bookShelfBean.clone());\n                Intent intent = new Intent(getActivity(), BookDetailActivity.class);\n                intent.putExtra(\"openFrom\", BookDetailPresenter.FROM_BOOKSHELF);\n                intent.putExtra(\"data_key\", key);\n                intent.putExtra(\"noteUrl\", bookShelfBean.getNoteUrl());\n                startActivityByAnim(intent, android.R.anim.fade_in, android.R.anim.fade_out);\n            }\n        };\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        if (resumed) {\n            resumed = false;\n            stopBookShelfRefreshAnim();\n        }\n    }\n\n    @Override\n    public void onPause() {\n        resumed = true;\n        super.onPause();\n    }\n\n    private void stopBookShelfRefreshAnim() {\n        if (bookShelfAdapter.getBooks() != null && bookShelfAdapter.getBooks().size() > 0) {\n            for (BookShelfBean bookShelfBean : bookShelfAdapter.getBooks()) {\n                if (bookShelfBean.isLoading()) {\n                    bookShelfBean.setLoading(false);\n                    refreshBook(bookShelfBean.getNoteUrl());\n                }\n            }\n        }\n    }\n\n    @Override\n    public void refreshBookShelf(List<BookShelfBean> bookShelfBeanList) {\n        bookShelfAdapter.replaceAll(bookShelfBeanList, bookPx);\n        if (bookShelfBeanList.size() > 0) {\n            binding.viewEmpty.rlEmptyView.setVisibility(View.GONE);\n        } else {\n            binding.viewEmpty.tvEmpty.setText(R.string.bookshelf_empty);\n            binding.viewEmpty.rlEmptyView.setVisibility(View.VISIBLE);\n        }\n    }\n\n    @Override\n    public void refreshBook(String noteUrl) {\n        bookShelfAdapter.refreshBook(noteUrl);\n    }\n\n    @Override\n    public void updateGroup(Integer group) {\n        this.group = group;\n    }\n\n    @Override\n    public SharedPreferences getPreferences() {\n        return preferences;\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        binding = null;\n    }\n\n    public void setArrange(boolean isArrange) {\n        if (bookShelfAdapter != null) {\n            bookShelfAdapter.setArrange(isArrange);\n            if (isArrange) {\n                binding.actionBar.setVisibility(View.VISIBLE);\n                upSelectCount();\n            } else {\n                binding.actionBar.setVisibility(View.GONE);\n            }\n        }\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    private void upSelectCount() {\n        binding.tvSelectCount.setText(String.format(\"%d/%d\", bookShelfAdapter.getSelected().size(), bookShelfAdapter.getBooks().size()));\n    }\n\n    private void delSelect() {\n        Single.create((SingleOnSubscribe<Boolean>) emitter -> {\n            for (String noteUrl : bookShelfAdapter.getSelected()) {\n                BookshelfHelp.removeFromBookShelf(BookshelfHelp.getBook(noteUrl));\n            }\n            bookShelfAdapter.getSelected().clear();\n            emitter.onSuccess(true);\n        }).compose(RxUtils::toSimpleSingle)\n                .subscribe(new MySingleObserver<Boolean>() {\n                    @Override\n                    public void onSuccess(Boolean aBoolean) {\n                        mPresenter.queryBookShelf(false, group);\n                    }\n                });\n    }\n\n    public interface CallbackValue {\n        boolean isRecreate();\n\n        int getGroup();\n\n        ViewPager getViewPager();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/BookmarkFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.base.MBaseFragment;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookmarkBean;\nimport com.kunfei.bookshelf.bean.OpenChapterBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.FragmentBookmarkListBinding;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.view.activity.ChapterListActivity;\nimport com.kunfei.bookshelf.view.adapter.BookmarkAdapter;\nimport com.kunfei.bookshelf.widget.modialog.BookmarkDialog;\n\nimport java.util.List;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\n\npublic class BookmarkFragment extends MBaseFragment<IPresenter> {\n\n    private FragmentBookmarkListBinding binding;\n    private BookShelfBean bookShelf;\n    private List<BookmarkBean> bookmarkBeanList;\n    private BookmarkAdapter adapter;\n\n    @Override\n    protected View createView(LayoutInflater inflater, ViewGroup container) {\n        binding = FragmentBookmarkListBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        RxBus.get().register(this);\n    }\n\n    /**\n     * 数据初始化\n     */\n    @Override\n    protected void initData() {\n        super.initData();\n        if (getFatherActivity() != null) {\n            bookShelf = getFatherActivity().getBookShelf();\n        }\n    }\n\n    /**\n     * 控件绑定\n     */\n    @Override\n    protected void bindView() {\n        super.bindView();\n        adapter = new BookmarkAdapter(bookShelf, new BookmarkAdapter.OnItemClickListener() {\n            @Override\n            public void itemClick(int index, int page) {\n                if (index != bookShelf.getDurChapter()) {\n                    RxBus.get().post(RxBusTag.SKIP_TO_CHAPTER, new OpenChapterBean(index, page));\n                }\n                if (getFatherActivity() != null) {\n                    getFatherActivity().searchViewCollapsed();\n                    getFatherActivity().finish();\n                }\n            }\n\n            @Override\n            public void itemLongClick(BookmarkBean bookmarkBean) {\n                if (getFatherActivity() != null) {\n                    getFatherActivity().searchViewCollapsed();\n                }\n                showBookmark(bookmarkBean);\n            }\n        });\n        binding.rvList.setLayoutManager(new LinearLayoutManager(getActivity()));\n        binding.rvList.setAdapter(adapter);\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        Single.create((SingleOnSubscribe<Boolean>) emitter -> {\n            if (bookShelf != null) {\n                bookmarkBeanList = BookshelfHelp.getBookmarkList(bookShelf.getBookInfoBean().getName());\n                emitter.onSuccess(true);\n            } else {\n                emitter.onSuccess(false);\n            }\n        }).compose(RxUtils::toSimpleSingle)\n                .subscribe(new MySingleObserver<Boolean>() {\n                    @Override\n                    public void onSuccess(Boolean aBoolean) {\n                        if (aBoolean) {\n                            adapter.setAllBookmark(bookmarkBeanList);\n                        }\n                    }\n                });\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        binding = null;\n        RxBus.get().unregister(this);\n    }\n\n    public void startSearch(String key) {\n        adapter.search(key);\n    }\n\n    private void showBookmark(BookmarkBean bookmarkBean) {\n        BookmarkDialog.builder(getContext(), bookmarkBean, false)\n                .setPositiveButton(new BookmarkDialog.Callback() {\n                    @Override\n                    public void saveBookmark(BookmarkBean bookmarkBean) {\n                        DbHelper.getDaoSession().getBookmarkBeanDao().insertOrReplace(bookmarkBean);\n                        adapter.notifyDataSetChanged();\n                    }\n\n                    @Override\n                    public void delBookmark(BookmarkBean bookmarkBean) {\n//                        Log.d(\"delBookmark\",\"before=\"+bookmarkBeanList.size());\n                        DbHelper.getDaoSession().getBookmarkBeanDao().delete(bookmarkBean);\n//                        Log.d(\"delBookmark\",\"after=\"+bookmarkBeanList.size());\n                        bookmarkBeanList = BookshelfHelp.getBookmarkList(bookShelf.getBookInfoBean().getName());\n//                        Log.d(\"delBookmark\",\"fine=\"+bookmarkBeanList.size());\n                        adapter.setAllBookmark(bookmarkBeanList);\n//                        adapter.notifyDataSetChanged();\n                    }\n\n                    @Override\n                    public void openChapter(int chapterIndex, int pageIndex) {\n                        RxBus.get().post(RxBusTag.OPEN_BOOK_MARK, bookmarkBean);\n                        if (getFatherActivity() != null) {\n                            getFatherActivity().finish();\n                        }\n                    }\n                }).show();\n    }\n\n    private ChapterListActivity getFatherActivity() {\n        return (ChapterListActivity) getActivity();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/ChapterListFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.base.MBaseFragment;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.OpenChapterBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.databinding.FragmentChapterListBinding;\nimport com.kunfei.bookshelf.view.activity.ChapterListActivity;\nimport com.kunfei.bookshelf.view.adapter.ChapterListAdapter;\n\nimport java.util.List;\nimport java.util.Locale;\n\npublic class ChapterListFragment extends MBaseFragment<IPresenter> {\n\n    private FragmentChapterListBinding binding;\n\n    private ChapterListAdapter chapterListAdapter;\n\n    private LinearLayoutManager layoutManager;\n\n    private BookShelfBean bookShelf;\n    private List<BookChapterBean> chapterBeanList;\n    private boolean isChapterReverse;\n\n    @Override\n    protected View createView(LayoutInflater inflater, ViewGroup container) {\n        binding = FragmentChapterListBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    public void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        RxBus.get().register(this);\n    }\n\n    /**\n     * 数据初始化\n     */\n    @Override\n    protected void initData() {\n        super.initData();\n        if (getFatherActivity() != null) {\n            bookShelf = getFatherActivity().getBookShelf();\n            chapterBeanList = getFatherActivity().getChapterBeanList();\n        }\n        isChapterReverse = preferences.getBoolean(\"isChapterReverse\", false);\n    }\n\n    /**\n     * 控件绑定\n     */\n    @Override\n    protected void bindView() {\n        super.bindView();\n        binding.rvList.setLayoutManager(layoutManager = new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, isChapterReverse));\n        binding.rvList.setItemAnimator(null);\n        chapterListAdapter = new ChapterListAdapter(bookShelf, chapterBeanList, (index, page) -> {\n            if (index != bookShelf.getDurChapter()) {\n                RxBus.get().post(RxBusTag.SKIP_TO_CHAPTER, new OpenChapterBean(index, page));\n            }\n            if (getFatherActivity() != null) {\n                getFatherActivity().searchViewCollapsed();\n                getFatherActivity().finish();\n            }\n        });\n        if (bookShelf != null) {\n            binding.rvList.setAdapter(chapterListAdapter);\n            updateIndex(bookShelf.getDurChapter());\n            updateChapterInfo();\n        }\n    }\n\n    /**\n     * 事件触发绑定\n     */\n    @Override\n    protected void bindEvent() {\n        super.bindEvent();\n        binding.tvCurrentChapterInfo.setOnClickListener(view -> layoutManager.scrollToPositionWithOffset(bookShelf.getDurChapter(), 0));\n        binding.ivChapterTop.setOnClickListener(v -> binding.rvList.scrollToPosition(0));\n        binding.ivChapterBottom.setOnClickListener(v -> {\n            if (chapterListAdapter.getItemCount() > 0) {\n                binding.rvList.scrollToPosition(chapterListAdapter.getItemCount() - 1);\n            }\n        });\n    }\n\n    public void startSearch(String key) {\n        chapterListAdapter.search(key);\n    }\n\n    private void updateIndex(int durChapter) {\n        chapterListAdapter.setIndex(durChapter);\n        layoutManager.scrollToPositionWithOffset(durChapter, 0);\n    }\n\n    private void updateChapterInfo() {\n        if (bookShelf != null) {\n            if (chapterListAdapter.getItemCount() == 0) {\n                binding.tvCurrentChapterInfo.setText(bookShelf.getDurChapterName());\n            } else {\n                binding.tvCurrentChapterInfo.setText(String.format(Locale.getDefault(), \"%s (%d/%d章)\", bookShelf.getDurChapterName(), bookShelf.getDurChapter() + 1, bookShelf.getChapterListSize()));\n            }\n        }\n    }\n\n    private ChapterListActivity getFatherActivity() {\n        return (ChapterListActivity) getActivity();\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        binding = null;\n        RxBus.get().unregister(this);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.CHAPTER_CHANGE)})\n    public void chapterChange(BookContentBean bookContentBean) {\n        if (bookShelf != null && bookShelf.getNoteUrl().equals(bookContentBean.getNoteUrl())) {\n            chapterListAdapter.upChapter(bookContentBean.getDurChapterIndex());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/FileCategoryFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport android.graphics.PorterDuff;\nimport android.os.Environment;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.databinding.FragmentFileCategoryBinding;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.FileHelp;\nimport com.kunfei.bookshelf.utils.FileStack;\nimport com.kunfei.bookshelf.utils.FileUtils;\nimport com.kunfei.bookshelf.view.adapter.FileSystemAdapter;\nimport com.kunfei.bookshelf.widget.itemdecoration.DividerItemDecoration;\n\nimport java.io.File;\nimport java.io.FileFilter;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\npublic class FileCategoryFragment extends BaseFileFragment {\n    private static final String TAG = \"FileCategoryFragment\";\n\n    private FragmentFileCategoryBinding binding;\n    private FileStack mFileStack;\n    private String rootFilePath;\n\n    @Override\n    protected View createView(LayoutInflater inflater, ViewGroup container) {\n        binding = FragmentFileCategoryBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        mFileStack = new FileStack();\n        setUpAdapter();\n    }\n\n    private void setUpAdapter() {\n        mAdapter = new FileSystemAdapter();\n        binding.fileCategoryRvContent.setLayoutManager(new LinearLayoutManager(getContext()));\n        binding.fileCategoryRvContent.addItemDecoration(new DividerItemDecoration(getContext()));\n        binding.fileCategoryRvContent.setAdapter(mAdapter);\n        setTextViewIconColor(binding.fileCategoryTvBackLast);\n    }\n\n    @Override\n    protected void bindEvent() {\n        super.bindEvent();\n        mAdapter.setOnItemClickListener(\n                (view, pos) -> {\n                    File file = mAdapter.getItem(pos);\n                    if (file.isDirectory()) {\n                        //保存当前信息。\n                        FileStack.FileSnapshot snapshot = new FileStack.FileSnapshot();\n                        snapshot.filePath = binding.fileCategoryTvPath.getText().toString();\n                        snapshot.files = new ArrayList<>(mAdapter.getItems());\n                        snapshot.scrollOffset = binding.fileCategoryRvContent.computeVerticalScrollOffset();\n                        mFileStack.push(snapshot);\n                        //切换下一个文件\n                        toggleFileTree(file);\n                    } else {\n\n                        //如果是已加载的文件，则点击事件无效。\n                        String id = mAdapter.getItem(pos).getAbsolutePath();\n                        if (BookshelfHelp.getBook(id) != null) {\n                            return;\n                        }\n                        //点击选中\n                        mAdapter.setCheckedItem(pos);\n                        //反馈\n                        if (mListener != null) {\n                            mListener.onItemCheckedChange(mAdapter.getItemIsChecked(pos));\n                        }\n                    }\n                }\n        );\n\n        binding.fileCategoryTvBackLast.setOnClickListener(v -> {\n            FileStack.FileSnapshot snapshot = mFileStack.pop();\n            int oldScrollOffset = binding.fileCategoryRvContent.computeHorizontalScrollOffset();\n            if (snapshot == null) return;\n            binding.fileCategoryTvPath.setText(snapshot.filePath);\n            mAdapter.refreshItems(snapshot.files);\n            binding.fileCategoryRvContent.scrollBy(0, snapshot.scrollOffset - oldScrollOffset);\n            //反馈\n            if (mListener != null) {\n                mListener.onCategoryChanged();\n            }\n                }\n        );\n\n        binding.tvSd.setOnClickListener(v -> {\n            if (getContext() != null) {\n                List<String> list = FileUtils.getStorageData(getContext());\n                if (list != null) {\n                    String[] filePathS = list.toArray(new String[0]);\n                    AlertDialog dialog = new AlertDialog.Builder(getContext())\n                            .setTitle(R.string.select_sd_file)\n                            .setSingleChoiceItems(filePathS, 0, (dialogInterface, i) -> {\n                                upRootFile(filePathS[i]);\n                                dialogInterface.dismiss();\n                            })\n                            .create();\n                    dialog.show();\n                }\n            }\n        });\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        upRootFile(Environment.getExternalStorageDirectory().getPath());\n    }\n\n    private void upRootFile(String rootFilePath) {\n        this.rootFilePath = rootFilePath;\n        toggleFileTree(new File(rootFilePath));\n    }\n\n    private void setTextViewIconColor(TextView textView) {\n        // textView.getCompoundDrawables()[0].mutate();\n        try {\n            textView.getCompoundDrawables()[0].setColorFilter(getResources().getColor(R.color.tv_text_default), PorterDuff.Mode.SRC_ATOP);\n        } catch (Exception ignored) {\n        }\n    }\n\n    private void toggleFileTree(File file) {\n        //路径名\n        binding.fileCategoryTvPath.setText(file.getPath().replace(rootFilePath, \"\"));\n        //获取数据\n        File[] files = file.listFiles(new SimpleFileFilter());\n        //转换成List\n        List<File> rootFiles = Arrays.asList(files);\n        //排序\n        Collections.sort(rootFiles, new FileComparator());\n        //加入\n        mAdapter.refreshItems(rootFiles);\n        //反馈\n        if (mListener != null) {\n            mListener.onCategoryChanged();\n        }\n    }\n\n    @Override\n    public int getFileCount() {\n        int count = 0;\n        Set<Map.Entry<File, Boolean>> entrys = mAdapter.getCheckMap().entrySet();\n        for (Map.Entry<File, Boolean> entry : entrys) {\n            if (!entry.getKey().isDirectory()) {\n                ++count;\n            }\n        }\n        return count;\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        binding = null;\n    }\n\n\n    public static class FileComparator implements Comparator<File> {\n        @Override\n        public int compare(File o1, File o2) {\n            if (o1.isDirectory() && o2.isFile()) {\n                return -1;\n            }\n            if (o2.isDirectory() && o1.isFile()) {\n                return 1;\n            }\n            return o1.getName().compareToIgnoreCase(o2.getName());\n        }\n    }\n\n    public static class SimpleFileFilter implements FileFilter {\n        @Override\n        public boolean accept(File pathname) {\n            if (pathname.getName().startsWith(\".\")) {\n                return false;\n            }\n            //文件夹内部数量为0\n            if (pathname.isDirectory() && (pathname.list() == null || pathname.list().length == 0)) {\n                return false;\n            }\n\n            //文件内容为空,或者不以txt为开头\n            return pathname.isDirectory() ||\n                    (pathname.length() != 0\n                            && (pathname.getName().toLowerCase().endsWith(FileHelp.SUFFIX_TXT)\n                            || pathname.getName().toLowerCase().endsWith(FileHelp.SUFFIX_EPUB)));\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/FindBookFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport static android.app.Activity.RESULT_OK;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.PointF;\nimport android.os.Bundle;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.PopupMenu;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.LinearSmoothScroller;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.MBaseFragment;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.FindKindBean;\nimport com.kunfei.bookshelf.bean.FindKindGroupBean;\nimport com.kunfei.bookshelf.databinding.FragmentBookFindBinding;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.presenter.FindBookPresenter;\nimport com.kunfei.bookshelf.presenter.contract.FindBookContract;\nimport com.kunfei.bookshelf.utils.ACache;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.view.activity.ChoiceBookActivity;\nimport com.kunfei.bookshelf.view.activity.SourceEditActivity;\nimport com.kunfei.bookshelf.view.adapter.FindKindAdapter;\nimport com.kunfei.bookshelf.view.adapter.FindLeftAdapter;\nimport com.kunfei.bookshelf.view.adapter.FindRightAdapter;\nimport com.kunfei.bookshelf.widget.recycler.expandable.OnRecyclerViewListener;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData;\nimport com.kunfei.bookshelf.widget.recycler.sectioned.GridSpacingItemDecoration;\nimport com.kunfei.bookshelf.widget.recycler.sectioned.SectionedSpanSizeLookup;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class FindBookFragment extends MBaseFragment<FindBookContract.Presenter> implements FindBookContract.View,\n        OnRecyclerViewListener.OnItemClickListener,\n        OnRecyclerViewListener.OnItemLongClickListener {\n\n\n    private FragmentBookFindBinding binding;\n    private FindLeftAdapter findLeftAdapter;\n    private FindRightAdapter findRightAdapter;\n    private FindKindAdapter findKindAdapter;\n    private LinearLayoutManager leftLayoutManager;\n    private RecyclerView.LayoutManager rightLayoutManager;\n    private List<RecyclerViewData> data = new ArrayList<>();\n\n    @Override\n    protected View createView(LayoutInflater inflater, ViewGroup container) {\n        binding = FragmentBookFindBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n    @Override\n    protected FindBookContract.Presenter initInjector() {\n        return new FindBookPresenter();\n    }\n\n    @Override\n    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        return super.onCreateView(inflater, container, savedInstanceState);\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        binding.rvFindRight.addItemDecoration(new GridSpacingItemDecoration(10));\n        binding.refreshLayout.setColorSchemeColors(ThemeStore.accentColor(MApplication.getInstance()));\n        binding.refreshLayout.setOnRefreshListener(() -> {\n            refreshData();\n            binding.refreshLayout.setRefreshing(false);\n        });\n        leftLayoutManager = new LinearLayoutManager(getContext());\n        initRecyclerView();\n    }\n\n    /**\n     * 首次逻辑操作\n     */\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        refreshData();\n    }\n\n    public void refreshData() {\n        if (mPresenter != null) {\n            mPresenter.initData();\n        }\n    }\n\n    @Override\n    public void upData(List<RecyclerViewData> group) {\n        this.data = group;\n        upStyle();\n        upUI();\n    }\n\n    public void upStyle() {\n        if (binding.emptyView.rlEmptyView == null) return;\n        initRecyclerView();\n        if (isFlexBox()) {\n            findRightAdapter.setData(data);\n            findLeftAdapter.setData(data);\n        } else {\n            findKindAdapter.setAllDatas(data);\n        }\n        upUI();\n    }\n\n    public void upUI() {\n        if (binding.emptyView.rlEmptyView == null) return;\n        if (data.size() == 0) {\n            binding.emptyView.tvEmpty.setText(R.string.no_find);\n            binding.emptyView.rlEmptyView.setVisibility(View.VISIBLE);\n        } else {\n            binding.emptyView.rlEmptyView.setVisibility(View.GONE);\n        }\n        if (isFlexBox()) {\n            binding.emptyView.rlEmptyView.setVisibility(View.GONE);\n            if (data.size() <= 1 | !showLeftView()) {\n                binding.rvFindLeft.setVisibility(View.GONE);\n                binding.vwDivider.setVisibility(View.GONE);\n            } else {\n                binding.rvFindLeft.setVisibility(View.VISIBLE);\n                binding.vwDivider.setVisibility(View.VISIBLE);\n            }\n        }\n    }\n\n    private boolean isFlexBox() {\n        return preferences.getBoolean(\"findTypeIsFlexBox\", true);\n    }\n\n    private boolean showLeftView() {\n        return preferences.getBoolean(\"showFindLeftView\", true);\n    }\n\n    private void initRecyclerView() {\n        if (binding.rvFindRight == null) return;\n        if (isFlexBox()) {\n            findKindAdapter = null;\n            findLeftAdapter = new FindLeftAdapter(getActivity(), pos -> {\n                int counts = 0;\n                for (int i = 0; i < pos; i++) {\n                    //position 为点击的position\n                    counts += findRightAdapter.getData().get(i).getChildList().size();\n                }\n                ((ScrollLinearLayoutManger) rightLayoutManager).scrollToPositionWithOffset(counts + pos, 0);\n            });\n            binding.rvFindLeft.setLayoutManager(leftLayoutManager);\n            binding.rvFindLeft.setAdapter(findLeftAdapter);\n\n            findRightAdapter = new FindRightAdapter(requireActivity(), this);\n            //设置header\n            rightLayoutManager = new ScrollLinearLayoutManger(getActivity(), 3);\n            ((ScrollLinearLayoutManger) rightLayoutManager).setSpanSizeLookup(new SectionedSpanSizeLookup(findRightAdapter, (ScrollLinearLayoutManger) rightLayoutManager));\n            binding.rvFindRight.setLayoutManager(rightLayoutManager);\n            binding.rvFindRight.setLayoutManager(rightLayoutManager);\n            binding.rvFindRight.setItemViewCacheSize(10);\n            binding.rvFindRight.setItemAnimator(null);\n            binding.rvFindRight.setHasFixedSize(true);\n            binding.rvFindRight.setAdapter(findRightAdapter);\n        } else {\n            rightLayoutManager = new LinearLayoutManager(getContext());\n            binding.rvFindLeft.setVisibility(View.GONE);\n            binding.vwDivider.setVisibility(View.GONE);\n            findLeftAdapter = null;\n            findRightAdapter = null;\n            findKindAdapter = new FindKindAdapter(getContext(), new ArrayList<>());\n            findKindAdapter.setOnItemClickListener(this);\n            findKindAdapter.setOnItemLongClickListener(this);\n            findKindAdapter.setCanExpandAll(false);\n            binding.rvFindRight.setLayoutManager(rightLayoutManager);\n            binding.rvFindRight.setAdapter(findKindAdapter);\n        }\n    }\n\n    @Override\n    public void onGroupItemClick(int position, int groupPosition, View view) {\n\n    }\n\n    @Override\n    public void onChildItemClick(int position, int groupPosition, int childPosition, View view) {\n        FindKindBean kindBean = (FindKindBean) findKindAdapter.getAllDatas().get(groupPosition).getChild(childPosition);\n\n        Intent intent = new Intent(getContext(), ChoiceBookActivity.class);\n        intent.putExtra(\"url\", kindBean.getKindUrl());\n        intent.putExtra(\"title\", kindBean.getKindName());\n        intent.putExtra(\"tag\", kindBean.getTag());\n        startActivityByAnim(intent, view, \"sharedView\", android.R.anim.fade_in, android.R.anim.fade_out);\n    }\n\n    @Override\n    public void onGroupItemLongClick(int position, int groupPosition, View view) {\n        if (getActivity() == null) return;\n        FindKindGroupBean groupBean;\n        if (isFlexBox()) {\n            groupBean = (FindKindGroupBean) findRightAdapter.getData().get(groupPosition).getGroupData();\n        } else {\n            groupBean = (FindKindGroupBean) findKindAdapter.getAllDatas().get(groupPosition).getGroupData();\n        }\n        BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(groupBean.getGroupTag());\n        if (sourceBean == null) {\n            return;\n        }\n        PopupMenu popupMenu = new PopupMenu(getContext(), view);\n        popupMenu.getMenu().add(Menu.NONE, R.id.menu_edit, Menu.NONE, R.string.edit);\n        popupMenu.getMenu().add(Menu.NONE, R.id.menu_top, Menu.NONE, R.string.to_top);\n        popupMenu.getMenu().add(Menu.NONE, R.id.menu_del, Menu.NONE, R.string.delete);\n        popupMenu.getMenu().add(Menu.NONE, R.id.menu_clear, Menu.NONE, R.string.clear_cache);\n        popupMenu.setOnMenuItemClickListener(item -> {\n            int itemId = item.getItemId();\n            if (itemId == R.id.menu_edit) {\n                SourceEditActivity.startThis(this, sourceBean);\n            } else if (itemId == R.id.menu_top) {\n                BookSourceManager.toTop(sourceBean)\n                        .subscribe(new MySingleObserver<Boolean>() {\n                            @Override\n                            public void onSuccess(Boolean aBoolean) {\n                                refreshData();\n                            }\n                        });\n            } else if (itemId == R.id.menu_del) {\n                BookSourceManager.removeBookSource(sourceBean);\n                refreshData();\n            } else if (itemId == R.id.menu_clear) {\n                ACache.get(getActivity(), \"findCache\").remove(sourceBean.getBookSourceUrl());\n            }\n            return true;\n        });\n        popupMenu.show();\n\n    }\n\n    @Override\n    public void onChildItemLongClick(int position, int groupPosition, int childPosition, View view) {\n\n    }\n\n    @Override\n    public void onDestroyView() {\n        super.onDestroyView();\n        binding = null;\n    }\n\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        if (resultCode == RESULT_OK) {\n            if (requestCode == SourceEditActivity.EDIT_SOURCE) {\n                refreshData();\n            }\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static class ScrollLinearLayoutManger extends GridLayoutManager {\n\n        public ScrollLinearLayoutManger(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n            super(context, attrs, defStyleAttr, defStyleRes);\n        }\n\n        public ScrollLinearLayoutManger(Context context, int spanCount) {\n            super(context, spanCount);\n        }\n\n        public ScrollLinearLayoutManger(Context context, int spanCount, int orientation, boolean reverseLayout) {\n            super(context, spanCount, orientation, reverseLayout);\n        }\n\n        @Override\n        public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {\n            RecyclerView.SmoothScroller smoothScroller = new CenterSmoothScroller(recyclerView.getContext());\n            smoothScroller.setTargetPosition(position);\n            startSmoothScroll(smoothScroller);\n        }\n\n        private class CenterSmoothScroller extends LinearSmoothScroller {\n\n            CenterSmoothScroller(Context context) {\n                super(context);\n            }\n\n            @Nullable\n            @Override\n            public PointF computeScrollVectorForPosition(int targetPosition) {\n                return ScrollLinearLayoutManger.this.computeScrollVectorForPosition(targetPosition);\n            }\n\n            @Override\n            public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {\n                return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);\n            }\n\n            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {\n                return 0.2f;\n            }\n\n            @Override\n            protected int getVerticalSnapPreference() {\n                return SNAP_TO_START;\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/LocalBookFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.bookshelf.databinding.FragmentLocalBookBinding;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.media.MediaStoreHelper;\nimport com.kunfei.bookshelf.view.adapter.FileSystemAdapter;\nimport com.kunfei.bookshelf.widget.itemdecoration.DividerItemDecoration;\n\n/**\n * Created by newbiechen on 17-5-27.\n * 本地书籍\n */\n\npublic class LocalBookFragment extends BaseFileFragment {\n\n    private FragmentLocalBookBinding binding;\n\n    @Override\n    protected View createView(LayoutInflater inflater, ViewGroup container) {\n        binding = FragmentLocalBookBinding.inflate(inflater, container, false);\n        return binding.getRoot();\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    @Override\n    protected IPresenter initInjector() {\n        return null;\n    }\n\n    @Override\n    protected void bindView() {\n        super.bindView();\n        setUpAdapter();\n    }\n\n    private void setUpAdapter() {\n        mAdapter = new FileSystemAdapter();\n        if (getContext() != null) {\n            binding.localBookRvContent.setLayoutManager(new LinearLayoutManager(getContext()));\n            binding.localBookRvContent.addItemDecoration(new DividerItemDecoration(getContext()));\n            binding.localBookRvContent.setAdapter(mAdapter);\n        }\n    }\n\n    @Override\n    protected void bindEvent() {\n        super.bindEvent();\n        mAdapter.setOnItemClickListener(\n                (view, pos) -> {\n                    //如果是已加载的文件，则点击事件无效。\n                    String id = mAdapter.getItem(pos).getAbsolutePath();\n                    if (BookshelfHelp.getBook(id) != null) {\n                        return;\n                    }\n\n                    //点击选中\n                    mAdapter.setCheckedItem(pos);\n\n                    //反馈\n                    if (mListener != null) {\n                        mListener.onItemCheckedChange(mAdapter.getItemIsChecked(pos));\n                    }\n                }\n        );\n    }\n\n    @Override\n    protected void firstRequest() {\n        super.firstRequest();\n        if (getActivity() != null) {\n            MediaStoreHelper.getAllBookFile(getActivity(),\n                    (files) -> {\n                        if (files.isEmpty()) {\n                            binding.refreshLayout.showEmpty();\n                        } else {\n                            mAdapter.refreshItems(files);\n                            binding.refreshLayout.showFinish();\n                            //反馈\n                            if (mListener != null) {\n                                mListener.onCategoryChanged();\n                            }\n                        }\n                    });\n        }\n    }\n\n    @Override\n    public void onDestroy() {\n        super.onDestroy();\n        binding = null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/SettingsFragment.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage com.kunfei.bookshelf.view.fragment\n\nimport android.content.Intent\nimport android.content.SharedPreferences\nimport android.content.SharedPreferences.OnSharedPreferenceChangeListener\nimport android.os.Bundle\nimport android.preference.ListPreference\nimport android.preference.Preference\nimport android.preference.PreferenceFragment\nimport android.preference.PreferenceScreen\nimport com.hwangjr.rxbus.RxBus\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.constant.RxBusTag\nimport com.kunfei.bookshelf.help.BookshelfHelp\nimport com.kunfei.bookshelf.help.FileHelp\nimport com.kunfei.bookshelf.help.ProcessTextHelp\nimport com.kunfei.bookshelf.help.permission.Permissions\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat\nimport com.kunfei.bookshelf.help.storage.BackupRestoreUi.selectBackupFolder\nimport com.kunfei.bookshelf.service.WebService\nimport com.kunfei.bookshelf.utils.FileUtils\nimport com.kunfei.bookshelf.view.activity.SettingActivity\nimport com.kunfei.bookshelf.widget.filepicker.picker.FilePicker\nimport org.jetbrains.anko.alert\nimport org.jetbrains.anko.noButton\nimport org.jetbrains.anko.okButton\n\n/**\n * Created by GKF on 2017/12/16.\n * 设置\n */\n@Suppress(\"DEPRECATION\")\nclass SettingsFragment : PreferenceFragment(), OnSharedPreferenceChangeListener {\n    private var settingActivity: SettingActivity? = null\n    private lateinit var bookshelfPxKey: String\n    private lateinit var downloadPathKey: String\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        preferenceManager.sharedPreferencesName = \"CONFIG\"\n        settingActivity = activity as? SettingActivity\n        settingActivity?.setupActionBar(getString(R.string.setting))\n        addPreferencesFromResource(R.xml.pref_settings)\n        val sharedPreferences = preferenceManager.sharedPreferences\n        val editor = sharedPreferences.edit()\n        val processTextEnabled = ProcessTextHelp.isProcessTextEnabled()\n        editor.putBoolean(\"process_text\", processTextEnabled)\n        if (sharedPreferences.getString(getString(R.string.pk_download_path), \"\") == \"\") {\n            editor.putString(getString(R.string.pk_download_path), FileHelp.getCachePath())\n        }\n        editor.apply()\n        bookshelfPxKey = getString(R.string.pk_bookshelf_px)\n        downloadPathKey = getString(R.string.pk_download_path)\n        upPreferenceSummary(bookshelfPxKey, sharedPreferences.getString(bookshelfPxKey, \"0\"))\n        upPreferenceSummary(downloadPathKey, MApplication.downloadPath)\n        upPreferenceSummary(\"backupPath\", sharedPreferences.getString(\"backupPath\", null))\n        preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)\n    }\n\n    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {\n        when (key) {\n            bookshelfPxKey -> {\n                upPreferenceSummary(key, sharedPreferences.getString(key, \"0\"))\n                RxBus.get().post(RxBusTag.RECREATE, true)\n            }\n            \"behaviorMain\" -> RxBus.get().post(RxBusTag.RECREATE, true)\n            \"process_text\" -> ProcessTextHelp.setProcessTextEnable(sharedPreferences.getBoolean(\"process_text\", true))\n            \"webPort\" -> WebService.upHttpServer(activity)\n            \"backupPath\" -> upPreferenceSummary(key, sharedPreferences.getString(key, null))\n            downloadPathKey -> upPreferenceSummary(downloadPathKey, MApplication.downloadPath)\n        }\n\n        if (key == bookshelfPxKey || key == \"behaviorMain\") {\n            RxBus.get().post(RxBusTag.RECREATE, true)\n        } else if (key == \"process_text\") {\n            ProcessTextHelp.setProcessTextEnable(sharedPreferences.getBoolean(\"process_text\", true))\n        } else if (key == \"webPort\") {\n            WebService.upHttpServer(activity)\n        }\n    }\n\n    private fun upPreferenceSummary(preferenceKey: String, value: String?) {\n        val preference = findPreference(preferenceKey) ?: return\n        if (preference is ListPreference) {\n            val index = preference.findIndexOfValue(value)\n            // Set the summary to reflect the new value.\n            preference.summary = if (index >= 0) preference.entries[index] else null\n        } else {\n            preference.summary = value\n        }\n    }\n\n    override fun onPreferenceTreeClick(preferenceScreen: PreferenceScreen, preference: Preference): Boolean {\n        when (preference.key) {\n            getString(R.string.pk_download_path) -> {\n                selectDownloadPath(preference)\n            }\n            \"backupPath\" -> {\n                selectBackupFolder(activity)\n            }\n            \"webDavSetting\" -> {\n                val webDavSettingsFragment = WebDavSettingsFragment()\n                fragmentManager.beginTransaction().replace(R.id.settingsFrameLayout, webDavSettingsFragment, \"webDavSettings\").commit()\n            }\n            \"clearCache\" -> {\n                alert {\n                    titleResource = R.string.clear_cache\n                    messageResource = R.string.sure_del_download_book\n                    okButton {\n                        BookshelfHelp.clearCaches(true)\n                    }\n                    noButton {\n                        BookshelfHelp.clearCaches(false)\n                    }\n                }.show()\n            }\n        }\n        return super.onPreferenceTreeClick(preferenceScreen, preference)\n    }\n\n    private fun selectDownloadPath(preference: Preference) {\n        PermissionsCompat.Builder(activity)\n                .addPermissions(Permissions.READ_EXTERNAL_STORAGE, Permissions.WRITE_EXTERNAL_STORAGE)\n                .rationale(R.string.set_download_per)\n                .onGranted {\n                    val picker = FilePicker(activity, FilePicker.DIRECTORY)\n                    picker.setBackgroundColor(resources.getColor(R.color.background))\n                    picker.setTopBackgroundColor(resources.getColor(R.color.background))\n                    picker.setRootPath(preference.summary.toString())\n                    picker.setItemHeight(30)\n                    picker.setOnFilePickListener { currentPath: String ->\n                        if (!currentPath.contains(FileUtils.getSdCardPath())) {\n                            MApplication.getInstance().setDownloadPath(null)\n                        } else {\n                            MApplication.getInstance().setDownloadPath(currentPath)\n                        }\n                        preference.summary = MApplication.downloadPath\n                    }\n                    picker.show()\n                    picker.cancelButton.setText(R.string.restore_default)\n                    picker.cancelButton.setOnClickListener {\n                        picker.dismiss()\n                        MApplication.getInstance().setDownloadPath(null)\n                        preference.summary = MApplication.downloadPath\n                    }\n                }\n                .request()\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {\n        super.onActivityResult(requestCode, resultCode, data)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/ThemeSettingsFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.preference.ListPreference;\nimport android.preference.Preference;\nimport android.preference.PreferenceFragment;\nimport android.preference.PreferenceScreen;\n\nimport androidx.appcompat.app.AlertDialog;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.help.LauncherIcon;\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.view.activity.ThemeSettingActivity;\n\nimport java.util.Objects;\n\n/**\n * Created by GKF on 2017/12/16.\n * 设置\n */\npublic class ThemeSettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {\n    private ThemeSettingActivity settingActivity;\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        getPreferenceManager().setSharedPreferencesName(\"CONFIG\");\n        addPreferencesFromResource(R.xml.pref_settings_theme);\n        settingActivity = (ThemeSettingActivity) this.getActivity();\n        SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences();\n        sharedPreferences.edit().putString(\"launcher_icon\", LauncherIcon.getInUseIcon()).apply();\n    }\n\n    private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (Preference preference, Object value) -> {\n        String stringValue = value.toString();\n\n        if (preference instanceof ListPreference) {\n            ListPreference listPreference = (ListPreference) preference;\n            int index = listPreference.findIndexOfValue(stringValue);\n            // Set the summary to reflect the new value.\n            preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null);\n        } else {\n            // For all other preferences, set the summary to the value's\n            preference.setSummary(stringValue);\n        }\n        return true;\n    };\n\n    private static void bindPreferenceSummaryToValue(Preference preference) {\n        // Set the listener to watch for value changes.\n        preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);\n        sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,\n                preference.getContext().getSharedPreferences(\"CONFIG\", Context.MODE_PRIVATE).getString(preference.getKey(), \"\"));\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);\n    }\n\n    @Override\n    public void onPause() {\n        getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);\n        super.onPause();\n    }\n\n    @Override\n    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {\n        AlertDialog alertDialog;\n        switch (key) {\n            case \"launcher_icon\":\n                LauncherIcon.ChangeIcon(sharedPreferences.getString(\"launcher_icon\", getString(R.string.icon_main)));\n                break;\n            case \"behaviorMain\":\n                RxBus.get().post(RxBusTag.RECREATE, true);\n                break;\n            case \"E-InkMode\":\n                MApplication.getInstance().upEInkMode();\n                break;\n            case \"immersionStatusBar\":\n            case \"navigationBarColorChange\":\n                settingActivity.initImmersionBar();\n                RxBus.get().post(RxBusTag.IMMERSION_CHANGE, true);\n                break;\n            case \"colorPrimary\":\n            case \"colorAccent\":\n            case \"colorBackground\":\n                if (!ColorUtils.isColorLight(sharedPreferences.getInt(\"colorBackground\", settingActivity.getResources().getColor(R.color.md_grey_100)))) {\n                    alertDialog = new AlertDialog.Builder(getActivity())\n                            .setTitle(\"白天背景太暗\")\n                            .setMessage(\"将会恢复默认背景？\")\n                            .setPositiveButton(R.string.ok, (dialog, which) -> {\n                                settingActivity.preferences.edit().putInt(\"colorBackground\", settingActivity.getResources().getColor(R.color.md_grey_100)).apply();\n                                upTheme(false);\n                            })\n                            .setNegativeButton(R.string.cancel, (dialogInterface, i) -> upTheme(false))\n                            .show();\n                    ATH.setAlertDialogTint(alertDialog);\n                } else {\n                    upTheme(false);\n                }\n                break;\n            case \"colorPrimaryNight\":\n            case \"colorAccentNight\":\n            case \"colorBackgroundNight\":\n                if (ColorUtils.isColorLight(sharedPreferences.getInt(\"colorBackgroundNight\", settingActivity.getResources().getColor(R.color.md_grey_800)))) {\n                    alertDialog = new AlertDialog.Builder(getActivity())\n                            .setTitle(\"夜间背景太亮\")\n                            .setMessage(\"将会恢复默认背景？\")\n                            .setPositiveButton(R.string.ok, (dialog, which) -> {\n                                settingActivity.preferences.edit().putInt(\"colorBackgroundNight\", settingActivity.getResources().getColor(R.color.md_grey_800)).apply();\n                                upTheme(true);\n                            })\n                            .setNegativeButton(R.string.cancel, (dialogInterface, i) -> upTheme(true))\n                            .show();\n                    ATH.setAlertDialogTint(alertDialog);\n                } else {\n                    upTheme(true);\n                }\n                break;\n        }\n    }\n\n    private void upTheme(boolean isNightTheme) {\n        if (settingActivity.isNightTheme() == isNightTheme) {\n            MApplication.getInstance().upThemeStore();\n            RxBus.get().post(RxBusTag.RECREATE, true);\n            new Handler(Looper.getMainLooper()).postDelayed(() -> {\n                if (getActivity() != null) {\n                    getActivity().recreate();\n                }\n            }, 300);\n        }\n    }\n\n    @Override\n    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {\n        if (Objects.equals(preference.getKey(), \"defaultTheme\")) {\n            AlertDialog alertDialog = new AlertDialog.Builder(getActivity())\n                    .setTitle(\"恢复默认主题\")\n                    .setMessage(\"是否确认恢复？\")\n                    .setPositiveButton(R.string.ok, (dialog, which) -> {\n                        settingActivity.preferences.edit()\n                                .putInt(\"colorPrimary\", settingActivity.getResources().getColor(R.color.md_grey_100))\n                                .putInt(\"colorAccent\", settingActivity.getResources().getColor(R.color.md_pink_600))\n                                .putInt(\"colorBackground\", settingActivity.getResources().getColor(R.color.md_grey_100))\n                                .putInt(\"colorPrimaryNight\", settingActivity.getResources().getColor(R.color.md_grey_800))\n                                .putInt(\"colorAccentNight\", settingActivity.getResources().getColor(R.color.md_pink_800))\n                                .putInt(\"colorBackgroundNight\", settingActivity.getResources().getColor(R.color.md_grey_800))\n                                .apply();\n                        MApplication.getInstance().upThemeStore();\n                        RxBus.get().post(RxBusTag.RECREATE, true);\n                    })\n                    .setNegativeButton(R.string.cancel, (dialogInterface, i) -> {\n                    })\n                    .show();\n            ATH.setAlertDialogTint(alertDialog);\n        }\n        return super.onPreferenceTreeClick(preferenceScreen, preference);\n    }\n\n\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/fragment/WebDavSettingsFragment.java",
    "content": "package com.kunfei.bookshelf.view.fragment;\n\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.os.Bundle;\nimport android.preference.ListPreference;\nimport android.preference.Preference;\nimport android.preference.PreferenceFragment;\nimport android.preference.PreferenceScreen;\nimport android.text.TextUtils;\nimport android.widget.Toast;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.observer.MySingleObserver;\nimport com.kunfei.bookshelf.help.FileHelp;\nimport com.kunfei.bookshelf.help.ProcessTextHelp;\nimport com.kunfei.bookshelf.help.storage.BackupRestoreUi;\nimport com.kunfei.bookshelf.help.storage.WebDavHelp;\nimport com.kunfei.bookshelf.view.activity.SettingActivity;\n\nimport java.util.ArrayList;\nimport java.util.Objects;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.schedulers.Schedulers;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.DEFAULT_WEB_DAV_URL;\n\n/**\n * Created by GKF on 2017/12/16.\n * 设置\n */\npublic class WebDavSettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {\n    private SettingActivity settingActivity;\n    private CompositeDisposable compositeDisposable = new CompositeDisposable();\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        getPreferenceManager().setSharedPreferencesName(\"CONFIG\");\n        settingActivity = (SettingActivity) this.getActivity();\n        settingActivity.setupActionBar(\"WebDav设置\");\n        SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences();\n        SharedPreferences.Editor editor = sharedPreferences.edit();\n        boolean processTextEnabled = ProcessTextHelp.isProcessTextEnabled();\n        editor.putBoolean(\"process_text\", processTextEnabled);\n        if (Objects.equals(sharedPreferences.getString(getString(R.string.pk_download_path), \"\"), \"\")) {\n            editor.putString(getString(R.string.pk_download_path), FileHelp.getCachePath());\n        }\n        editor.apply();\n        addPreferencesFromResource(R.xml.pref_settings_web_dav);\n        bindPreferenceSummaryToValue(findPreference(\"web_dav_url\"));\n        bindPreferenceSummaryToValue(findPreference(\"web_dav_account\"));\n        bindPreferenceSummaryToValue(findPreference(\"web_dav_password\"));\n    }\n\n    private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (Preference preference, Object value) -> {\n        String stringValue = value.toString();\n\n        if (preference.getKey().equals(\"web_dav_url\")) {\n            if (TextUtils.isEmpty(stringValue)) {\n                preference.setSummary(DEFAULT_WEB_DAV_URL);\n            } else {\n                preference.setSummary(stringValue);\n            }\n        } else if (preference.getKey().equals(\"web_dav_account\")) {\n            if (TextUtils.isEmpty(stringValue)) {\n                preference.setSummary(\"输入你的WebDav账号\");\n            } else {\n                preference.setSummary(stringValue);\n            }\n        } else if (preference.getKey().equals(\"web_dav_password\")) {\n            if (TextUtils.isEmpty(stringValue)) {\n                preference.setSummary(\"输入你的WebDav授权密码\");\n            } else {\n                preference.setSummary(\"************\");\n            }\n        } else if (preference instanceof ListPreference) {\n            ListPreference listPreference = (ListPreference) preference;\n            int index = listPreference.findIndexOfValue(stringValue);\n            // Set the summary to reflect the new value.\n            preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null);\n        } else {\n            // For all other preferences, set the summary to the value's\n            preference.setSummary(stringValue);\n        }\n        return true;\n    };\n\n    private static void bindPreferenceSummaryToValue(Preference preference) {\n        // Set the listener to watch for value changes.\n        preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);\n        sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,\n                preference.getContext().getSharedPreferences(\"CONFIG\", Context.MODE_PRIVATE).getString(preference.getKey(), \"\"));\n    }\n\n    @Override\n    public void onResume() {\n        super.onResume();\n        getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);\n    }\n\n    @Override\n    public void onPause() {\n        getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);\n        super.onPause();\n    }\n\n    @Override\n    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {\n\n    }\n\n    @Override\n    public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {\n        if (preference.getKey().equals(\"web_dav_restore\")) {\n            restore();\n        }\n        return super.onPreferenceTreeClick(preferenceScreen, preference);\n    }\n\n    private void restore() {\n\n        Single.create((SingleOnSubscribe<ArrayList<String>>) emitter -> {\n            emitter.onSuccess(WebDavHelp.INSTANCE.getWebDavFileNames());\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MySingleObserver<ArrayList<String>>() {\n                    @Override\n                    public void onSuccess(ArrayList<String> strings) {\n                        if (!WebDavHelp.INSTANCE.showRestoreDialog(getActivity(), strings, BackupRestoreUi.INSTANCE)) {\n                            Toast.makeText(getActivity(), \"没有备份\", Toast.LENGTH_SHORT).show();\n                        }\n                    }\n                });\n    }\n\n    @Override\n    public void onDestroy() {\n        compositeDisposable.dispose();\n        super.onDestroy();\n    }\n\n    @Override\n    public void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/CheckAddShelfPop.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.popupwindow;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.PopupWindow;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.R;\n\npublic class CheckAddShelfPop extends PopupWindow {\n    private Context mContext;\n    private View view;\n    private OnItemClickListener itemClick;\n    private String bookName;\n\n    @SuppressLint(\"InflateParams\")\n    public CheckAddShelfPop(Context context, @NonNull String bookName, @NonNull OnItemClickListener itemClick) {\n        super(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n        mContext = context;\n        this.bookName = bookName;\n        this.itemClick = itemClick;\n        view = LayoutInflater.from(mContext).inflate(R.layout.mo_dialog_two, null);\n        this.setContentView(view);\n\n        initView();\n        setBackgroundDrawable(mContext.getResources().getDrawable(R.drawable.shape_pop_checkaddshelf_bg));\n        setFocusable(true);\n        setTouchable(true);\n    }\n\n    private void initView() {\n        TextView tvBookName = view.findViewById(R.id.tv_msg);\n        tvBookName.setText(mContext.getString(R.string.check_add_bookshelf, bookName));\n        TextView tvExit = view.findViewById(R.id.tv_cancel);\n        tvExit.setText(\"退出阅读\");\n        tvExit.setOnClickListener(v -> {\n            dismiss();\n            itemClick.clickExit();\n        });\n        TextView tvAddShelf = view.findViewById(R.id.tv_done);\n        tvAddShelf.setText(\"放入书架\");\n        tvAddShelf.setOnClickListener(v -> itemClick.clickAddShelf());\n    }\n\n    public interface OnItemClickListener {\n        void clickExit();\n\n        void clickAddShelf();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/KeyboardToolPop.kt",
    "content": "package com.kunfei.bookshelf.view.popupwindow\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport android.widget.PopupWindow\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.kunfei.bookshelf.base.adapter.ItemViewHolder\nimport com.kunfei.bookshelf.base.adapter.RecyclerAdapter\nimport com.kunfei.bookshelf.databinding.ItemTextBinding\nimport com.kunfei.bookshelf.databinding.PopupKeyboardToolBinding\nimport org.jetbrains.anko.sdk27.listeners.onClick\n\n\nclass KeyboardToolPop(\n        context: Context,\n        private val chars: List<String>,\n        val callBack: CallBack?\n) : PopupWindow(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) {\n\n    private val binding = PopupKeyboardToolBinding.inflate(LayoutInflater.from(context))\n\n    init {\n        isTouchable = true\n        isOutsideTouchable = false\n        isFocusable = false\n        inputMethodMode = INPUT_METHOD_NEEDED //解决遮盖输入法\n        contentView = binding.root\n        initRecyclerView()\n    }\n\n    private fun initRecyclerView() = with(contentView) {\n        val adapter = Adapter(context)\n        binding.recyclerView.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)\n        binding.recyclerView.adapter = adapter\n        adapter.setItems(chars)\n    }\n\n    inner class Adapter(context: Context) :\n            RecyclerAdapter<String, ItemTextBinding>(context) {\n\n        override fun getViewBinding(parent: ViewGroup): ItemTextBinding {\n            return ItemTextBinding.inflate(inflater, parent, false)\n        }\n\n        override fun convert(holder: ItemViewHolder, binding: ItemTextBinding, item: String, payloads: MutableList<Any>) {\n            with(binding) {\n                textView.text = item\n                root.onClick { callBack?.sendText(item) }\n            }\n        }\n\n        override fun registerListener(holder: ItemViewHolder, binding: ItemTextBinding) {\n\n        }\n    }\n\n    interface CallBack {\n        fun sendText(text: String)\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/MediaPlayerPop.java",
    "content": "package com.kunfei.bookshelf.view.popupwindow;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.PorterDuff;\nimport android.graphics.drawable.Drawable;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.widget.FrameLayout;\nimport android.widget.SeekBar;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.bumptech.glide.RequestBuilder;\nimport com.bumptech.glide.load.engine.DiskCacheStrategy;\nimport com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;\nimport com.bumptech.glide.request.RequestOptions;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.databinding.PopMediaPlayerBinding;\nimport com.kunfei.bookshelf.help.BlurTransformation;\nimport com.kunfei.bookshelf.help.glide.ImageLoader;\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.TimeUtils;\nimport com.kunfei.bookshelf.utils.theme.MaterialValueHelper;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\nimport java.text.DateFormat;\nimport java.text.SimpleDateFormat;\nimport java.util.Locale;\n\npublic class MediaPlayerPop extends FrameLayout {\n    @SuppressLint(\"ConstantLocale\")\n    private final DateFormat timeFormat = new SimpleDateFormat(\"mm:ss\", Locale.getDefault());\n\n    private PopMediaPlayerBinding binding = PopMediaPlayerBinding.inflate(LayoutInflater.from(getContext()), this, true);\n    private int primaryTextColor;\n    private Callback callback;\n\n    public MediaPlayerPop(@NonNull Context context) {\n        super(context);\n        init(context);\n    }\n\n    public MediaPlayerPop(@NonNull Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public MediaPlayerPop(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public MediaPlayerPop(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n        init(context);\n    }\n\n    private void init(Context context) {\n        binding.getRoot().setBackgroundColor(ThemeStore.primaryColor(context));\n        binding.vwBg.setOnClickListener(null);\n        primaryTextColor = MaterialValueHelper.getPrimaryTextColor(context, ColorUtils.isColorLight(ThemeStore.primaryColor(context)));\n        setColor(binding.ivSkipPrevious.getDrawable());\n        setColor(binding.ivSkipNext.getDrawable());\n        setColor(binding.ivChapter.getDrawable());\n        setColor(binding.ivTimer.getDrawable());\n        binding.seekBar.setEnabled(false);\n        binding.seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {\n\n            }\n\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n\n            }\n\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n                if (callback != null) {\n                    callback.onStopTrackingTouch(seekBar.getProgress());\n                }\n            }\n        });\n    }\n\n    private void setColor(Drawable drawable) {\n        drawable.mutate();\n        drawable.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_ATOP);\n    }\n\n    public void setCallback(Callback callback) {\n        this.callback = callback;\n    }\n\n    public void setSeekBarEnable(boolean enable) {\n        binding.seekBar.setEnabled(enable);\n    }\n\n    public void upAudioSize(int audioSize) {\n        binding.seekBar.setMax(audioSize);\n        binding.tvAllTime.setText(TimeUtils.millis2String(audioSize, timeFormat));\n    }\n\n    public void upAudioDur(int audioDur) {\n        binding.seekBar.setProgress(audioDur);\n        binding.tvDurTime.setText(TimeUtils.millis2String(audioDur, timeFormat));\n    }\n\n    public void setIvCoverBgClickListener(OnClickListener onClickListener) {\n        binding.ivCoverBg.setOnClickListener(onClickListener);\n    }\n\n    public void setPlayClickListener(OnClickListener onClickListener) {\n        binding.fabPlayStop.setOnClickListener(onClickListener);\n    }\n\n    public void setPrevClickListener(OnClickListener onClickListener) {\n        binding.ivSkipPrevious.setOnClickListener(onClickListener);\n    }\n\n    public void setNextClickListener(OnClickListener onClickListener) {\n        binding.ivSkipNext.setOnClickListener(onClickListener);\n    }\n\n    public void setIvTimerClickListener(OnClickListener onClickListener) {\n        binding.ivTimer.setOnClickListener(onClickListener);\n    }\n\n    public void setIvChapterClickListener(OnClickListener onClickListener) {\n        binding.ivChapter.setOnClickListener(onClickListener);\n    }\n\n    public void setFabReadAloudImage(int id) {\n        binding.fabPlayStop.setImageResource(id);\n    }\n\n    public void setCover(String coverPath) {\n        ImageLoader.INSTANCE.load(getContext(), coverPath)\n                .apply(new RequestOptions().dontAnimate().diskCacheStrategy(DiskCacheStrategy.RESOURCE).centerCrop()\n                        .placeholder(R.drawable.image_cover_default))\n                .into(binding.ivCover);\n        ImageLoader.INSTANCE.load(getContext(), coverPath)\n                .transition(DrawableTransitionOptions.withCrossFade(1500))\n                .thumbnail(defaultCover())\n                .centerCrop()\n                .apply(RequestOptions.bitmapTransform(new BlurTransformation(getContext(), 25)))\n                .into(binding.ivCoverBg);\n    }\n\n    private RequestBuilder<Drawable> defaultCover() {\n        return ImageLoader.INSTANCE.load(getContext(), R.drawable.image_cover_default)\n                .apply(RequestOptions.bitmapTransform(new BlurTransformation(getContext(), 25)));\n    }\n\n    public interface Callback {\n\n        void onStopTrackingTouch(int dur);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/MoreSettingPop.kt",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.popupwindow\n\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.CompoundButton\nimport android.widget.FrameLayout\nimport androidx.appcompat.app.AlertDialog\nimport com.hwangjr.rxbus.RxBus\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.constant.RxBusTag\nimport com.kunfei.bookshelf.databinding.PopMoreSettingBinding\nimport com.kunfei.bookshelf.help.ReadBookControl\nimport com.kunfei.bookshelf.utils.theme.ATH\nimport com.kunfei.bookshelf.widget.modialog.PageKeyDialog\nimport org.jetbrains.anko.sdk27.listeners.onClick\n\nclass MoreSettingPop : FrameLayout {\n\n    private val readBookControl = ReadBookControl.getInstance()\n    private var callback: Callback? = null\n    private val binding = PopMoreSettingBinding.inflate(LayoutInflater.from(context), this, true)\n\n    constructor(context: Context) : super(context) {\n        init(context)\n    }\n\n    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {\n        init(context)\n    }\n\n    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {\n        init(context)\n    }\n\n    private fun init(context: Context) {\n        binding.vwBg.setOnClickListener(null)\n    }\n\n    fun setListener(callback: Callback) {\n        this.callback = callback\n        initData()\n        bindEvent()\n    }\n\n    private fun bindEvent() {\n        setOnClickListener { this.visibility = View.GONE }\n        binding.sbImmersionStatusBar.setOnCheckedChangeListener { compoundButton: CompoundButton, b: Boolean ->\n            if (compoundButton.isPressed) {\n                readBookControl.immersionStatusBar = b\n                callback?.upBar()\n                RxBus.get().post(RxBusTag.RECREATE, true)\n            }\n        }\n        binding.sbLightNovelParagraph.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.lightNovelParagraph = isChecked\n                callback?.recreate()\n            }\n        }\n        binding.sbHideStatusBar.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.hideStatusBar = isChecked\n                callback?.recreate()\n            }\n        }\n        binding.sbToLh.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.toLh = isChecked\n                callback?.recreate()\n            }\n        }\n        binding.sbHideNavigationBar.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.hideNavigationBar = isChecked\n                initData()\n                callback?.recreate()\n            }\n        }\n        binding.swVolumeNextPage.setOnCheckedChangeListener { compoundButton: CompoundButton, b: Boolean ->\n            if (compoundButton.isPressed) {\n                readBookControl.canKeyTurn = b\n                upView()\n            }\n        }\n        binding.swReadAloudKey.setOnCheckedChangeListener { compoundButton: CompoundButton, b: Boolean ->\n            if (compoundButton.isPressed) {\n                readBookControl.aloudCanKeyTurn = b\n            }\n        }\n        binding.sbClick.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.canClickTurn = isChecked\n                upView()\n            }\n        }\n        binding.sbClickAllNext.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.clickAllNext = isChecked\n            }\n        }\n        binding.sbShowTitle.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.showTitle = isChecked\n                callback?.refreshPage()\n            }\n        }\n        binding.sbShowTimeBattery.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.showTimeBattery = isChecked\n                callback?.refreshPage()\n            }\n        }\n        binding.sbShowLine.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.showLine = isChecked\n                callback?.refreshPage()\n            }\n        }\n        binding.llScreenTimeOut.setOnClickListener {\n            val dialog = AlertDialog.Builder(context)\n                    .setTitle(context.getString(R.string.keep_light))\n                    .setSingleChoiceItems(\n                            context.resources.getStringArray(R.array.screen_time_out),\n                            readBookControl.screenTimeOut\n                    ) { dialogInterface: DialogInterface, i: Int ->\n                        readBookControl.screenTimeOut = i\n                        upScreenTimeOut(i)\n                        callback?.keepScreenOnChange(i)\n                        dialogInterface.dismiss()\n                    }\n                    .create()\n            dialog.show()\n            ATH.setAlertDialogTint(dialog)\n        }\n        binding.llJFConvert.setOnClickListener {\n            val dialog = AlertDialog.Builder(context)\n                    .setTitle(context.getString(R.string.jf_convert))\n                    .setSingleChoiceItems(context.resources.getStringArray(R.array.convert_s), readBookControl.textConvert) { dialogInterface: DialogInterface, i: Int ->\n                        readBookControl.textConvert = i\n                        upFConvert(i)\n                        dialogInterface.dismiss()\n                        callback?.refreshPage()\n                    }\n                    .create()\n            dialog.show()\n            ATH.setAlertDialogTint(dialog)\n        }\n        binding.llScreenDirection.setOnClickListener {\n            val dialog = AlertDialog.Builder(context)\n                    .setTitle(context.getString(R.string.screen_direction))\n                    .setSingleChoiceItems(context.resources.getStringArray(R.array.screen_direction_list_title), readBookControl.screenDirection) { dialogInterface: DialogInterface, i: Int ->\n                        readBookControl.screenDirection = i\n                        upScreenDirection(i)\n                        dialogInterface.dismiss()\n                        callback?.recreate()\n                    }\n                    .create()\n            dialog.show()\n            ATH.setAlertDialogTint(dialog)\n        }\n        binding.llNavigationBarColor.setOnClickListener {\n            val dialog = AlertDialog.Builder(context)\n                    .setTitle(context.getString(R.string.re_navigation_bar_color))\n                    .setSingleChoiceItems(context.resources.getStringArray(R.array.NavBarColors), readBookControl.navBarColor) { dialogInterface: DialogInterface, i: Int ->\n                        readBookControl.navBarColor = i\n                        upNavBarColor(i)\n                        dialogInterface.dismiss()\n                        callback?.recreate()\n                    }\n                    .create()\n            dialog.show()\n            ATH.setAlertDialogTint(dialog)\n        }\n        binding.sbSelectText.setOnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->\n            if (buttonView.isPressed) {\n                readBookControl.isCanSelectText = isChecked\n            }\n        }\n        binding.llClickKeyCode.onClick {\n            PageKeyDialog(context).show()\n        }\n    }\n\n    private fun initData() {\n        upScreenDirection(readBookControl.screenDirection)\n        upScreenTimeOut(readBookControl.screenTimeOut)\n        upFConvert(readBookControl.textConvert)\n        upNavBarColor(readBookControl.navBarColor)\n        binding.sbImmersionStatusBar.isChecked = readBookControl.immersionStatusBar\n        binding.swVolumeNextPage.isChecked = readBookControl.canKeyTurn\n        binding.swReadAloudKey.isChecked = readBookControl.aloudCanKeyTurn\n        binding.sbLightNovelParagraph.isChecked = readBookControl.lightNovelParagraph;\n        binding.sbHideStatusBar.isChecked = readBookControl.hideStatusBar\n        binding.sbToLh.isChecked = readBookControl.toLh\n        binding.sbHideNavigationBar.isChecked = readBookControl.hideNavigationBar\n        binding.sbClick.isChecked = readBookControl.canClickTurn\n        binding.sbClickAllNext.isChecked = readBookControl.clickAllNext\n        binding.sbShowTitle.isChecked = readBookControl.showTitle\n        binding.sbShowTimeBattery.isChecked = readBookControl.showTimeBattery\n        binding.sbShowLine.isChecked = readBookControl.showLine\n        binding.sbSelectText.isChecked = readBookControl.isCanSelectText\n        upView()\n    }\n\n    private fun upView() {\n        if (readBookControl.hideStatusBar) {\n            binding.sbShowTimeBattery.isEnabled = true\n            binding.sbToLh.isEnabled = true\n        } else {\n            binding.sbShowTimeBattery.isEnabled = false\n            binding.sbToLh.isEnabled = false\n        }\n        binding.swReadAloudKey.isEnabled = readBookControl.canKeyTurn\n        binding.sbClickAllNext.isEnabled = readBookControl.canClickTurn\n        if (readBookControl.hideNavigationBar) {\n            binding.llNavigationBarColor.isEnabled = false\n            binding.reNavBarColorVal.isEnabled = false\n        } else {\n            binding.llNavigationBarColor.isEnabled = true\n            binding.reNavBarColorVal.isEnabled = true\n        }\n    }\n\n    private fun upScreenTimeOut(screenTimeOut: Int) {\n        binding.tvScreenTimeOut.text = context.resources.getStringArray(R.array.screen_time_out)[screenTimeOut]\n    }\n\n    private fun upFConvert(fConvert: Int) {\n        binding.tvJFConvert.text = context.resources.getStringArray(R.array.convert_s)[fConvert]\n    }\n\n    private fun upScreenDirection(screenDirection: Int) {\n        val screenDirectionListTitle = context.resources.getStringArray(R.array.screen_direction_list_title)\n        if (screenDirection >= screenDirectionListTitle.size) {\n            binding.tvScreenDirection.text = screenDirectionListTitle[0]\n        } else {\n            binding.tvScreenDirection.text = screenDirectionListTitle[screenDirection]\n        }\n    }\n\n    private fun upNavBarColor(nColor: Int) {\n        binding.reNavBarColorVal.text = context.resources.getStringArray(R.array.NavBarColors)[nColor]\n    }\n\n    interface Callback {\n        fun upBar()\n        fun keepScreenOnChange(keepScreenOn: Int)\n        fun recreate()\n        fun refreshPage()\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadAdjustMarginPop.kt",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.popupwindow\n\nimport android.app.Activity\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport android.widget.SeekBar\nimport android.widget.TextView\nimport com.kunfei.bookshelf.databinding.PopReadAdjustMarginBinding\nimport com.kunfei.bookshelf.help.ReadBookControl\nimport org.jetbrains.anko.sdk27.listeners.onClick\n\nclass ReadAdjustMarginPop : FrameLayout {\n\n    val binding = PopReadAdjustMarginBinding.inflate(LayoutInflater.from(context), this, true)\n    private var activity: Activity? = null\n    private val readBookControl = ReadBookControl.getInstance()\n    private var callback: Callback? = null\n\n    constructor(context: Context) : super(context)\n\n    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)\n\n    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)\n\n    init {\n        binding.vwBg.setOnClickListener(null)\n    }\n\n    fun setListener(activity: Activity, callback: Callback) {\n        this.activity = activity\n        this.callback = callback\n        initData(0)\n        bindEvent()\n    }\n\n    fun show() {\n        initData(0)\n    }\n\n    private fun initData(flag: Int) {\n        if (flag == 0) {\n            // 字距\n            setSeekBarView(binding.hpbMrF, binding.tvHpbMrF, -0.5f, 0.5f, readBookControl.textLetterSpacing, 100)\n            // 行距\n            setSeekBarView(binding.hpbMrRm, binding.tvHpbMrRm, 0.5f, 3.0f, readBookControl.lineMultiplier, 10)\n            // 段距\n            setSeekBarView(binding.hpbMrDm, binding.tvHpbMrDm, 1.0f, 5.0f, readBookControl.paragraphSize, 10)\n        }\n        if (flag == 0 || flag == 1) {\n            // 正文边距\n            setSeekBarView(binding.hpbMrZT, binding.tvHpbMrZT, 0, 100, readBookControl.paddingTop)\n            setSeekBarView(binding.hpbMrZL, binding.tvHpbMrZL, 0, 100, readBookControl.paddingLeft)\n            setSeekBarView(binding.hpbMrZR, binding.tvHpbMrZR, 0, 100, readBookControl.paddingRight)\n            setSeekBarView(binding.hpbMrZB, binding.tvHpbMrZB, 0, 100, readBookControl.paddingBottom)\n        }\n        if (flag == 0 || flag == 2) {\n            // Tip边距\n            setSeekBarView(binding.hpbMrTT, binding.tvHpbMrTT, 0, 100, readBookControl.tipPaddingTop)\n            setSeekBarView(binding.hpbMrTL, binding.tvHpbMrTL, 0, 100, readBookControl.tipPaddingLeft)\n            setSeekBarView(binding.hpbMrTR, binding.tvHpbMrTR, 0, 100, readBookControl.tipPaddingRight)\n            setSeekBarView(binding.hpbMrTB, binding.tvHpbMrTB, 0, 100, readBookControl.tipPaddingBottom)\n        }\n    }\n\n    private fun bindEvent() = with(binding) {\n        //字距调节\n        hpbMrF.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {\n            override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {\n                readBookControl.textLetterSpacing = i / 100.0f - 0.5f\n                tvHpbMrF.text = String.format(\"%.2f\", readBookControl.textLetterSpacing)\n                callback?.upTextSize()\n            }\n\n            override fun onStartTrackingTouch(seekBar: SeekBar) {}\n            override fun onStopTrackingTouch(seekBar: SeekBar) {}\n        })\n        ivMrFAdd.onClick { hpbMrF.progress = hpbMrF.progress + 1 }\n        ivMrFRemove.onClick { hpbMrF.progress = hpbMrF.progress - 1 }\n\n        //行距调节\n        hpbMrRm.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {\n            override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {\n                readBookControl.lineMultiplier = i / 10.0f + 0.5f\n                tvHpbMrRm.text = String.format(\"%.1f\", readBookControl.lineMultiplier)\n                callback?.upTextSize()\n            }\n\n            override fun onStartTrackingTouch(seekBar: SeekBar) {}\n            override fun onStopTrackingTouch(seekBar: SeekBar) {}\n        })\n        ivMrRmAdd.onClick { hpbMrRm.progress = hpbMrRm.progress + 1 }\n        ivMrRmRemove.onClick { hpbMrRm.progress = hpbMrRm.progress - 1 }\n\n        //段距调节\n        hpbMrDm.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {\n            override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {\n                readBookControl.paragraphSize = i / 10.0f + 1.0f\n                tvHpbMrDm.text = String.format(\"%.1f\", readBookControl.paragraphSize)\n                callback?.upTextSize()\n            }\n\n            override fun onStartTrackingTouch(seekBar: SeekBar) {}\n            override fun onStopTrackingTouch(seekBar: SeekBar) {}\n        })\n        ivMrDmAdd.onClick { hpbMrDm.progress = hpbMrDm.progress + 1 }\n        ivMrDmRemove.onClick { hpbMrDm.progress = hpbMrDm.progress - 1 }\n\n        //段距调节\n        val pdChange = object : SeekBar.OnSeekBarChangeListener {\n            override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {\n                var flag = 1\n                when {\n                    seekBar === hpbMrZT -> readBookControl.paddingTop = i\n                    seekBar === hpbMrZL -> readBookControl.paddingLeft = i\n                    seekBar === hpbMrZR -> readBookControl.paddingRight = i\n                    seekBar === hpbMrZB -> readBookControl.paddingBottom = i\n                    else -> {\n                        flag = 2\n                        when {\n                            seekBar === hpbMrTT -> readBookControl.tipPaddingTop = i\n                            seekBar === hpbMrTL -> readBookControl.tipPaddingLeft = i\n                            seekBar === hpbMrTR -> readBookControl.tipPaddingRight = i\n                            else -> readBookControl.tipPaddingBottom = i\n                        }\n                    }\n                }\n                initData(flag)\n                callback?.upMargin()\n            }\n\n            override fun onStartTrackingTouch(seekBar: SeekBar) {}\n            override fun onStopTrackingTouch(seekBar: SeekBar) {}\n        }\n        hpbMrZT.setOnSeekBarChangeListener(pdChange)\n        hpbMrZL.setOnSeekBarChangeListener(pdChange)\n        hpbMrZR.setOnSeekBarChangeListener(pdChange)\n        hpbMrZB.setOnSeekBarChangeListener(pdChange)\n        hpbMrTT.setOnSeekBarChangeListener(pdChange)\n        hpbMrTL.setOnSeekBarChangeListener(pdChange)\n        hpbMrTR.setOnSeekBarChangeListener(pdChange)\n        hpbMrTB.setOnSeekBarChangeListener(pdChange)\n        ivMrZTAdd.onClick { hpbMrZT.progress = hpbMrZT.progress + 1 }\n        ivMrZTRemove.onClick { hpbMrZT.progress = hpbMrZT.progress - 1 }\n        ivMrZLAdd.onClick { hpbMrZL.progress = hpbMrZL.progress + 1 }\n        ivMrZLRemove.onClick { hpbMrZL.progress = hpbMrZL.progress - 1 }\n        ivMrZRAdd.onClick { hpbMrZR.progress = hpbMrZR.progress + 1 }\n        ivMrZRRemove.onClick { hpbMrZR.progress = hpbMrZR.progress - 1 }\n        ivMrZBAdd.onClick { hpbMrZB.progress = hpbMrZB.progress + 1 }\n        ivMrZBRemove.onClick { hpbMrZB.progress = hpbMrZB.progress - 1 }\n        ivMrTTAdd.onClick { hpbMrTT.progress = hpbMrTT.progress + 1 }\n        ivMrTTRemove.onClick { hpbMrTT.progress = hpbMrTT.progress - 1 }\n        ivMrTLAdd.onClick { hpbMrTL.progress = hpbMrTL.progress + 1 }\n        ivMrTLRemove.onClick { hpbMrTL.progress = hpbMrTL.progress - 1 }\n        ivMrTRAdd.onClick { hpbMrTR.progress = hpbMrTR.progress + 1 }\n        ivMrTRRemove.onClick { hpbMrTR.progress = hpbMrTR.progress - 1 }\n        ivMrTBAdd.onClick { hpbMrTB.progress = hpbMrTB.progress + 1 }\n        ivMrTBRemove.onClick { hpbMrTB.progress = hpbMrTB.progress - 1 }\n    }\n\n    private fun setSeekBarView(hpb: SeekBar, tv: TextView?, min: Float, max: Float, value: Float, p: Int) {\n        val a = (min * p).toInt()\n        val b = (max * p).toInt() - a\n        hpb.max = b\n        hpb.progress = (value * p).toInt() - a\n        when {\n            p >= 100 -> tv?.text = String.format(\"%.2f\", value)\n            p >= 10 -> tv?.text = String.format(\"%.1f\", value)\n            else -> tv?.text = String.format(\"%.0f\", value)\n        }\n    }\n\n    private fun setSeekBarView(hpb: SeekBar, tv: TextView, min: Int, max: Int, value: Int) {\n        hpb.max = max - min\n        hpb.progress = value - min\n        tv.text = String.format(\"%d\", value)\n    }\n\n    interface Callback {\n        fun upTextSize()\n        fun upMargin()\n        fun refresh()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadAdjustPop.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.popupwindow;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.WindowManager;\nimport android.widget.FrameLayout;\nimport android.widget.SeekBar;\n\nimport com.kunfei.bookshelf.databinding.PopReadAdjustBinding;\nimport com.kunfei.bookshelf.help.ReadBookControl;\n\npublic class ReadAdjustPop extends FrameLayout {\n\n    private PopReadAdjustBinding binding = PopReadAdjustBinding.inflate(LayoutInflater.from(getContext()), this, true);\n    private Activity activity;\n    private ReadBookControl readBookControl = ReadBookControl.getInstance();\n    private Callback callback;\n\n    public ReadAdjustPop(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public ReadAdjustPop(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public ReadAdjustPop(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    private void init(Context context) {\n        binding.vwBg.setOnClickListener(null);\n    }\n\n    public void setListener(Activity activity, Callback callback) {\n        this.activity = activity;\n        this.callback = callback;\n        initData();\n        bindEvent();\n        initLight();\n    }\n\n    public void show() {\n        initLight();\n    }\n\n    private void initData() {\n        binding.scbTtsFollowSys.setChecked(readBookControl.isSpeechRateFollowSys());\n        binding.hpbTtsSpeechRate.setEnabled(!readBookControl.isSpeechRateFollowSys());\n        //CPM范围设置 每分钟阅读200字到2000字 默认500字/分钟\n        binding.hpbClick.setMax(readBookControl.maxCPM - readBookControl.minCPM);\n        binding.hpbClick.setProgress(readBookControl.getCPM());\n        binding.tvAutoPage.setText(String.format(\"%sCPM\", readBookControl.getCPM()));\n        binding.hpbTtsSpeechRate.setProgress(readBookControl.getSpeechRate() - 5);\n    }\n\n    private void bindEvent() {\n        //亮度调节\n        binding.llFollowSys.setOnClickListener(v -> {\n            binding.scbFollowSys.setChecked(!binding.scbFollowSys.isChecked(), true);\n        });\n        binding.scbFollowSys.setOnCheckedChangeListener((checkBox, isChecked) -> {\n            readBookControl.setLightFollowSys(isChecked);\n            if (isChecked) {\n                //跟随系统\n                binding.hpbLight.setEnabled(false);\n                setScreenBrightness();\n            } else {\n                //不跟随系统\n                binding.hpbLight.setEnabled(true);\n                setScreenBrightness(readBookControl.getLight());\n            }\n        });\n        binding.hpbLight.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {\n                if (!readBookControl.getLightFollowSys()) {\n                    readBookControl.setLight(i);\n                    setScreenBrightness(i);\n                }\n            }\n\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n\n            }\n\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n\n            }\n        });\n\n        //自动翻页阅读速度(CPM)\n        binding.hpbClick.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {\n                binding.tvAutoPage.setText(String.format(\"%sCPM\", i + readBookControl.minCPM));\n                readBookControl.setCPM(i + readBookControl.minCPM);\n            }\n\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n\n            }\n\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n\n            }\n        });\n\n        //朗读语速调节\n        binding.llTtsSpeechRate.setOnClickListener(v -> {\n            binding.scbTtsFollowSys.setChecked(!binding.scbTtsFollowSys.isChecked(), true);\n        });\n        binding.scbTtsFollowSys.setOnCheckedChangeListener((checkBox, isChecked) -> {\n            if (isChecked) {\n                //跟随系统\n                binding.hpbTtsSpeechRate.setEnabled(false);\n                readBookControl.setSpeechRateFollowSys(true);\n                if (callback != null) {\n                    callback.speechRateFollowSys();\n                }\n            } else {\n                //不跟随系统\n                binding.hpbTtsSpeechRate.setEnabled(true);\n                readBookControl.setSpeechRateFollowSys(false);\n                if (callback != null) {\n                    callback.changeSpeechRate(readBookControl.getSpeechRate());\n                }\n            }\n        });\n        binding.hpbTtsSpeechRate.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {\n\n            }\n\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n\n            }\n\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n                readBookControl.setSpeechRate(seekBar.getProgress() + 5);\n                if (callback != null) {\n                    callback.changeSpeechRate(readBookControl.getSpeechRate());\n                }\n            }\n        });\n    }\n\n    public void setScreenBrightness() {\n        WindowManager.LayoutParams params = activity.getWindow().getAttributes();\n        params.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;\n        activity.getWindow().setAttributes(params);\n    }\n\n    public void setScreenBrightness(int value) {\n        if (value < 1) value = 1;\n        WindowManager.LayoutParams params = activity.getWindow().getAttributes();\n        params.screenBrightness = value * 1.0f / 255f;\n        activity.getWindow().setAttributes(params);\n    }\n\n    public void initLight() {\n        binding.hpbLight.setProgress(readBookControl.getLight());\n        binding.scbFollowSys.setChecked(readBookControl.getLightFollowSys());\n        if (!readBookControl.getLightFollowSys()) {\n            setScreenBrightness(readBookControl.getLight());\n        }\n    }\n\n    public interface Callback {\n        void changeSpeechRate(int speechRate);\n\n        void speechRateFollowSys();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadBottomMenu.java",
    "content": "package com.kunfei.bookshelf.view.popupwindow;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.SeekBar;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.databinding.PopReadMenuBinding;\nimport com.kunfei.bookshelf.service.ReadAloudService;\n\npublic class ReadBottomMenu extends FrameLayout {\n\n    private PopReadMenuBinding binding = PopReadMenuBinding.inflate(LayoutInflater.from(getContext()), this, true);\n    private Callback callback;\n\n    public ReadBottomMenu(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public ReadBottomMenu(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public ReadBottomMenu(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    private void init(Context context) {\n        binding.vwBg.setOnClickListener(null);\n        binding.vwNavigationBar.setOnClickListener(null);\n    }\n\n    public void setNavigationBarHeight(int height) {\n        ViewGroup.LayoutParams layoutParams = binding.vwNavigationBar.getLayoutParams();\n        layoutParams.height = height;\n        binding.vwNavigationBar.setLayoutParams(layoutParams);\n    }\n\n    public void setListener(Callback callback) {\n        this.callback = callback;\n        bindEvent();\n    }\n\n    private void bindEvent() {\n        binding.llReadAloudTimer.setOnClickListener(view -> callback.dismiss());\n        binding.llFloatingButton.setOnClickListener(view -> callback.dismiss());\n\n        //阅读进度\n        binding.hpbReadProgress.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {\n            @Override\n            public void onProgressChanged(SeekBar seekBar, int i, boolean b) {\n\n            }\n\n            @Override\n            public void onStartTrackingTouch(SeekBar seekBar) {\n\n            }\n\n            @Override\n            public void onStopTrackingTouch(SeekBar seekBar) {\n                callback.skipToPage(seekBar.getProgress());\n            }\n        });\n\n        //朗读定时\n        binding.fabReadAloudTimer.setOnClickListener(view -> ReadAloudService.setTimer(getContext(), 10));\n\n        //朗读\n        binding.fabReadAloud.setOnClickListener(view -> callback.onMediaButton());\n        //长按停止朗读\n        binding.fabReadAloud.setOnLongClickListener(view -> {\n            if (ReadAloudService.running) {\n                callback.toast(R.string.aloud_stop);\n                ReadAloudService.stop(getContext());\n            } else {\n                callback.toast(R.string.read_aloud);\n            }\n            return true;\n        });\n\n        //自动翻页\n        binding.fabAutoPage.setOnClickListener(view -> callback.autoPage());\n        binding.fabAutoPage.setOnLongClickListener(view -> {\n            callback.toast(R.string.auto_next_page);\n            return true;\n        });\n\n        //替换\n        binding.fabReplaceRule.setOnClickListener(view -> callback.openReplaceRule());\n        binding.fabReplaceRule.setOnLongClickListener(view -> {\n            callback.toast(R.string.replace_rule_title);\n            return true;\n        });\n\n        //夜间模式\n        binding.fabNightTheme.setOnClickListener(view -> callback.setNightTheme());\n        binding.fabNightTheme.setOnLongClickListener(view -> {\n            callback.toast(R.string.night_theme);\n            return true;\n        });\n\n        //上一章\n        binding.tvPre.setOnClickListener(view -> callback.skipPreChapter());\n\n        //下一章\n        binding.tvNext.setOnClickListener(view -> callback.skipNextChapter());\n\n        //目录\n        binding.llCatalog.setOnClickListener(view -> callback.openChapterList());\n\n        //调节\n        binding.llAdjust.setOnClickListener(view -> callback.openAdjust());\n\n        //界面\n        binding.llFont.setOnClickListener(view -> callback.openReadInterface());\n\n        //设置\n        binding.llSetting.setOnClickListener(view -> callback.openMoreSetting());\n\n        binding.tvReadAloudTimer.setOnClickListener(null);\n    }\n\n    public void setFabReadAloudImage(int id) {\n        binding.fabReadAloud.setImageResource(id);\n    }\n\n    public void setReadAloudTimer(boolean visibility) {\n        if (visibility) {\n            binding.llReadAloudTimer.setVisibility(VISIBLE);\n        } else {\n            binding.llReadAloudTimer.setVisibility(GONE);\n        }\n    }\n\n    public void setReadAloudTimer(String text) {\n        binding.tvReadAloudTimer.setText(text);\n    }\n\n    public void setFabReadAloudText(String text) {\n        binding.fabReadAloud.setContentDescription(text);\n    }\n\n    public SeekBar getReadProgress() {\n        return binding.hpbReadProgress;\n    }\n\n    public void setTvPre(boolean enable) {\n        binding.tvPre.setEnabled(enable);\n    }\n\n    public void setTvNext(boolean enable) {\n        binding.tvNext.setEnabled(enable);\n    }\n\n    public void setAutoPage(boolean autoPage) {\n        if (autoPage) {\n            binding.fabAutoPage.setImageResource(R.drawable.ic_auto_page_stop);\n            binding.fabAutoPage.setContentDescription(getContext().getString(R.string.auto_next_page_stop));\n        } else {\n            binding.fabAutoPage.setImageResource(R.drawable.ic_auto_page);\n            binding.fabAutoPage.setContentDescription(getContext().getString(R.string.auto_next_page));\n        }\n    }\n\n    public void setFabNightTheme(boolean isNightTheme) {\n        if (isNightTheme) {\n            binding.fabNightTheme.setImageResource(R.drawable.ic_daytime);\n        } else {\n            binding.fabNightTheme.setImageResource(R.drawable.ic_brightness);\n        }\n    }\n\n    public interface Callback {\n        void skipToPage(int page);\n\n        void onMediaButton();\n\n        void autoPage();\n\n        void setNightTheme();\n\n        void skipPreChapter();\n\n        void skipNextChapter();\n\n        void openReplaceRule();\n\n        void openChapterList();\n\n        void openAdjust();\n\n        void openReadInterface();\n\n        void openMoreSetting();\n\n        void toast(int id);\n\n        void dismiss();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadInterfacePop.kt",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.bookshelf.view.popupwindow\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.graphics.Color\nimport android.net.Uri\nimport android.os.Build\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.widget.FrameLayout\nimport androidx.appcompat.app.AlertDialog\nimport androidx.documentfile.provider.DocumentFile\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.databinding.PopReadInterfaceBinding\nimport com.kunfei.bookshelf.help.ReadBookControl\nimport com.kunfei.bookshelf.help.permission.Permissions\nimport com.kunfei.bookshelf.help.permission.PermissionsCompat\nimport com.kunfei.bookshelf.utils.*\nimport com.kunfei.bookshelf.utils.theme.ATH\nimport com.kunfei.bookshelf.view.activity.ReadBookActivity\nimport com.kunfei.bookshelf.view.activity.ReadStyleActivity\nimport com.kunfei.bookshelf.widget.font.FontSelector\nimport com.kunfei.bookshelf.widget.font.FontSelector.OnThisListener\nimport com.kunfei.bookshelf.widget.page.animation.PageAnimation\nimport timber.log.Timber\n\nclass ReadInterfacePop : FrameLayout {\n    private val binding = PopReadInterfaceBinding.inflate(\n        LayoutInflater.from(\n            context\n        ), this, true\n    )\n    private var activity: ReadBookActivity? = null\n    private val readBookControl = ReadBookControl.getInstance()\n    private var callback: Callback? = null\n\n    constructor(context: Context) : super(context) {\n        init()\n    }\n\n    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {\n        init()\n    }\n\n    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(\n        context,\n        attrs,\n        defStyleAttr\n    ) {\n        init()\n    }\n\n    private fun init() {\n        binding.vwBg.setOnClickListener(null)\n    }\n\n    fun setListener(readBookActivity: ReadBookActivity, callback: Callback) {\n        activity = readBookActivity\n        this.callback = callback\n        initData()\n        bindEvent()\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    private fun initData() {\n        setBg()\n        updateBg(readBookControl.textDrawableIndex)\n        updateBoldText(readBookControl.textBold)\n        updatePageMode(readBookControl.pageMode)\n        binding.nbTextSize.text = String.format(\"%d\", readBookControl.textSize)\n    }\n\n    /**\n     * 控件事件\n     */\n    @SuppressLint(\"DefaultLocale\")\n    private fun bindEvent() {\n        //字号减\n        binding.nbTextSizeDec.setOnClickListener {\n            var fontSize = readBookControl.textSize - 1\n            if (fontSize < 10) fontSize = 10\n            readBookControl.textSize = fontSize\n            binding.nbTextSize.text = String.format(\"%d\", readBookControl.textSize)\n            callback!!.upTextSize()\n        }\n        //字号加\n        binding.nbTextSizeAdd.setOnClickListener {\n            var fontSize = readBookControl.textSize + 1\n            if (fontSize > 40) fontSize = 40\n            readBookControl.textSize = fontSize\n            binding.nbTextSize.text = String.format(\"%d\", readBookControl.textSize)\n            callback!!.upTextSize()\n        }\n        //缩进\n        binding.flIndent.setOnClickListener {\n            val dialog = AlertDialog.Builder(\n                activity!!, R.style.alertDialogTheme\n            )\n                .setTitle(activity!!.getString(R.string.indent))\n                .setSingleChoiceItems(\n                    activity!!.resources.getStringArray(R.array.indent),\n                    readBookControl.indent\n                ) { dialogInterface: DialogInterface, i: Int ->\n                    readBookControl.indent = i\n                    callback!!.refresh()\n                    dialogInterface.dismiss()\n                }\n                .create()\n            dialog.show()\n            ATH.setAlertDialogTint(dialog)\n        }\n        //翻页模式\n        binding.tvPageMode.setOnClickListener {\n            val dialog = AlertDialog.Builder(\n                activity!!, R.style.alertDialogTheme\n            )\n                .setTitle(activity!!.getString(R.string.page_mode))\n                .setSingleChoiceItems(\n                    PageAnimation.Mode.getAllPageMode(),\n                    readBookControl.pageMode\n                ) { dialogInterface: DialogInterface, i: Int ->\n                    readBookControl.pageMode = i\n                    updatePageMode(i)\n                    callback!!.upPageMode()\n                    dialogInterface.dismiss()\n                }\n                .create()\n            dialog.show()\n            ATH.setAlertDialogTint(dialog)\n        }\n        //加粗切换\n        binding.flTextBold.setOnClickListener {\n            readBookControl.textBold = !readBookControl.textBold\n            updateBoldText(readBookControl.textBold)\n            callback!!.upTextSize()\n        }\n        //行距单倍\n        binding.tvRowDef0.setOnClickListener {\n            readBookControl.lineMultiplier = 0.6f\n            readBookControl.paragraphSize = 1.5f\n            callback!!.upTextSize()\n        }\n        //行距双倍\n        binding.tvRowDef1.setOnClickListener {\n            readBookControl.lineMultiplier = 1.2f\n            readBookControl.paragraphSize = 1.8f\n            callback!!.upTextSize()\n        }\n        //行距三倍\n        binding.tvRowDef2.setOnClickListener {\n            readBookControl.lineMultiplier = 1.8f\n            readBookControl.paragraphSize = 2.0f\n            callback!!.upTextSize()\n        }\n        //行距默认\n        binding.tvRowDef.setOnClickListener {\n            readBookControl.lineMultiplier = 1.0f\n            readBookControl.paragraphSize = 1.8f\n            callback!!.upTextSize()\n        }\n        //自定义间距\n        binding.tvOther.setOnClickListener { activity!!.readAdjustMarginIn() }\n        //背景选择\n        binding.civBgWhite.setOnClickListener {\n            updateBg(0)\n            callback!!.bgChange()\n        }\n        binding.civBgYellow.setOnClickListener {\n            updateBg(1)\n            callback!!.bgChange()\n        }\n        binding.civBgGreen.setOnClickListener {\n            updateBg(2)\n            callback!!.bgChange()\n        }\n        binding.civBgBlue.setOnClickListener {\n            updateBg(3)\n            callback!!.bgChange()\n        }\n        binding.civBgBlack.setOnClickListener {\n            updateBg(4)\n            callback!!.bgChange()\n        }\n        //自定义阅读样式\n        binding.civBgWhite.setOnLongClickListener { customReadStyle(0) }\n        binding.civBgYellow.setOnLongClickListener { customReadStyle(1) }\n        binding.civBgGreen.setOnLongClickListener { customReadStyle(2) }\n        binding.civBgBlue.setOnLongClickListener { customReadStyle(3) }\n        binding.civBgBlack.setOnLongClickListener { customReadStyle(4) }\n\n        //选择字体\n        binding.flTextFont.setOnClickListener {\n            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {\n                activity!!.selectFontDir()\n            } else {\n                PermissionsCompat.Builder(activity!!)\n                    .addPermissions(\n                        Permissions.READ_EXTERNAL_STORAGE,\n                        Permissions.WRITE_EXTERNAL_STORAGE\n                    )\n                    .rationale(R.string.get_storage_per)\n                    .onGranted {\n                        kotlin.runCatching {\n                            selectFont(\n                                DocumentUtils.listFiles(FileUtils.getSdCardPath() + \"/Fonts\") {\n                                    it.name.matches(FontSelector.fontRegex)\n                                }\n                            )\n                        }.onFailure {\n                            context.toastOnUi(\"获取文件出错\\n${it.localizedMessage}\")\n                        }\n                    }\n                    .request()\n            }\n        }\n\n        //长按清除字体\n        binding.flTextFont.setOnLongClickListener {\n            clearFontPath()\n            activity!!.toast(R.string.clear_font)\n            true\n        }\n    }\n\n    fun showFontSelector(uri: Uri) {\n        kotlin.runCatching {\n            val doc = DocumentFile.fromTreeUri(context, uri)\n            DocumentUtils.listFiles(doc!!.uri) {\n                it.name.matches(FontSelector.fontRegex)\n            }.let {\n                selectFont(it)\n            }\n        }.onFailure {\n            context.toastOnUi(\"获取文件列表出错\\n${it.localizedMessage}\")\n            Timber.e(it)\n        }\n    }\n\n    private fun selectFont(docItems: List<FileDoc?>?) {\n        FontSelector(context, readBookControl.fontPath)\n            .setListener(object : OnThisListener {\n                override fun setDefault() {\n                    clearFontPath()\n                }\n\n                override fun setFontPath(fileDoc: FileDoc) {\n                    setReadFonts(fileDoc)\n                }\n            })\n            .create(docItems)\n            .show()\n    }\n\n    //自定义阅读样式\n    private fun customReadStyle(index: Int): Boolean {\n        val intent = Intent(activity, ReadStyleActivity::class.java)\n        intent.putExtra(\"index\", index)\n        activity!!.startActivity(intent)\n        return false\n    }\n\n    //设置字体\n    fun setReadFonts(fileDoc: FileDoc) {\n        if (fileDoc.isContentScheme) {\n            val file = FileUtils.createFileIfNotExist(context.externalFiles, \"Fonts\", fileDoc.name)\n            file.writeBytes(fileDoc.uri.readBytes(context))\n            readBookControl.setReadBookFont(file.absolutePath)\n        } else {\n            readBookControl.setReadBookFont(fileDoc.uri.toString())\n        }\n        callback!!.refresh()\n    }\n\n    //清除字体\n    fun clearFontPath() {\n        readBookControl.setReadBookFont(null)\n        callback!!.refresh()\n    }\n\n    private fun updatePageMode(pageMode: Int) {\n        binding.tvPageMode.text = String.format(\"%s\", PageAnimation.Mode.getPageMode(pageMode))\n    }\n\n    private fun updateBoldText(isBold: Boolean) {\n        binding.flTextBold.isSelected = isBold\n    }\n\n    fun setBg() {\n        binding.tv0.setTextColor(readBookControl.getTextColor(0))\n        binding.tv1.setTextColor(readBookControl.getTextColor(1))\n        binding.tv2.setTextColor(readBookControl.getTextColor(2))\n        binding.tv3.setTextColor(readBookControl.getTextColor(3))\n        binding.tv4.setTextColor(readBookControl.getTextColor(4))\n        binding.civBgWhite.setImageDrawable(readBookControl.getBgDrawable(0, activity, 100, 180))\n        binding.civBgYellow.setImageDrawable(readBookControl.getBgDrawable(1, activity, 100, 180))\n        binding.civBgGreen.setImageDrawable(readBookControl.getBgDrawable(2, activity, 100, 180))\n        binding.civBgBlue.setImageDrawable(readBookControl.getBgDrawable(3, activity, 100, 180))\n        binding.civBgBlack.setImageDrawable(readBookControl.getBgDrawable(4, activity, 100, 180))\n    }\n\n    private fun updateBg(index: Int) {\n        binding.civBgWhite.borderColor = activity!!.getCompatColor(R.color.tv_text_default)\n        binding.civBgYellow.borderColor = activity!!.getCompatColor(R.color.tv_text_default)\n        binding.civBgGreen.borderColor = activity!!.getCompatColor(R.color.tv_text_default)\n        binding.civBgBlack.borderColor = activity!!.getCompatColor(R.color.tv_text_default)\n        binding.civBgBlue.borderColor = activity!!.getCompatColor(R.color.tv_text_default)\n        when (index) {\n            0 -> binding.civBgWhite.borderColor = Color.parseColor(\"#F3B63F\")\n            1 -> binding.civBgYellow.borderColor = Color.parseColor(\"#F3B63F\")\n            2 -> binding.civBgGreen.borderColor = Color.parseColor(\"#F3B63F\")\n            3 -> binding.civBgBlue.borderColor = Color.parseColor(\"#F3B63F\")\n            4 -> binding.civBgBlack.borderColor = Color.parseColor(\"#F3B63F\")\n        }\n        readBookControl.textDrawableIndex = index\n    }\n\n    interface Callback {\n        fun upPageMode()\n        fun upTextSize()\n        fun upMargin()\n        fun bgChange()\n        fun refresh()\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/view/popupwindow/ReadLongPressPop.java",
    "content": "package com.kunfei.bookshelf.view.popupwindow;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Path;\nimport android.graphics.RectF;\nimport android.graphics.Region;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.databinding.PopReadLongPressBinding;\nimport com.kunfei.bookshelf.help.ReadBookControl;\nimport com.kunfei.bookshelf.utils.DensityUtil;\n\npublic class ReadLongPressPop extends FrameLayout {\n\n    private PopReadLongPressBinding binding = PopReadLongPressBinding.inflate(LayoutInflater.from(getContext()), this, true);\n    //private ReadBookActivity activity;\n    private ReadBookControl readBookControl = ReadBookControl.getInstance();\n    private OnBtnClickListener clickListener;\n\n    public ReadLongPressPop(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public ReadLongPressPop(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public ReadLongPressPop(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    @Override\n    protected void dispatchDraw(Canvas canvas) {\n        Path path = new Path();\n        path.addRoundRect(new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()), DensityUtil.dp2px(getContext(), 4), DensityUtil.dp2px(getContext(), 4), Path.Direction.CW);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n            canvas.clipPath(path);\n        } else {\n            canvas.clipPath(path, Region.Op.REPLACE);\n        }\n\n        super.dispatchDraw(canvas);\n    }\n\n    private void init(Context context) {\n        binding.getRoot().setOnClickListener(null);\n    }\n\n    public void setListener(@NonNull OnBtnClickListener clickListener) {\n        //this.activity = readBookActivity;\n        this.clickListener = clickListener;\n        initData();\n        bindEvent();\n    }\n\n    private void initData() {\n\n    }\n\n    private void bindEvent() {\n\n        //复制\n        binding.flCp.setOnClickListener(v -> clickListener.copySelect());\n\n        //替换\n        binding.flReplace.setOnClickListener(v -> clickListener.replaceSelect());\n\n        //标记广告\n        binding.flReplaceAd.setOnClickListener(v -> clickListener.replaceSelectAd());\n    }\n\n    public interface OnBtnClickListener {\n        void copySelect();\n\n        void replaceSelect();\n\n        void replaceSelectAd();\n\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/HttpServer.java",
    "content": "package com.kunfei.bookshelf.web;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.web.controller.BookshelfController;\nimport com.kunfei.bookshelf.web.controller.SourceController;\nimport com.kunfei.bookshelf.web.utils.AssetsWeb;\nimport com.kunfei.bookshelf.web.utils.ReturnData;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\nimport fi.iki.elonen.NanoHTTPD;\n\npublic class HttpServer extends NanoHTTPD {\n    private AssetsWeb assetsWeb = new AssetsWeb(\"web\");\n\n    public HttpServer(int port) {\n        super(port);\n    }\n\n\n    @Override\n    public Response serve(IHTTPSession session) {\n        ReturnData returnData = null;\n        String uri = session.getUri();\n\n        try {\n            switch (session.getMethod().name()) {\n                case \"OPTIONS\":\n                    Response response = newFixedLengthResponse(\"\");\n                    response.addHeader(\"Access-Control-Allow-Methods\", \"POST\");\n                    response.addHeader(\"Access-Control-Allow-Headers\", \"content-type\");\n                    response.addHeader(\"Access-Control-Allow-Origin\", session.getHeaders().get(\"origin\"));\n                    //response.addHeader(\"Access-Control-Max-Age\", \"3600\");\n                    return response;\n\n                case \"POST\":\n                    Map<String, String> files = new HashMap<>();\n                    session.parseBody(files);\n                    String postData = files.get(\"postData\");\n\n                    switch (uri) {\n                        case \"/saveSource\":\n                            returnData = new SourceController().saveSource(postData);\n                            break;\n                        case \"/saveSources\":\n                            returnData = new SourceController().saveSources(postData);\n                            break;\n                        case \"/saveBook\":\n                            returnData = new BookshelfController().saveBook(postData);\n                            break;\n                        case \"/deleteSources\":\n                            returnData = new SourceController().deleteSources(postData);\n                    }\n                    break;\n\n                case \"GET\":\n                    Map<String, List<String>> parameters = session.getParameters();\n\n                    switch (uri) {\n                        case \"/getSource\":\n                            returnData = new SourceController().getSource(parameters);\n                            break;\n                        case \"/getSources\":\n                            returnData = new SourceController().getSources();\n                            break;\n                        case \"/getBookshelf\":\n                            returnData = new BookshelfController().getBookshelf();\n                            break;\n                        case \"/getChapterList\":\n                            returnData = new BookshelfController().getChapterList(parameters);\n                            break;\n                        case \"/getBookContent\":\n                            returnData = new BookshelfController().getBookContent(parameters);\n                            break;\n                    }\n                    break;\n            }\n\n            if (returnData == null) {\n                if (uri.endsWith(\"/\")) {\n                    uri = uri + \"index.html\";\n                }\n                return assetsWeb.getResponse(uri);\n            }\n\n            Response response = newFixedLengthResponse(new Gson().toJson(returnData));\n            response.addHeader(\"Access-Control-Allow-Methods\", \"GET, POST\");\n            response.addHeader(\"Access-Control-Allow-Origin\", session.getHeaders().get(\"origin\"));\n            return response;\n        } catch (Exception e) {\n            return newFixedLengthResponse(e.getMessage());\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/ShareServer.java",
    "content": "package com.kunfei.bookshelf.web;\n\nimport com.google.gson.Gson;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\n\nimport java.util.List;\n\nimport fi.iki.elonen.NanoHTTPD;\n\npublic class ShareServer extends NanoHTTPD {\n\n    private Callback callback;\n\n    public ShareServer(int port, Callback callback) {\n        super(port);\n        this.callback = callback;\n    }\n\n    @Override\n    public Response serve(IHTTPSession session) {\n        return newFixedLengthResponse(new Gson().toJson(callback.getSources()));\n    }\n\n    public interface Callback {\n        List<BookSourceBean> getSources();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/WebSocketServer.java",
    "content": "package com.kunfei.bookshelf.web;\n\nimport com.kunfei.bookshelf.web.controller.SourceDebugWebSocket;\n\nimport fi.iki.elonen.NanoWSD;\n\npublic class WebSocketServer extends NanoWSD {\n\n    public WebSocketServer(int port) {\n        super(port);\n    }\n\n    @Override\n    protected WebSocket openWebSocket(IHTTPSession handshake) {\n        if (handshake.getUri().equals(\"/sourceDebug\")) {\n            return new SourceDebugWebSocket(handshake);\n        }\n        return null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/controller/BookshelfController.java",
    "content": "package com.kunfei.bookshelf.web.controller;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.utils.GsonUtils;\nimport com.kunfei.bookshelf.web.utils.ReturnData;\n\nimport java.util.List;\nimport java.util.Map;\n\npublic class BookshelfController {\n\n    public ReturnData getBookshelf() {\n        List<BookShelfBean> shelfBeans = BookshelfHelp.getAllBook();\n        ReturnData returnData = new ReturnData();\n        if (shelfBeans.isEmpty()) {\n            return returnData.setErrorMsg(\"还没有添加小说\");\n        }\n        return returnData.setData(shelfBeans);\n    }\n\n    public ReturnData getChapterList(Map<String, List<String>> parameters) {\n        List<String> strings = parameters.get(\"url\");\n        ReturnData returnData = new ReturnData();\n        if (strings == null) {\n            return returnData.setErrorMsg(\"参数url不能为空，请指定书籍地址\");\n        }\n        List<BookChapterBean> chapterList = BookshelfHelp.getChapterList(strings.get(0));\n        return returnData.setData(chapterList);\n    }\n\n    public ReturnData getBookContent(Map<String, List<String>> parameters) {\n        List<String> strings = parameters.get(\"url\");\n        ReturnData returnData = new ReturnData();\n        if (strings == null) {\n            return returnData.setErrorMsg(\"参数url不能为空，请指定内容地址\");\n        }\n        BookChapterBean chapter = DbHelper.getDaoSession().getBookChapterBeanDao().load(strings.get(0));\n        if (chapter == null) {\n            return returnData.setErrorMsg(\"未找到\");\n        }\n        BookShelfBean bookShelfBean = BookshelfHelp.getBook(chapter.getNoteUrl());\n        if (bookShelfBean == null) {\n            return returnData.setErrorMsg(\"未找到\");\n        }\n        String content = BookshelfHelp.getChapterCache(bookShelfBean, chapter);\n        if (!TextUtils.isEmpty(content)) {\n            return returnData.setData(content);\n        }\n        try {\n            BookContentBean bookContentBean = WebBookModel.getInstance().getBookContent(bookShelfBean, chapter, null).blockingFirst();\n            return returnData.setData(bookContentBean.getDurChapterContent());\n        } catch (Exception e) {\n            return returnData.setErrorMsg(e.getMessage());\n        }\n    }\n\n    public ReturnData saveBook(String postData) {\n        BookShelfBean bookShelfBean = GsonUtils.parseJObject(postData, BookShelfBean.class);\n        ReturnData returnData = new ReturnData();\n        if (bookShelfBean != null) {\n            DbHelper.getDaoSession().getBookShelfBeanDao().insertOrReplace(bookShelfBean);\n            return returnData.setData(\"\");\n        }\n        return returnData.setErrorMsg(\"格式不对\");\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/controller/SourceController.java",
    "content": "package com.kunfei.bookshelf.web.controller;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.utils.GsonUtils;\nimport com.kunfei.bookshelf.web.utils.ReturnData;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\npublic class SourceController {\n\n    public ReturnData saveSource(String postData) {\n        BookSourceBean bookSourceBean = GsonUtils.parseJObject(postData, BookSourceBean.class);\n        ReturnData returnData = new ReturnData();\n        if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl())) {\n            return returnData.setErrorMsg(\"书源名称和URL不能为空\");\n        }\n        BookSourceManager.addBookSource(bookSourceBean);\n        return returnData.setData(\"\");\n    }\n\n    public ReturnData saveSources(String postData) {\n        List<BookSourceBean> bookSourceBeans = GsonUtils.parseJArray(postData, BookSourceBean.class);\n        List<BookSourceBean> okSources = new ArrayList<>();\n        for (BookSourceBean bookSourceBean : bookSourceBeans) {\n            if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl())) {\n                continue;\n            }\n            BookSourceManager.addBookSource(bookSourceBean);\n            okSources.add(bookSourceBean);\n        }\n        return (new ReturnData()).setData(okSources);\n    }\n\n    public ReturnData getSource(Map<String, List<String>> parameters) {\n        List<String> strings = parameters.get(\"url\");\n        ReturnData returnData = new ReturnData();\n        if (strings == null) {\n            return returnData.setErrorMsg(\"参数url不能为空，请指定书源地址\");\n        }\n        BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(strings.get(0));\n        if (bookSourceBean == null) {\n            return returnData.setErrorMsg(\"未找到书源，请检查书源地址\");\n        }\n        return returnData.setData(bookSourceBean);\n    }\n\n    public ReturnData getSources() {\n        List<BookSourceBean> bookSourceBeans = BookSourceManager.getAllBookSource();\n        ReturnData returnData = new ReturnData();\n        if (bookSourceBeans.size() == 0) {\n            return returnData.setErrorMsg(\"设备书源列表为空\");\n        }\n        return returnData.setData(BookSourceManager.getAllBookSource());\n    }\n\n    public ReturnData deleteSources(String postData) {\n        List<BookSourceBean> bookSourceBeans = GsonUtils.parseJArray(postData, BookSourceBean.class);\n        /*List<BookSourceBean> okSources= new ArrayList<>();*/\n        for (BookSourceBean bookSourceBean : bookSourceBeans) {\n            /*if (TextUtils.isEmpty(bookSourceBean.getBookSourceName()) || TextUtils.isEmpty(bookSourceBean.getBookSourceUrl())) {\n                continue;\n            }*/\n            BookSourceManager.removeBookSource(bookSourceBean);\n            /*if(BookSourceManager.getBookSourceByUrl(bookSourceBean.getBookSourceUrl()) == null){\n                okSources.add(bookSourceBean);\n            }*/\n        }\n        return (new ReturnData()).setData(\"已执行\"/*okSources*/);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/controller/SourceDebugWebSocket.java",
    "content": "package com.kunfei.bookshelf.web.controller;\n\nimport android.text.TextUtils;\n\nimport com.google.gson.Gson;\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.model.content.Debug;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport java.io.IOException;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\nimport fi.iki.elonen.NanoHTTPD;\nimport fi.iki.elonen.NanoWSD;\nimport io.reactivex.Observable;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\nimport static com.kunfei.bookshelf.constant.AppConstant.MAP_STRING;\n\npublic class SourceDebugWebSocket extends NanoWSD.WebSocket {\n    private CompositeDisposable compositeDisposable;\n\n    public SourceDebugWebSocket(NanoHTTPD.IHTTPSession handshakeRequest) {\n        super(handshakeRequest);\n    }\n\n    @Override\n    protected void onOpen() {\n        RxBus.get().register(this);\n        compositeDisposable = new CompositeDisposable();\n        Observable.interval(10, 10, TimeUnit.SECONDS)\n                .observeOn(Schedulers.io())\n                .subscribe(new MyObserver<Long>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(Long aLong) {\n                        try {\n                            ping(new byte[]{aLong.byteValue()});\n                        } catch (IOException e) {\n                            e.printStackTrace();\n                        }\n                    }\n                });\n    }\n\n    @Override\n    protected void onClose(NanoWSD.WebSocketFrame.CloseCode code, String reason, boolean initiatedByRemote) {\n        RxBus.get().unregister(this);\n        compositeDisposable.dispose();\n        Debug.SOURCE_DEBUG_TAG = null;\n    }\n\n    @Override\n    protected void onMessage(NanoWSD.WebSocketFrame message) {\n        if (!StringUtils.isJsonType(message.getTextPayload())) return;\n        Map<String, String> debugBean = new Gson().fromJson(message.getTextPayload(), MAP_STRING);\n        String tag = debugBean.get(\"tag\");\n        String key = debugBean.get(\"key\");\n        if (TextUtils.isEmpty(tag) || TextUtils.isEmpty(key)) {\n            try {\n                send(MApplication.getInstance().getString(R.string.cannot_empty));\n                close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, \"调试结束\", false);\n            } catch (IOException ignored) {\n            }\n            return;\n        }\n        Debug.newDebug(tag, key, compositeDisposable);\n    }\n\n    @Override\n    protected void onPong(NanoWSD.WebSocketFrame pong) {\n\n    }\n\n    @Override\n    protected void onException(IOException exception) {\n        Debug.SOURCE_DEBUG_TAG = null;\n    }\n\n    @Subscribe(thread = EventThread.EXECUTOR, tags = {@Tag(RxBusTag.PRINT_DEBUG_LOG)})\n    public void printDebugLog(String msg) {\n        try {\n            send(msg);\n            if (msg.equals(\"finish\")) {\n                close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, \"调试结束\", false);\n                Debug.SOURCE_DEBUG_TAG = null;\n            }\n        } catch (IOException e) {\n            Debug.SOURCE_DEBUG_TAG = null;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/utils/AssetsWeb.java",
    "content": "package com.kunfei.bookshelf.web.utils;\n\nimport android.content.res.AssetManager;\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.MApplication;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\n\nimport fi.iki.elonen.NanoHTTPD;\n\n\npublic class AssetsWeb {\n    private AssetManager assetManager;\n    private String rootPath = \"web\";\n\n    public AssetsWeb(String rootPath) {\n        if (!TextUtils.isEmpty(rootPath)) {\n            this.rootPath = rootPath;\n        }\n        assetManager = MApplication.getInstance().getAssets();\n    }\n\n    public NanoHTTPD.Response getResponse(String path) throws IOException {\n        path = (rootPath + path).replaceAll(\"/+\", File.separator);\n        InputStream inputStream = assetManager.open(path);\n        return NanoHTTPD.newChunkedResponse(NanoHTTPD.Response.Status.OK,\n                getMimeType(path),\n                inputStream);\n    }\n\n    private String getMimeType(String path) {\n        String suffix = path.substring(path.lastIndexOf(\".\"));\n        String mimeType = \"text/html\";\n        if (suffix.equalsIgnoreCase(\".html\") || suffix.equalsIgnoreCase(\".htm\")) {\n            mimeType = \"text/html\";\n        } else if (suffix.equalsIgnoreCase(\".js\")) {\n            mimeType = \"text/javascript\";\n        } else if (suffix.equalsIgnoreCase(\".css\")) {\n            mimeType = \"text/css\";\n        } else if (suffix.equalsIgnoreCase(\".ico\")) {\n            mimeType = \"image/x-icon\";\n        }\n        return mimeType;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/web/utils/ReturnData.java",
    "content": "package com.kunfei.bookshelf.web.utils;\n\n\npublic class ReturnData {\n\n    private boolean isSuccess;\n\n    private int errorCode;\n\n    private String errorMsg;\n\n    private Object data;\n\n    public ReturnData() {\n        this.isSuccess = false;\n        this.errorMsg = \"未知错误,请联系开发者!\";\n    }\n\n    public boolean isSuccess() {\n        return isSuccess;\n    }\n\n    public void setSuccess(boolean success) {\n        isSuccess = success;\n    }\n\n    public int getErrorCode() {\n        return errorCode;\n    }\n\n    public void setErrorCode(int errorCode) {\n        this.errorCode = errorCode;\n    }\n\n    public String getErrorMsg() {\n        return errorMsg;\n    }\n\n    public ReturnData setErrorMsg(String errorMsg) {\n        this.isSuccess = false;\n        this.errorMsg = errorMsg;\n        return this;\n    }\n\n    public Object getData() {\n        return data;\n    }\n\n    public ReturnData setData(Object data) {\n        this.isSuccess = true;\n        this.errorMsg = \"\";\n        this.data = data;\n        return this;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/BadgeView.java",
    "content": "package com.kunfei.bookshelf.widget;\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.graphics.drawable.ShapeDrawable;\nimport android.graphics.drawable.shapes.RoundRectShape;\nimport android.text.TextUtils;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.FrameLayout.LayoutParams;\nimport android.widget.TabWidget;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\nimport com.kunfei.bookshelf.R;\n\n\n/**\n * Created by milad heydari on 5/6/2016.\n */\npublic class BadgeView extends AppCompatTextView {\n\n    private boolean mHideOnNull = true;\n    private float radius;\n\n    public BadgeView(Context context) {\n        this(context, null);\n    }\n\n    public BadgeView(Context context, AttributeSet attrs) {\n        this(context, attrs, android.R.attr.textViewStyle);\n    }\n\n    public BadgeView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n\n        init();\n    }\n\n    private void init() {\n        if (!(getLayoutParams() instanceof LayoutParams)) {\n            LayoutParams layoutParams =\n                    new LayoutParams(\n                            ViewGroup.LayoutParams.WRAP_CONTENT,\n                            ViewGroup.LayoutParams.WRAP_CONTENT,\n                            Gravity.CENTER);\n            setLayoutParams(layoutParams);\n        }\n\n        // set default font\n        setTextColor(Color.WHITE);\n        //setTypeface(Typeface.DEFAULT_BOLD);\n        setTextSize(TypedValue.COMPLEX_UNIT_SP, 11);\n        setPadding(dip2Px(5), dip2Px(1), dip2Px(5), dip2Px(1));\n        radius = 8;\n\n        // set default background\n        setBackground(radius, Color.parseColor(\"#d3321b\"));\n\n        setGravity(Gravity.CENTER);\n\n        // default values\n        setHideOnNull(true);\n        setBadgeCount(0);\n        setMinWidth(dip2Px(16));\n        setMinHeight(dip2Px(16));\n    }\n\n    public void setBackground(float dipRadius, int badgeColor) {\n        int radius = dip2Px(dipRadius);\n        float[] radiusArray = new float[]{radius, radius, radius, radius, radius, radius, radius, radius};\n\n        RoundRectShape roundRect = new RoundRectShape(radiusArray, null, null);\n        ShapeDrawable bgDrawable = new ShapeDrawable(roundRect);\n        bgDrawable.getPaint().setColor(badgeColor);\n        setBackground(bgDrawable);\n    }\n\n    public void setBackground(int badgeColor) {\n        setBackground(radius, badgeColor);\n    }\n\n    /**\n     * @return Returns true if view is hidden on badge value 0 or null;\n     */\n    public boolean isHideOnNull() {\n        return mHideOnNull;\n    }\n\n    /**\n     * @param hideOnNull the hideOnNull to set\n     */\n    public void setHideOnNull(boolean hideOnNull) {\n        mHideOnNull = hideOnNull;\n        setText(getText());\n    }\n\n    /**\n     * @see android.widget.TextView#setText(java.lang.CharSequence, android.widget.TextView.BufferType)\n     */\n    @Override\n    public void setText(CharSequence text, BufferType type) {\n        if (isHideOnNull() && TextUtils.isEmpty(text)) {\n            setVisibility(GONE);\n        } else {\n            setVisibility(VISIBLE);\n        }\n        super.setText(text, type);\n    }\n\n    public void setBadgeCount(int count) {\n        setText(String.valueOf(count));\n        if (count == 0) {\n            setVisibility(GONE);\n        }\n    }\n\n    public void setHighlight(boolean highlight) {\n        setBackground(getResources().getColor(highlight ? R.color.highlight : R.color.darker_gray));\n    }\n\n    public Integer getBadgeCount() {\n        if (getText() == null) {\n            return null;\n        }\n        String text = getText().toString();\n        try {\n            return Integer.parseInt(text);\n        } catch (NumberFormatException e) {\n            return null;\n        }\n    }\n\n    public void setBadgeGravity(int gravity) {\n        LayoutParams params = (LayoutParams) getLayoutParams();\n        params.gravity = gravity;\n        setLayoutParams(params);\n    }\n\n    public int getBadgeGravity() {\n        LayoutParams params = (LayoutParams) getLayoutParams();\n        return params.gravity;\n    }\n\n    public void setBadgeMargin(int dipMargin) {\n        setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin);\n    }\n\n    public void setBadgeMargin(int leftDipMargin, int topDipMargin, int rightDipMargin, int bottomDipMargin) {\n        LayoutParams params = (LayoutParams) getLayoutParams();\n        params.leftMargin = dip2Px(leftDipMargin);\n        params.topMargin = dip2Px(topDipMargin);\n        params.rightMargin = dip2Px(rightDipMargin);\n        params.bottomMargin = dip2Px(bottomDipMargin);\n        setLayoutParams(params);\n    }\n\n    public int[] getBadgeMargin() {\n        LayoutParams params = (LayoutParams) getLayoutParams();\n        return new int[]{params.leftMargin, params.topMargin, params.rightMargin, params.bottomMargin};\n    }\n\n    public void incrementBadgeCount(int increment) {\n        Integer count = getBadgeCount();\n        if (count == null) {\n            setBadgeCount(increment);\n        } else {\n            setBadgeCount(increment + count);\n        }\n    }\n\n    public void decrementBadgeCount(int decrement) {\n        incrementBadgeCount(-decrement);\n    }\n\n    /**\n     * Attach the BadgeView to the TabWidget\n     *\n     * @param target   the TabWidget to attach the BadgeView\n     * @param tabIndex index of the tab\n     */\n    public void setTargetView(TabWidget target, int tabIndex) {\n        View tabView = target.getChildTabViewAt(tabIndex);\n        setTargetView(tabView);\n    }\n\n    /**\n     * Attach the BadgeView to the target view\n     *\n     * @param target the view to attach the BadgeView\n     */\n    public void setTargetView(View target) {\n        if (getParent() != null) {\n            ((ViewGroup) getParent()).removeView(this);\n        }\n\n        if (target == null) {\n            return;\n        }\n\n        if (target.getParent() instanceof FrameLayout) {\n            ((FrameLayout) target.getParent()).addView(this);\n\n        } else if (target.getParent() instanceof ViewGroup) {\n            // use a new Framelayout container for adding badge\n            ViewGroup parentContainer = (ViewGroup) target.getParent();\n            int groupIndex = parentContainer.indexOfChild(target);\n            parentContainer.removeView(target);\n\n            FrameLayout badgeContainer = new FrameLayout(getContext());\n            ViewGroup.LayoutParams parentLayoutParams = target.getLayoutParams();\n\n            badgeContainer.setLayoutParams(parentLayoutParams);\n            target.setLayoutParams(new ViewGroup.LayoutParams(\n                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));\n\n            parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams);\n            badgeContainer.addView(target);\n\n            badgeContainer.addView(this);\n        }\n\n    }\n\n    /**\n     * converts dip to px\n     */\n    private int dip2Px(float dip) {\n        return (int) (dip * getContext().getResources().getDisplayMetrics().density + 0.5f);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/HorizontalListView.java",
    "content": "package com.kunfei.bookshelf.widget;\n\nimport android.content.Context;\nimport android.database.DataSetObserver;\nimport android.graphics.Rect;\nimport android.util.AttributeSet;\nimport android.view.GestureDetector;\nimport android.view.GestureDetector.OnGestureListener;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.ListAdapter;\nimport android.widget.Scroller;\n\nimport java.util.LinkedList;\nimport java.util.Queue;\n\npublic class HorizontalListView extends AdapterView<ListAdapter> {\n\n    public boolean mAlwaysOverrideTouch = true;\n    protected ListAdapter mAdapter;\n    private int mLeftViewIndex = -1;\n    private int mRightViewIndex = 0;\n    protected int mCurrentX;\n    protected int mNextX;\n    private int mMaxX = Integer.MAX_VALUE;\n    private int mDisplayOffset = 0;\n    protected Scroller mScroller;\n    private GestureDetector mGesture;\n    private Queue<View> mRemovedViewQueue = new LinkedList<View>();\n    private OnItemSelectedListener mOnItemSelected;\n    private OnItemClickListener mOnItemClicked;\n    private OnItemLongClickListener mOnItemLongClicked;\n    private boolean mDataChanged = false;\n\n\n    public HorizontalListView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        initView();\n    }\n\n    private synchronized void initView() {\n        mLeftViewIndex = -1;\n        mRightViewIndex = 0;\n        mDisplayOffset = 0;\n        mCurrentX = 0;\n        mNextX = 0;\n        mMaxX = Integer.MAX_VALUE;\n        mScroller = new Scroller(getContext());\n        mGesture = new GestureDetector(getContext(), mOnGesture);\n    }\n\n    @Override\n    public void setOnItemSelectedListener(OnItemSelectedListener listener) {\n        mOnItemSelected = listener;\n    }\n\n    @Override\n    public void setOnItemClickListener(OnItemClickListener listener){\n        mOnItemClicked = listener;\n    }\n\n    @Override\n    public void setOnItemLongClickListener(OnItemLongClickListener listener) {\n        mOnItemLongClicked = listener;\n    }\n\n    private DataSetObserver mDataObserver = new DataSetObserver() {\n\n        @Override\n        public void onChanged() {\n            synchronized(HorizontalListView.this){\n                mDataChanged = true;\n            }\n            invalidate();\n            requestLayout();\n        }\n\n        @Override\n        public void onInvalidated() {\n            reset();\n            invalidate();\n            requestLayout();\n        }\n\n    };\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        getParent().requestDisallowInterceptTouchEvent(true);\n        return mGesture.onTouchEvent(ev);\n    };\n    @Override\n    public ListAdapter getAdapter() {\n        return mAdapter;\n    }\n\n    @Override\n    public View getSelectedView() {\n        return null;\n    }\n\n    @Override\n    public void setAdapter(ListAdapter adapter) {\n        if(mAdapter != null) {\n            mAdapter.unregisterDataSetObserver(mDataObserver);\n        }\n        mAdapter = adapter;\n        mAdapter.registerDataSetObserver(mDataObserver);\n        reset();\n    }\n\n    private synchronized void reset(){\n        initView();\n        removeAllViewsInLayout();\n        requestLayout();\n    }\n\n    @Override\n    public void setSelection(int position) {\n    }\n\n    private void addAndMeasureChild(final View child, int viewPos) {\n        LayoutParams params = child.getLayoutParams();\n        if(params == null) {\n            params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);\n        }\n\n        addViewInLayout(child, viewPos, params, true);\n        child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),\n                MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));\n    }\n\n\n\n    @Override\n    protected synchronized void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        super.onLayout(changed, left, top, right, bottom);\n\n        if(mAdapter == null){\n            return;\n        }\n\n        if(mDataChanged){\n            int oldCurrentX = mCurrentX;\n            initView();\n            removeAllViewsInLayout();\n            mNextX = oldCurrentX;\n            mDataChanged = false;\n        }\n\n        if(mScroller.computeScrollOffset()){\n            mNextX = mScroller.getCurrX();\n        }\n\n        if(mNextX <= 0){\n            mNextX = 0;\n            mScroller.forceFinished(true);\n        }\n        if(mNextX >= mMaxX) {\n            mNextX = mMaxX;\n            mScroller.forceFinished(true);\n        }\n\n        int dx = mCurrentX - mNextX;\n\n        removeNonVisibleItems(dx);\n        fillList(dx);\n        positionItems(dx);\n\n        mCurrentX = mNextX;\n\n        if(!mScroller.isFinished()){\n            post(new Runnable(){\n                @Override\n                public void run() {\n                    requestLayout();\n                }\n            });\n\n        }\n    }\n\n    private void fillList(final int dx) {\n        int edge = 0;\n        View child = getChildAt(getChildCount()-1);\n        if(child != null) {\n            edge = child.getRight();\n        }\n        fillListRight(edge, dx);\n\n        edge = 0;\n        child = getChildAt(0);\n        if(child != null) {\n            edge = child.getLeft();\n        }\n        fillListLeft(edge, dx);\n\n\n    }\n\n    private void fillListRight(int rightEdge, final int dx) {\n        while(rightEdge + dx < getWidth() && mRightViewIndex < mAdapter.getCount()) {\n\n            View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this);\n            addAndMeasureChild(child, -1);\n            rightEdge += child.getMeasuredWidth();\n\n            if(mRightViewIndex == mAdapter.getCount()-1) {\n                mMaxX = mCurrentX + rightEdge - getWidth();\n            }\n\n            if (mMaxX < 0) {\n                mMaxX = 0;\n            }\n            mRightViewIndex++;\n        }\n\n    }\n\n    private void fillListLeft(int leftEdge, final int dx) {\n        while(leftEdge + dx > 0 && mLeftViewIndex >= 0) {\n            View child = mAdapter.getView(mLeftViewIndex, mRemovedViewQueue.poll(), this);\n            addAndMeasureChild(child, 0);\n            leftEdge -= child.getMeasuredWidth();\n            mLeftViewIndex--;\n            mDisplayOffset -= child.getMeasuredWidth();\n        }\n    }\n\n    private void removeNonVisibleItems(final int dx) {\n        View child = getChildAt(0);\n        while(child != null && child.getRight() + dx <= 0) {\n            mDisplayOffset += child.getMeasuredWidth();\n            mRemovedViewQueue.offer(child);\n            removeViewInLayout(child);\n            mLeftViewIndex++;\n            child = getChildAt(0);\n\n        }\n\n        child = getChildAt(getChildCount()-1);\n        while(child != null && child.getLeft() + dx >= getWidth()) {\n            mRemovedViewQueue.offer(child);\n            removeViewInLayout(child);\n            mRightViewIndex--;\n            child = getChildAt(getChildCount()-1);\n        }\n    }\n\n    private void positionItems(final int dx) {\n        if(getChildCount() > 0){\n            mDisplayOffset += dx;\n            int left = mDisplayOffset;\n            for(int i=0;i<getChildCount();i++){\n                View child = getChildAt(i);\n                int childWidth = child.getMeasuredWidth();\n                child.layout(left, 0, left + childWidth, child.getMeasuredHeight());\n                left += childWidth + child.getPaddingRight();\n            }\n        }\n    }\n\n    public synchronized void scrollTo(int x) {\n        mScroller.startScroll(mNextX, 0, x - mNextX, 0);\n        requestLayout();\n    }\n\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent ev) {\n        boolean handled = super.dispatchTouchEvent(ev);\n        handled |= mGesture.onTouchEvent(ev);\n        return handled;\n    }\n\n    protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,\n                              float velocityY) {\n        synchronized(HorizontalListView.this){\n            mScroller.fling(mNextX, 0, (int)-velocityX, 0, 0, mMaxX, 0, 0);\n        }\n        requestLayout();\n\n        return true;\n    }\n\n    protected boolean onDown(MotionEvent e) {\n        mScroller.forceFinished(true);\n        return true;\n    }\n\n    private OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() {\n\n        @Override\n        public boolean onDown(MotionEvent e) {\n            return HorizontalListView.this.onDown(e);\n        }\n\n        @Override\n        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,\n                               float velocityY) {\n            return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);\n        }\n\n        @Override\n        public boolean onScroll(MotionEvent e1, MotionEvent e2,\n                                float distanceX, float distanceY) {\n\n            synchronized(HorizontalListView.this){\n                mNextX += (int)distanceX;\n            }\n            requestLayout();\n\n            return true;\n        }\n\n        @Override\n        public boolean onSingleTapConfirmed(MotionEvent e) {\n            for(int i=0;i<getChildCount();i++){\n                View child = getChildAt(i);\n                if (isEventWithinView(e, child)) {\n                    if(mOnItemClicked != null){\n                        mOnItemClicked.onItemClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId( mLeftViewIndex + 1 + i ));\n                    }\n                    if(mOnItemSelected != null){\n                        mOnItemSelected.onItemSelected(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId( mLeftViewIndex + 1 + i ));\n                    }\n                    break;\n                }\n\n            }\n            return true;\n        }\n\n        @Override\n        public void onLongPress(MotionEvent e) {\n            int childCount = getChildCount();\n            for (int i = 0; i < childCount; i++) {\n                View child = getChildAt(i);\n                if (isEventWithinView(e, child)) {\n                    if (mOnItemLongClicked != null) {\n                        mOnItemLongClicked.onItemLongClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i));\n                    }\n                    break;\n                }\n\n            }\n        }\n\n        private boolean isEventWithinView(MotionEvent e, View child) {\n            Rect viewRect = new Rect();\n            int[] childPosition = new int[2];\n            child.getLocationOnScreen(childPosition);\n            int left = childPosition[0];\n            int right = left + child.getWidth();\n            int top = childPosition[1];\n            int bottom = top + child.getHeight();\n            viewRect.set(left, top, right, bottom);\n            return viewRect.contains((int) e.getRawX(), (int) e.getRawY());\n        }\n    };\n\n\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/RotateLoading.java",
    "content": "package com.kunfei.bookshelf.widget;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorSet;\nimport android.animation.ObjectAnimator;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.RectF;\nimport android.util.AttributeSet;\nimport android.util.TypedValue;\nimport android.view.View;\nimport android.view.animation.LinearInterpolator;\n\nimport com.kunfei.bookshelf.R;\n\n/**\n * RotateLoading\n * Created by Victor on 2015/4/28.\n */\npublic class RotateLoading extends View {\n\n    private static final int DEFAULT_WIDTH = 6;\n    private static final int DEFAULT_SHADOW_POSITION = 2;\n    private static final int DEFAULT_SPEED_OF_DEGREE = 10;\n\n    private Paint mPaint;\n\n    private RectF loadingRectF;\n    private RectF shadowRectF;\n\n    private int topDegree = 10;\n    private int bottomDegree = 190;\n\n    private float arc;\n\n    private int width;\n\n    private boolean changeBigger = true;\n\n    private int shadowPosition;\n\n    private boolean isStart = false;\n\n    private int color;\n\n    private int speedOfDegree;\n\n    private float speedOfArc;\n\n    public RotateLoading(Context context) {\n        super(context);\n        initView(context, null);\n    }\n\n    public RotateLoading(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        initView(context, attrs);\n    }\n\n    public RotateLoading(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        initView(context, attrs);\n    }\n\n    private void initView(Context context, AttributeSet attrs) {\n        color = Color.WHITE;\n        width = dpToPx(context, DEFAULT_WIDTH);\n        shadowPosition = dpToPx(getContext(), DEFAULT_SHADOW_POSITION);\n        speedOfDegree = DEFAULT_SPEED_OF_DEGREE;\n\n        if (null != attrs) {\n            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RotateLoading);\n            color = typedArray.getColor(R.styleable.RotateLoading_loading_color, Color.WHITE);\n            width = typedArray.getDimensionPixelSize(R.styleable.RotateLoading_loading_width, dpToPx(context, DEFAULT_WIDTH));\n            shadowPosition = typedArray.getInt(R.styleable.RotateLoading_shadow_position, DEFAULT_SHADOW_POSITION);\n            speedOfDegree = typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE);\n            typedArray.recycle();\n        }\n        speedOfArc = speedOfDegree / 4f;\n        mPaint = new Paint();\n        mPaint.setColor(color);\n        mPaint.setAntiAlias(true);\n        mPaint.setStyle(Paint.Style.STROKE);\n        mPaint.setStrokeWidth(width);\n        mPaint.setStrokeCap(Paint.Cap.ROUND);\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n        super.onSizeChanged(w, h, oldw, oldh);\n\n        arc = 10;\n\n        loadingRectF = new RectF(2 * width, 2 * width, w - 2 * width, h - 2 * width);\n        shadowRectF = new RectF(2 * width + shadowPosition, 2 * width + shadowPosition, w - 2 * width + shadowPosition, h - 2 * width + shadowPosition);\n    }\n\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n\n        if (!isStart) {\n            return;\n        }\n\n        mPaint.setColor(Color.parseColor(\"#1a000000\"));\n        canvas.drawArc(shadowRectF, topDegree, arc, false, mPaint);\n        canvas.drawArc(shadowRectF, bottomDegree, arc, false, mPaint);\n\n        mPaint.setColor(color);\n        canvas.drawArc(loadingRectF, topDegree, arc, false, mPaint);\n        canvas.drawArc(loadingRectF, bottomDegree, arc, false, mPaint);\n\n        topDegree += speedOfDegree;\n        bottomDegree += speedOfDegree;\n        if (topDegree > 360) {\n            topDegree = topDegree - 360;\n        }\n        if (bottomDegree > 360) {\n            bottomDegree = bottomDegree - 360;\n        }\n\n        if (changeBigger) {\n            if (arc < 160) {\n                arc += speedOfArc;\n                invalidate();\n            }\n        } else {\n            if (arc > speedOfDegree) {\n                arc -= 2 * speedOfArc;\n                invalidate();\n            }\n        }\n        if (arc >= 160 || arc <= 10) {\n            changeBigger = !changeBigger;\n            invalidate();\n        }\n    }\n\n    public void setLoadingColor(int color) {\n        this.color = color;\n    }\n\n    public int getLoadingColor() {\n        return color;\n    }\n\n    public void start() {\n        startAnimator();\n        isStart = true;\n        invalidate();\n    }\n\n    public void stop() {\n        stopAnimator();\n        invalidate();\n    }\n\n    public boolean isStart() {\n        return isStart;\n    }\n\n    private void startAnimator() {\n        ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(this, \"scaleX\", 0.0f, 1);\n        ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(this, \"scaleY\", 0.0f, 1);\n        scaleXAnimator.setDuration(300);\n        scaleXAnimator.setInterpolator(new LinearInterpolator());\n        scaleYAnimator.setDuration(300);\n        scaleYAnimator.setInterpolator(new LinearInterpolator());\n        AnimatorSet animatorSet = new AnimatorSet();\n        animatorSet.playTogether(scaleXAnimator, scaleYAnimator);\n        animatorSet.start();\n    }\n\n    private void stopAnimator() {\n        ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(this, \"scaleX\", 1, 0);\n        ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(this, \"scaleY\", 1, 0);\n        scaleXAnimator.setDuration(300);\n        scaleXAnimator.setInterpolator(new LinearInterpolator());\n        scaleYAnimator.setDuration(300);\n        scaleYAnimator.setInterpolator(new LinearInterpolator());\n        AnimatorSet animatorSet = new AnimatorSet();\n        animatorSet.playTogether(scaleXAnimator, scaleYAnimator);\n        animatorSet.addListener(new Animator.AnimatorListener() {\n            @Override\n            public void onAnimationStart(Animator animation) {\n\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                isStart = false;\n            }\n\n            @Override\n            public void onAnimationCancel(Animator animation) {\n\n            }\n\n            @Override\n            public void onAnimationRepeat(Animator animation) {\n\n            }\n        });\n        animatorSet.start();\n    }\n\n\n    public int dpToPx(Context context, float dpVal) {\n        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.getResources().getDisplayMetrics());\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/ScrollTextView.java",
    "content": "package com.kunfei.bookshelf.widget;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.text.Layout;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\npublic class ScrollTextView extends AppCompatTextView {\n\n    //滑动距离的最大边界\n    private int mOffsetHeight;\n\n    //是否到顶或者到底的标志\n    private boolean mBottomFlag = false;\n\n\n    public ScrollTextView(Context context) {\n        super(context);\n    }\n\n    public ScrollTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        initOffsetHeight();\n    }\n\n    @Override\n    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {\n        super.onTextChanged(text, start, lengthBefore, lengthAfter);\n        initOffsetHeight();\n    }\n\n    private void initOffsetHeight() {\n        int paddingTop;\n        int paddingBottom;\n        int mHeight;\n        int mLayoutHeight;\n\n        //获得内容面板\n        Layout mLayout = getLayout();\n        if (mLayout == null) return;\n        //获得内容面板的高度\n        mLayoutHeight = mLayout.getHeight();\n        //获取上内边距\n        paddingTop = getTotalPaddingTop();\n        //获取下内边距\n        paddingBottom = getTotalPaddingBottom();\n\n        //获得控件的实际高度\n        mHeight = getMeasuredHeight();\n\n        //计算滑动距离的边界\n        mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight;\n        if (mOffsetHeight <= 0) {\n            scrollTo(0, 0);\n        }\n    }\n\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent event) {\n        if (event.getAction() == MotionEvent.ACTION_DOWN) {\n            //如果是新的按下事件，则对mBottomFlag重新初始化\n            mBottomFlag = mOffsetHeight <= 0;\n        }\n        //如果已经不要这次事件，则传出取消的信号，这里的作用不大\n        if (mBottomFlag) {\n            event.setAction(MotionEvent.ACTION_CANCEL);\n        }\n        return super.dispatchTouchEvent(event);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        boolean result = super.onTouchEvent(event);\n        //如果是需要拦截，则再拦截，这个方法会在onScrollChanged方法之后再调用一次\n        if (!mBottomFlag)\n            getParent().requestDisallowInterceptTouchEvent(true);\n        return result;\n    }\n\n    @Override\n    protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {\n        super.onScrollChanged(horiz, vert, oldHoriz, oldVert);\n        if (vert == mOffsetHeight || vert == 0) {\n            //这里触发父布局或祖父布局的滑动事件\n            getParent().requestDisallowInterceptTouchEvent(false);\n            mBottomFlag = true;\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/check_box/SmoothCheckBox.java",
    "content": "package com.kunfei.bookshelf.widget.check_box;\n\nimport android.animation.ValueAnimator;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.Point;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.animation.LinearInterpolator;\nimport android.widget.Checkable;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.DensityUtil;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\npublic class SmoothCheckBox extends View implements Checkable {\n\n    private static final int DEF_DRAW_SIZE = 25;\n    private static final int DEF_ANIM_DURATION = 300;\n\n    private Paint mPaint, mTickPaint, mFloorPaint;\n    private Point[] mTickPoints;\n    private Point mCenterPoint;\n    private Path mTickPath;\n\n\n    private float mLeftLineDistance, mRightLineDistance, mDrewDistance;\n    private float mScaleVal = 1.0f, mFloorScale = 1.0f;\n    private int mWidth, mAnimDuration, mStrokeWidth;\n    private int mCheckedColor, mUnCheckedColor, mFloorColor, mFloorUnCheckedColor;\n\n    private boolean mChecked;\n    private boolean mTickDrawing;\n    private OnCheckedChangeListener mListener;\n\n    public SmoothCheckBox(Context context) {\n        this(context, null);\n    }\n\n    public SmoothCheckBox(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public SmoothCheckBox(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public SmoothCheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n        init(context, attrs);\n    }\n\n    private static int getGradientColor(int startColor, int endColor, float percent) {\n        int startA = Color.alpha(startColor);\n        int startR = Color.red(startColor);\n        int startG = Color.green(startColor);\n        int startB = Color.blue(startColor);\n\n        int endA = Color.alpha(endColor);\n        int endR = Color.red(endColor);\n        int endG = Color.green(endColor);\n        int endB = Color.blue(endColor);\n\n        int currentA = (int) (startA * (1 - percent) + endA * percent);\n        int currentR = (int) (startR * (1 - percent) + endR * percent);\n        int currentG = (int) (startG * (1 - percent) + endG * percent);\n        int currentB = (int) (startB * (1 - percent) + endB * percent);\n        return Color.argb(currentA, currentR, currentG, currentB);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n\n        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox);\n        int tickColor = ThemeStore.accentColor(context);\n        mCheckedColor = context.getResources().getColor(R.color.background_card);\n        mUnCheckedColor = context.getResources().getColor(R.color.background_menu);\n        mFloorColor = context.getResources().getColor(R.color.transparent30);\n        tickColor = ta.getColor(R.styleable.SmoothCheckBox_color_tick, tickColor);\n        mAnimDuration = ta.getInt(R.styleable.SmoothCheckBox_duration, DEF_ANIM_DURATION);\n        mFloorColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked_stroke, mFloorColor);\n        mCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_checked, mCheckedColor);\n        mUnCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked, mUnCheckedColor);\n        mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SmoothCheckBox_stroke_width, DensityUtil.dp2px(getContext(), 0));\n        ta.recycle();\n\n        mFloorUnCheckedColor = mFloorColor;\n        mTickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        mTickPaint.setStyle(Paint.Style.STROKE);\n        mTickPaint.setStrokeCap(Paint.Cap.ROUND);\n        mTickPaint.setColor(tickColor);\n\n        mFloorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        mFloorPaint.setStyle(Paint.Style.FILL);\n        mFloorPaint.setColor(mFloorColor);\n\n        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setColor(mCheckedColor);\n\n        mTickPath = new Path();\n        mCenterPoint = new Point();\n        mTickPoints = new Point[3];\n        mTickPoints[0] = new Point();\n        mTickPoints[1] = new Point();\n        mTickPoints[2] = new Point();\n\n        setOnClickListener(v -> {\n            toggle();\n            mTickDrawing = false;\n            mDrewDistance = 0;\n            if (isChecked()) {\n                startCheckedAnimation();\n            } else {\n                startUnCheckedAnimation();\n            }\n        });\n    }\n\n    @Override\n    public boolean isChecked() {\n        return mChecked;\n    }\n\n    @Override\n    public void setChecked(boolean checked) {\n        mChecked = checked;\n        reset();\n        invalidate();\n        if (mListener != null) {\n            mListener.onCheckedChanged(SmoothCheckBox.this, mChecked);\n        }\n    }\n\n    @Override\n    public void toggle() {\n        this.setChecked(!isChecked());\n    }\n\n    /**\n     * checked with animation\n     *\n     * @param checked checked\n     * @param animate change with animation\n     */\n    public void setChecked(boolean checked, boolean animate) {\n        if (animate) {\n            mTickDrawing = false;\n            mChecked = checked;\n            mDrewDistance = 0f;\n            if (checked) {\n                startCheckedAnimation();\n            } else {\n                startUnCheckedAnimation();\n            }\n            if (mListener != null) {\n                mListener.onCheckedChanged(SmoothCheckBox.this, mChecked);\n            }\n\n        } else {\n            this.setChecked(checked);\n        }\n    }\n\n    private void reset() {\n        mTickDrawing = true;\n        mFloorScale = 1.0f;\n        mScaleVal = isChecked() ? 0f : 1.0f;\n        mFloorColor = isChecked() ? mCheckedColor : mFloorUnCheckedColor;\n        mDrewDistance = isChecked() ? (mLeftLineDistance + mRightLineDistance) : 0;\n    }\n\n    private int measureSize(int measureSpec) {\n        int defSize = DensityUtil.dp2px(getContext(), DEF_DRAW_SIZE);\n        int specSize = MeasureSpec.getSize(measureSpec);\n        int specMode = MeasureSpec.getMode(measureSpec);\n\n        int result = 0;\n        switch (specMode) {\n            case MeasureSpec.UNSPECIFIED:\n            case MeasureSpec.AT_MOST:\n                result = Math.min(defSize, specSize);\n                break;\n            case MeasureSpec.EXACTLY:\n                result = specSize;\n                break;\n        }\n        return result;\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec));\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        mWidth = getMeasuredWidth();\n        mStrokeWidth = (mStrokeWidth == 0 ? getMeasuredWidth() / 10 : mStrokeWidth);\n        mStrokeWidth = mStrokeWidth > getMeasuredWidth() / 5 ? getMeasuredWidth() / 5 : mStrokeWidth;\n        mStrokeWidth = (mStrokeWidth < 3) ? 3 : mStrokeWidth;\n        mCenterPoint.x = mWidth / 2;\n        mCenterPoint.y = getMeasuredHeight() / 2;\n\n        mTickPoints[0].x = Math.round((float) getMeasuredWidth() / 30 * 7);\n        mTickPoints[0].y = Math.round((float) getMeasuredHeight() / 30 * 14);\n        mTickPoints[1].x = Math.round((float) getMeasuredWidth() / 30 * 13);\n        mTickPoints[1].y = Math.round((float) getMeasuredHeight() / 30 * 20);\n        mTickPoints[2].x = Math.round((float) getMeasuredWidth() / 30 * 22);\n        mTickPoints[2].y = Math.round((float) getMeasuredHeight() / 30 * 10);\n\n        mLeftLineDistance = (float) Math.sqrt(Math.pow(mTickPoints[1].x - mTickPoints[0].x, 2) +\n                Math.pow(mTickPoints[1].y - mTickPoints[0].y, 2));\n        mRightLineDistance = (float) Math.sqrt(Math.pow(mTickPoints[2].x - mTickPoints[1].x, 2) +\n                Math.pow(mTickPoints[2].y - mTickPoints[1].y, 2));\n        mTickPaint.setStrokeWidth(mStrokeWidth);\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        drawBorder(canvas);\n        drawCenter(canvas);\n        drawTick(canvas);\n    }\n\n    private void drawCenter(Canvas canvas) {\n        mPaint.setColor(mUnCheckedColor);\n        float radius = (mCenterPoint.x - mStrokeWidth) * mScaleVal;\n        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, radius, mPaint);\n    }\n\n    private void drawBorder(Canvas canvas) {\n        mFloorPaint.setColor(mFloorColor);\n        int radius = mCenterPoint.x;\n        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, radius * mFloorScale, mFloorPaint);\n    }\n\n    private void drawTick(Canvas canvas) {\n        if (mTickDrawing && isChecked()) {\n            drawTickPath(canvas);\n        }\n    }\n\n    private void drawTickPath(Canvas canvas) {\n        mTickPath.reset();\n        // draw left of the tick\n        if (mDrewDistance < mLeftLineDistance) {\n            float step = (mWidth / 20.0f) < 3 ? 3 : (mWidth / 20.0f);\n            mDrewDistance += step;\n            float stopX = mTickPoints[0].x + (mTickPoints[1].x - mTickPoints[0].x) * mDrewDistance / mLeftLineDistance;\n            float stopY = mTickPoints[0].y + (mTickPoints[1].y - mTickPoints[0].y) * mDrewDistance / mLeftLineDistance;\n\n            mTickPath.moveTo(mTickPoints[0].x, mTickPoints[0].y);\n            mTickPath.lineTo(stopX, stopY);\n            canvas.drawPath(mTickPath, mTickPaint);\n\n            if (mDrewDistance > mLeftLineDistance) {\n                mDrewDistance = mLeftLineDistance;\n            }\n        } else {\n\n            mTickPath.moveTo(mTickPoints[0].x, mTickPoints[0].y);\n            mTickPath.lineTo(mTickPoints[1].x, mTickPoints[1].y);\n            canvas.drawPath(mTickPath, mTickPaint);\n\n            // draw right of the tick\n            if (mDrewDistance < mLeftLineDistance + mRightLineDistance) {\n                float stopX = mTickPoints[1].x + (mTickPoints[2].x - mTickPoints[1].x) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance;\n                float stopY = mTickPoints[1].y - (mTickPoints[1].y - mTickPoints[2].y) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance;\n\n                mTickPath.reset();\n                mTickPath.moveTo(mTickPoints[1].x, mTickPoints[1].y);\n                mTickPath.lineTo(stopX, stopY);\n                canvas.drawPath(mTickPath, mTickPaint);\n\n                float step = (mWidth / 20f) < 3 ? 3 : (mWidth / 20f);\n                mDrewDistance += step;\n            } else {\n                mTickPath.reset();\n                mTickPath.moveTo(mTickPoints[1].x, mTickPoints[1].y);\n                mTickPath.lineTo(mTickPoints[2].x, mTickPoints[2].y);\n                canvas.drawPath(mTickPath, mTickPaint);\n            }\n        }\n\n        // invalidate\n        if (mDrewDistance < mLeftLineDistance + mRightLineDistance) {\n            postDelayed(this::postInvalidate, 10);\n        }\n    }\n\n    private void startCheckedAnimation() {\n        ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0f);\n        animator.setDuration(mAnimDuration / 3 * 2);\n        animator.setInterpolator(new LinearInterpolator());\n        animator.addUpdateListener(animation -> {\n            mScaleVal = (float) animation.getAnimatedValue();\n            mFloorColor = getGradientColor(mUnCheckedColor, mCheckedColor, 1 - mScaleVal);\n            postInvalidate();\n        });\n        animator.start();\n\n        ValueAnimator floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f);\n        floorAnimator.setDuration(mAnimDuration);\n        floorAnimator.setInterpolator(new LinearInterpolator());\n        floorAnimator.addUpdateListener(animation -> {\n            mFloorScale = (float) animation.getAnimatedValue();\n            postInvalidate();\n        });\n        floorAnimator.start();\n\n        drawTickDelayed();\n    }\n\n    private void startUnCheckedAnimation() {\n        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);\n        animator.setDuration(mAnimDuration);\n        animator.setInterpolator(new LinearInterpolator());\n        animator.addUpdateListener(animation -> {\n            mScaleVal = (float) animation.getAnimatedValue();\n            mFloorColor = getGradientColor(mCheckedColor, mFloorUnCheckedColor, mScaleVal);\n            postInvalidate();\n        });\n        animator.start();\n\n        ValueAnimator floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f);\n        floorAnimator.setDuration(mAnimDuration);\n        floorAnimator.setInterpolator(new LinearInterpolator());\n        floorAnimator.addUpdateListener(animation -> {\n            mFloorScale = (float) animation.getAnimatedValue();\n            postInvalidate();\n        });\n        floorAnimator.start();\n    }\n\n    private void drawTickDelayed() {\n        postDelayed(() -> {\n            mTickDrawing = true;\n            postInvalidate();\n        }, mAnimDuration);\n    }\n\n    public void setOnCheckedChangeListener(OnCheckedChangeListener l) {\n        this.mListener = l;\n    }\n\n    public interface OnCheckedChangeListener {\n        void onCheckedChanged(SmoothCheckBox checkBox, boolean isChecked);\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/ExplosionAnimator.java",
    "content": "/*\n * Copyright (C) 2015 tyrantgit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.kunfei.bookshelf.widget.explosion_field;\n\nimport android.animation.ValueAnimator;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.view.View;\nimport android.view.animation.AccelerateInterpolator;\nimport android.view.animation.Interpolator;\n\nimport java.util.Random;\n\npublic class ExplosionAnimator extends ValueAnimator {\n\n    static long DEFAULT_DURATION = 0x400;\n    private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(0.6f);\n    private static final float END_VALUE = 1.4f;\n    private static final float X = Utils.dp2Px(5);\n    private static final float Y = Utils.dp2Px(20);\n    private static final float V = Utils.dp2Px(2);\n    private static final float W = Utils.dp2Px(1);\n    private Paint mPaint;\n    private Particle[] mParticles;\n    private Rect mBound;\n    private View mContainer;\n\n    public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) {\n        mPaint = new Paint();\n        mBound = new Rect(bound);\n        int partLen = 15;\n        mParticles = new Particle[partLen * partLen];\n        Random random = new Random(System.currentTimeMillis());\n        int w = bitmap.getWidth() / (partLen + 2);\n        int h = bitmap.getHeight() / (partLen + 2);\n        for (int i = 0; i < partLen; i++) {\n            for (int j = 0; j < partLen; j++) {\n                mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random);\n            }\n        }\n        mContainer = container;\n        setFloatValues(0f, END_VALUE);\n        setInterpolator(DEFAULT_INTERPOLATOR);\n        setDuration(DEFAULT_DURATION);\n    }\n\n    private Particle generateParticle(int color, Random random) {\n        Particle particle = new Particle();\n        particle.color = color;\n        particle.radius = V;\n        if (random.nextFloat() < 0.2f) {\n            particle.baseRadius = V + ((X - V) * random.nextFloat());\n        } else {\n            particle.baseRadius = W + ((V - W) * random.nextFloat());\n        }\n        float nextFloat = random.nextFloat();\n        particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f);\n        particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat());\n        particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f;\n        float f = nextFloat < 0.2f ? particle.bottom : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f;\n        particle.bottom = f;\n        particle.mag = 4.0f * particle.top / particle.bottom;\n        particle.neg = (-particle.mag) / particle.bottom;\n        f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f));\n        particle.baseCx = f;\n        particle.cx = f;\n        f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f));\n        particle.baseCy = f;\n        particle.cy = f;\n        particle.life = END_VALUE / 10 * random.nextFloat();\n        particle.overflow = 0.4f * random.nextFloat();\n        particle.alpha = 1f;\n        return particle;\n    }\n\n    public boolean draw(Canvas canvas) {\n        if (!isStarted()) {\n            return false;\n        }\n        for (Particle particle : mParticles) {\n            particle.advance((float) getAnimatedValue());\n            if (particle.alpha > 0f) {\n                mPaint.setColor(particle.color);\n                mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha));\n                canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint);\n            }\n        }\n        mContainer.invalidate();\n        return true;\n    }\n\n    @Override\n    public void start() {\n        super.start();\n        mContainer.invalidate(mBound);\n    }\n\n    private class Particle {\n        float alpha;\n        int color;\n        float cx;\n        float cy;\n        float radius;\n        float baseCx;\n        float baseCy;\n        float baseRadius;\n        float top;\n        float bottom;\n        float mag;\n        float neg;\n        float life;\n        float overflow;\n\n\n        public void advance(float factor) {\n            float f = 0f;\n            float normalization = factor / END_VALUE;\n            if (normalization < life || normalization > 1f - overflow) {\n                alpha = 0f;\n                return;\n            }\n            normalization = (normalization - life) / (1f - life - overflow);\n            float f2 = normalization * END_VALUE;\n            if (normalization >= 0.7f) {\n                f = (normalization - 0.7f) / 0.3f;\n            }\n            alpha = 1f - f;\n            f = bottom * f2;\n            cx = baseCx + f;\n            cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;\n            radius = V + (baseRadius - V) * f2;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/ExplosionField.java",
    "content": "/*\n * Copyright (C) 2015 tyrantgit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.kunfei.bookshelf.widget.explosion_field;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ValueAnimator;\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.media.MediaPlayer;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.Window;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Random;\n\n\npublic class ExplosionField extends View {\n\n    private long customDuration = ExplosionAnimator.DEFAULT_DURATION;\n    private int idPlayAnimationEffect = 0;\n    private OnAnimatorListener mZAnimatorListener;\n    private OnClickListener mOnClickListener;\n\n    private List<ExplosionAnimator> mExplosions = new ArrayList<>();\n    private int[] mExpandInset = new int[2];\n\n    public ExplosionField(Context context) {\n        super(context);\n        init();\n    }\n\n    public ExplosionField(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init();\n    }\n\n    public ExplosionField(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init();\n    }\n\n    private void init() {\n\n        Arrays.fill(mExpandInset, Utils.dp2Px(32));\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n        for (ExplosionAnimator explosion : mExplosions) {\n            explosion.draw(canvas);\n        }\n    }\n\n    public void playSoundAnimationEffect(int id) {\n        this.idPlayAnimationEffect = id;\n    }\n\n    public void setCustomDuration(long customDuration) {\n        this.customDuration = customDuration;\n    }\n\n    public void addActionEvent(OnAnimatorListener ievents) {\n        this.mZAnimatorListener = ievents;\n    }\n\n\n    public void expandExplosionBound(int dx, int dy) {\n        mExpandInset[0] = dx;\n        mExpandInset[1] = dy;\n    }\n\n    public void explode(Bitmap bitmap, Rect bound, long startDelay) {\n        explode(bitmap, bound, startDelay, null);\n    }\n\n    public void explode(Bitmap bitmap, Rect bound, long startDelay, final View view) {\n        long currentDuration = customDuration;\n        final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound);\n        explosion.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mExplosions.remove(animation);\n                if (view != null) {\n                    view.setScaleX(1);\n                    view.setScaleY(1);\n                    view.setAlpha(1);\n                    view.setOnClickListener(mOnClickListener);//set event\n\n                }\n            }\n        });\n        explosion.setStartDelay(startDelay);\n        explosion.setDuration(currentDuration);\n        mExplosions.add(explosion);\n        explosion.start();\n    }\n\n    public void explode(View view) {\n        explode(view, false);\n    }\n\n    public void explode(final View view, Boolean restartState) {\n\n        Rect r = new Rect();\n        view.getGlobalVisibleRect(r);\n        int[] location = new int[2];\n        getLocationOnScreen(location);\n//        getLocationInWindow(location);\n//        view.getLocationInWindow(location);\n        r.offset(-location[0], -location[1]);\n        r.inset(-mExpandInset[0], -mExpandInset[1]);\n        int startDelay = 100;\n        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150);\n        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n\n            Random random = new Random();\n\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f);\n                view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f);\n            }\n        });\n\n        animator.addListener(new Animator.AnimatorListener() {\n            @Override\n            public void onAnimationStart(Animator animator) {\n                if (idPlayAnimationEffect != 0)\n                    MediaPlayer.create(getContext(), idPlayAnimationEffect).start();\n            }\n\n            @Override\n            public void onAnimationEnd(Animator animator) {\n                if (mZAnimatorListener != null) {\n                    mZAnimatorListener.onAnimationEnd(animator, ExplosionField.this);\n                }\n            }\n\n            @Override\n            public void onAnimationCancel(Animator animator) {\n                Log.i(\"PRUEBA\", \"CANCEL\");\n            }\n\n            @Override\n            public void onAnimationRepeat(Animator animator) {\n                Log.i(\"PRUEBA\", \"REPEAT\");\n            }\n        });\n\n        animator.start();\n        view.animate().setDuration(150).setStartDelay(startDelay).scaleX(0f).scaleY(0f).alpha(0f).start();\n        if (restartState)\n            explode(Utils.createBitmapFromView(view), r, startDelay, view);\n        else\n            explode(Utils.createBitmapFromView(view), r, startDelay);\n\n    }\n\n    public void clear() {\n        mExplosions.clear();\n        invalidate();\n    }\n\n    public static ExplosionField attach2Window(Activity activity) {\n        ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);\n        ExplosionField explosionField = new ExplosionField(activity);\n        rootView.addView(explosionField, new ViewGroup.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));\n        return explosionField;\n    }\n\n    public void setOnClickListener(OnClickListener mOnClickListener) {\n        this.mOnClickListener = mOnClickListener;\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/OnAnimatorListener.java",
    "content": "package com.kunfei.bookshelf.widget.explosion_field;\n\nimport android.animation.Animator;\nimport android.view.View;\n\npublic interface OnAnimatorListener {\n    void onAnimationEnd(Animator animator, View view);\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/explosion_field/Utils.java",
    "content": "/*\n * Copyright (C) 2015 tyrantgit\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.kunfei.bookshelf.widget.explosion_field;\n\n\nimport android.content.res.Resources;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.view.View;\nimport android.widget.ImageView;\n\npublic class Utils {\n\n    private Utils() {\n    }\n\n    private static final float DENSITY = Resources.getSystem().getDisplayMetrics().density;\n    private static final Canvas sCanvas = new Canvas();\n\n    public static int dp2Px(int dp) {\n        return Math.round(dp * DENSITY);\n    }\n\n    public static Bitmap createBitmapFromView(View view) {\n        if (view instanceof ImageView) {\n            Drawable drawable = ((ImageView) view).getDrawable();\n            if (drawable != null && drawable instanceof BitmapDrawable) {\n                return ((BitmapDrawable) drawable).getBitmap();\n            }\n        }\n        view.clearFocus();\n        Bitmap bitmap = createBitmapSafely(view.getWidth(),\n                view.getHeight(), Bitmap.Config.ARGB_8888, 1);\n        if (bitmap != null) {\n            synchronized (sCanvas) {\n                Canvas canvas = sCanvas;\n                canvas.setBitmap(bitmap);\n                view.draw(canvas);\n                canvas.setBitmap(null);\n            }\n        }\n        return bitmap;\n    }\n\n    public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) {\n        try {\n            return Bitmap.createBitmap(width, height, config);\n        } catch (OutOfMemoryError e) {\n            e.printStackTrace();\n            if (retryCount > 0) {\n                System.gc();\n                return createBitmapSafely(width, height, config, retryCount - 1);\n            }\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/adapter/FileAdapter.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.adapter;\n\nimport android.graphics.drawable.Drawable;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.widget.filepicker.entity.FileItem;\nimport com.kunfei.bookshelf.widget.filepicker.icons.FilePickerIcon;\nimport com.kunfei.bookshelf.widget.filepicker.util.ConvertUtils;\nimport com.kunfei.bookshelf.widget.filepicker.util.FileUtils;\n\nimport java.io.File;\nimport java.util.ArrayList;\n\n\npublic class FileAdapter extends RecyclerView.Adapter<FileAdapter.MyViewHolder> {\n    public static final String DIR_ROOT = \".\";\n    public static final String DIR_PARENT = \"..\";\n    private ArrayList<FileItem> data = new ArrayList<>();\n    private String rootPath = null;\n    private String currentPath = null;\n    private String[] allowExtensions = null;//允许的扩展名\n    private boolean onlyListDir = false;//是否仅仅读取目录\n    private boolean showHomeDir = false;//是否显示返回主目录\n    private boolean showUpDir = true;//是否显示返回上一级\n    private boolean showHideDir = true;//是否显示隐藏的目录（以“.”开头）\n    private int itemHeight = 40;// dp\n    private Drawable homeIcon = null;\n    private Drawable upIcon = null;\n    private Drawable folderIcon = null;\n    private Drawable fileIcon = null;\n    private CallBack callBack;\n\n    public void setCallBack(CallBack callBack) {\n        this.callBack = callBack;\n    }\n\n    public FileItem getItem(int pos) {\n        return data.get(pos);\n    }\n\n    public String getCurrentPath() {\n        return currentPath;\n    }\n\n    public void setFileIcon(Drawable fileIcon) {\n        this.fileIcon = fileIcon;\n    }\n\n    public void setFolderIcon(Drawable folderIcon) {\n        this.folderIcon = folderIcon;\n    }\n\n    public void setHomeIcon(Drawable homeIcon) {\n        this.homeIcon = homeIcon;\n    }\n\n    public void setUpIcon(Drawable upIcon) {\n        this.upIcon = upIcon;\n    }\n\n    /**\n     * 允许的扩展名\n     */\n    public void setAllowExtensions(String[] allowExtensions) {\n        this.allowExtensions = allowExtensions;\n    }\n\n    /**\n     * 是否仅仅读取目录\n     */\n    public void setOnlyListDir(boolean onlyListDir) {\n        this.onlyListDir = onlyListDir;\n    }\n\n    public boolean isOnlyListDir() {\n        return onlyListDir;\n    }\n\n    /**\n     * 是否显示返回主目录\n     */\n    public void setShowHomeDir(boolean showHomeDir) {\n        this.showHomeDir = showHomeDir;\n    }\n\n    public boolean isShowHomeDir() {\n        return showHomeDir;\n    }\n\n    /**\n     * 是否显示返回上一级\n     */\n    public void setShowUpDir(boolean showUpDir) {\n        this.showUpDir = showUpDir;\n    }\n\n    public boolean isShowUpDir() {\n        return showUpDir;\n    }\n\n    /**\n     * 是否显示隐藏的目录（以“.”开头）\n     */\n    public void setShowHideDir(boolean showHideDir) {\n        this.showHideDir = showHideDir;\n    }\n\n    public boolean isShowHideDir() {\n        return showHideDir;\n    }\n\n    public void setItemHeight(int itemHeight) {\n        this.itemHeight = itemHeight;\n    }\n\n    public void loadData(String path) {\n        if (path == null) {\n            return;\n        }\n        if (homeIcon == null) {\n            homeIcon = ConvertUtils.toDrawable(FilePickerIcon.getHOME());\n        }\n        if (upIcon == null) {\n            upIcon = ConvertUtils.toDrawable(FilePickerIcon.getUPDIR());\n        }\n        if (folderIcon == null) {\n            folderIcon = ConvertUtils.toDrawable(FilePickerIcon.getFOLDER());\n        }\n        if (fileIcon == null) {\n            fileIcon = ConvertUtils.toDrawable(FilePickerIcon.getFILE());\n        }\n        ArrayList<FileItem> datas = new ArrayList<FileItem>();\n        if (rootPath == null) {\n            rootPath = path;\n        }\n        currentPath = path;\n        if (showHomeDir) {\n            //添加“返回主目录”\n            FileItem fileRoot = new FileItem();\n            fileRoot.setDirectory(true);\n            fileRoot.setIcon(homeIcon);\n            fileRoot.setName(DIR_ROOT);\n            fileRoot.setSize(0);\n            fileRoot.setPath(rootPath);\n            datas.add(fileRoot);\n        }\n        if (showUpDir && !path.equals(\"/\")) {\n            //添加“返回上一级目录”\n            FileItem fileParent = new FileItem();\n            fileParent.setDirectory(true);\n            fileParent.setIcon(upIcon);\n            fileParent.setName(DIR_PARENT);\n            fileParent.setSize(0);\n            fileParent.setPath(new File(path).getParent());\n            datas.add(fileParent);\n        }\n        File[] files;\n        if (allowExtensions == null) {\n            if (onlyListDir) {\n                files = FileUtils.listDirs(currentPath);\n            } else {\n                files = FileUtils.listDirsAndFiles(currentPath);\n            }\n        } else {\n            if (onlyListDir) {\n                files = FileUtils.listDirs(currentPath, allowExtensions);\n            } else {\n                files = FileUtils.listDirsAndFiles(currentPath, allowExtensions);\n            }\n        }\n        if (files != null) {\n            for (File file : files) {\n                if (!showHideDir && file.getName().startsWith(\".\")) {\n                    continue;\n                }\n                FileItem fileItem = new FileItem();\n                boolean isDirectory = file.isDirectory();\n                fileItem.setDirectory(isDirectory);\n                if (isDirectory) {\n                    fileItem.setIcon(folderIcon);\n                    fileItem.setSize(0);\n                } else {\n                    fileItem.setIcon(fileIcon);\n                    fileItem.setSize(file.length());\n                }\n                fileItem.setName(file.getName());\n                fileItem.setPath(file.getAbsolutePath());\n                datas.add(fileItem);\n            }\n        }\n        data.clear();\n        data.addAll(datas);\n        notifyDataSetChanged();\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_file_filepicker, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {\n        FileItem fileItem = data.get(position);\n        holder.imageView.setImageDrawable(fileItem.getIcon());\n        holder.textView.setText(fileItem.getName());\n        holder.itemView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                if (callBack != null) {\n                    callBack.onFileClick(position);\n                }\n            }\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        return data.size();\n    }\n\n    class MyViewHolder extends RecyclerView.ViewHolder {\n        ImageView imageView;\n        TextView textView;\n\n        MyViewHolder(@NonNull View itemView) {\n            super(itemView);\n            imageView = itemView.findViewById(R.id.image_view);\n            textView = itemView.findViewById(R.id.text_view);\n        }\n    }\n\n    public interface CallBack {\n        void onFileClick(int position);\n    }\n\n}\n\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/adapter/PathAdapter.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.adapter;\n\nimport android.graphics.drawable.Drawable;\nimport android.os.Environment;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.widget.filepicker.icons.FilePickerIcon;\nimport com.kunfei.bookshelf.widget.filepicker.util.ConvertUtils;\n\nimport java.util.Collections;\nimport java.util.LinkedList;\n\n\npublic class PathAdapter extends RecyclerView.Adapter<PathAdapter.MyViewHolder> {\n    private static final String ROOT_HINT = \"SD\";\n    private LinkedList<String> paths = new LinkedList<>();\n    private Drawable arrowIcon = null;\n    private String sdCardDirectory = Environment.getExternalStorageDirectory().getAbsolutePath();\n    private CallBack callBack;\n\n    public void setCallBack(CallBack callBack) {\n        this.callBack = callBack;\n    }\n\n    public String getItem(int position) {\n        StringBuilder tmp = new StringBuilder(sdCardDirectory + \"/\");\n        //忽略根目录\n        if (position == 0) {\n            return tmp.toString();\n        }\n        for (int i = 1; i <= position; i++) {\n            tmp.append(paths.get(i)).append(\"/\");\n        }\n        return tmp.toString();\n    }\n\n    public void setArrowIcon(Drawable arrowIcon) {\n        this.arrowIcon = arrowIcon;\n    }\n\n    public void updatePath(String path) {\n        path = path.replace(sdCardDirectory, \"\");\n        if (arrowIcon == null) {\n            arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getARROW());\n        }\n        paths.clear();\n        if (!path.equals(\"/\") && !path.equals(\"\")) {\n            String[] tmps = path.substring(path.indexOf(\"/\") + 1).split(\"/\");\n            Collections.addAll(paths, tmps);\n        }\n        paths.addFirst(ROOT_HINT);\n        notifyDataSetChanged();\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_path_filepicker, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {\n        holder.textView.setText(paths.get(position));\n        holder.imageView.setImageDrawable(arrowIcon);\n        holder.itemView.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                if (callBack != null) {\n                    callBack.onPathClick(position);\n                }\n            }\n        });\n    }\n\n    @Override\n    public int getItemCount() {\n        return paths.size();\n    }\n\n    class MyViewHolder extends RecyclerView.ViewHolder {\n        ImageView imageView;\n        TextView textView;\n\n        MyViewHolder(@NonNull View itemView) {\n            super(itemView);\n            imageView = itemView.findViewById(R.id.image_view);\n            textView = itemView.findViewById(R.id.text_view);\n        }\n    }\n\n    public interface CallBack {\n        void onPathClick(int position);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/drawable/StateBaseDrawable.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.drawable;\n\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.StateListDrawable;\n\n/**\n * 按下状态与普通状态下显示不同的图片或颜色\n * <br />\n * Author:李玉江[QQ:1032694760]\n * DateTime:2017/01/01 05:30\n * Builder:Android Studio\n */\npublic abstract class StateBaseDrawable extends StateListDrawable {\n\n    protected void addState(Drawable pressed) {\n        addState(new ColorDrawable(Color.TRANSPARENT), pressed);\n    }\n\n    protected void addState(Drawable normal, Drawable pressed) {\n        addState(new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled}, pressed);\n        addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_focused}, pressed);\n        addState(new int[]{android.R.attr.state_enabled}, normal);\n        addState(new int[]{android.R.attr.state_focused}, pressed);\n        addState(new int[]{android.R.attr.state_window_focused}, normal);\n        addState(new int[]{}, normal);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/drawable/StateColorDrawable.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.drawable;\n\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\n\nimport androidx.annotation.ColorInt;\n\n/**\n * 按下状态与普通状态下显示不同的颜色\n * <br />\n * Author:李玉江[QQ:1032694760]\n * DateTime:2017/01/01 05:30\n * Builder:Android Studio\n */\npublic class StateColorDrawable extends StateBaseDrawable {\n\n    public StateColorDrawable(@ColorInt int pressedColor) {\n        this(Color.TRANSPARENT, pressedColor);\n    }\n\n    public StateColorDrawable(@ColorInt int normalColor, @ColorInt int pressedColor) {\n        addState(new ColorDrawable(normalColor), new ColorDrawable(pressedColor));\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/entity/FileItem.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.entity;\n\nimport android.graphics.drawable.Drawable;\n\n/**\n * 文件项信息\n *\n * @author 李玉江[QQ:1032694760]\n * @since 2014-05-23 18:02\n */\npublic class FileItem extends JavaBean {\n    private Drawable icon;\n    private String name;\n    private String path = \"/\";\n    private long size = 0;\n    private boolean isDirectory = false;\n\n    public void setIcon(Drawable icon) {\n        this.icon = icon;\n    }\n\n    public Drawable getIcon() {\n        return icon;\n    }\n\n    public String getName() {\n        return name;\n    }\n\n    public void setName(String name) {\n        this.name = name;\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public void setPath(String path) {\n        this.path = path;\n    }\n\n    public long getSize() {\n        return size;\n    }\n\n    public void setSize(long size) {\n        this.size = size;\n    }\n\n    public boolean isDirectory() {\n        return isDirectory;\n    }\n\n    public void setDirectory(boolean isDirectory) {\n        this.isDirectory = isDirectory;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/entity/JavaBean.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.entity;\n\nimport java.io.Serializable;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\n/**\n * JavaBean类\n *\n * @author 李玉江[QQ:1032694760]\n * @since 2014-04-23 16:14\n */\npublic class JavaBean implements Serializable {\n    private static final long serialVersionUID = -6111323241670458039L;\n\n    /**\n     * 反射出所有字段值\n     */\n    @Override\n    public String toString() {\n        ArrayList<Field> list = new ArrayList<>();\n        Class<?> clazz = getClass();\n        list.addAll(Arrays.asList(clazz.getDeclaredFields()));//得到自身的所有字段\n        StringBuilder sb = new StringBuilder();\n        while (clazz != Object.class) {\n            clazz = clazz.getSuperclass();//得到继承自父类的字段\n            Field[] fields = clazz.getDeclaredFields();\n            for (Field field : fields) {\n                int modifier = field.getModifiers();\n                if (Modifier.isPublic(modifier) || Modifier.isProtected(modifier)) {\n                    list.add(field);\n                }\n            }\n        }\n        Field[] fields = list.toArray(new Field[0]);\n        for (Field field : fields) {\n            String fieldName = field.getName();\n            try {\n                Object obj = field.get(this);\n                sb.append(fieldName);\n                sb.append(\"=\");\n                sb.append(obj);\n                sb.append(\"\\n\");\n            } catch (IllegalAccessException ignored) {\n            }\n        }\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/icons/FilePickerIcon.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.icons;\n\n/**\n * Generated by https://github.com/gzu-liyujiang/Image2ByteVar\n *\n * @author 李玉江[QQ:1023694760]\n * @since 2017/01/04 06:03\n */\npublic class FilePickerIcon {\n\n    public static byte[] getFILE() {\n        return FILE;\n    }\n\n    public static byte[] getFOLDER() {\n        return FOLDER;\n    }\n\n    public static byte[] getHOME() {\n        return HOME;\n    }\n\n    public static byte[] getUPDIR() {\n        return UPDIR;\n    }\n\n    public static byte[] getARROW() {\n        return ARROW;\n    }\n\n    // fixed: 17-1-7 \"static final\" arrays should be \"private\"\n    private static final byte[] FILE = {\n            -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,\n            0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71,\n            68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0,\n            0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 1, -73, 73, 68, 65, 84,\n            88, -123, -19, -106, 63, 75, 3, 49, 24, -121, 127, -74, 69, -37, 110, -30, -94, -109, -120, -126,\n            -125, -101, 56, 72, 63, -125, -97, -64, -63, 77, 28, 69, 20, 81, 68, 113, 83, 113, -12, 91,\n            -120, -96, -117, -120, -85, 31, 65, 29, 28, -100, -124, -94, -101, 46, -83, -105, 92, 114, 113, -24,\n            31, -38, -110, 59, -34, -9, 114, 87, 69, -14, 64, 41, -92, 73, -34, -89, -55, -17, 114, 1,\n            60, 30, -113, -25, 79, 50, 66, -19, 120, -7, 16, 26, 0, -112, 82, -61, 24, 3, 41, 35,\n            0, -128, 20, 10, 74, 69, -48, 58, -126, 82, -90, -37, -90, -75, -127, 82, 81, 119, 124, 16,\n            40, -100, 109, 79, -109, -21, 13, 82, -94, 118, -108, 82, 99, -91, 54, -58, 25, 2, 0, 120,\n            -85, 7, -72, -71, 127, -57, -18, -58, 12, -102, -51, 87, 115, 113, 56, -105, 74, -74, 64, -19,\n            104, -116, 73, 51, 63, 0, -96, 88, 104, -107, 57, -34, -102, -59, -6, -63, 75, -86, -119, -56,\n            -94, -99, -83, -26, -96, 123, -122, 20, -37, -107, 78, -9, -25, -79, -74, -13, -52, -106, 37, -117,\n            114, -23, 72, 42, 109, -96, -93, 8, 66, 0, -97, 95, -83, -49, -55, -34, 2, 86, 55, 31,\n            89, -78, -12, -116, 10, -59, 18, -20, 48, 90, -86, -96, -47, -48, -72, -66, -5, 64, -40, -98,\n            -94, 90, -27, -81, 15, 89, -76, -9, 9, -114, 99, 80, 18, 0, -90, 38, -127, -38, -46, 4,\n            -76, 78, -97, 113, -128, 33, 42, -124, 78, -4, -35, 38, -87, -62, -42, -9, -14, -30, 120, 127,\n            -69, 6, -82, 110, -21, -44, -46, 0, -72, 103, 77, 12, 73, -110, 125, 109, -55, -1, 53, 17,\n            86, 70, 109, 66, 113, -72, 72, -39, 32, -89, -38, 53, 99, -82, 100, -14, 48, -39, -74, 57,\n            107, -100, -49, -47, -84, -77, 24, 7, 121, 69, -125, -64, 126, -114, 82, -91, -66, 3, 106, 37,\n            59, -12, 21, 85, -46, -87, 80, -91, -20, 52, -100, 46, -38, -108, -87, 111, 104, -103, -64, 58,\n            71, -121, -15, -48, -60, -63, -54, -24, -80, -14, 104, 35, -73, -37, 83, -42, -112, 69, 5, -15,\n            -10, -108, 23, -12, 3, 63, -92, -65, 63, 43, 101, -5, -10, 7, 14, -111, 112, -66, -108, -28,\n            -111, 71, 27, -1, 47, -93, -65, 77, 38, -9, 81, 27, 46, 121, -76, -63, 18, 29, 86, 30,\n            109, -80, 68, -113, -50, -97, -14, -14, -16, 120, 60, -98, 1, 126, 0, -110, 81, -78, -5, 36,\n            19, -64, 3, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126};\n    private static final byte[] FOLDER = {\n            -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,\n            0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71,\n            68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0,\n            0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -102, 73, 68, 65, 84,\n            88, -123, -19, -105, 61, -113, 19, 49, 16, -122, 95, 111, -110, 75, -124, -124, -60, -49, 65, 20,\n            -48, 34, 81, 80, 32, 81, 83, 80, -94, 52, 72, 8, 78, 66, -94, 64, 20, -108, 124, 20,\n            39, -47, 64, -51, -11, -7, 87, -96, 11, -38, -75, 61, 99, 15, -59, 110, 54, 118, -42, -5,\n            113, -71, 77, 117, -5, 54, -55, -50, -38, -42, -77, -81, 61, 99, 27, -104, 52, 105, -46, -92,\n            73, -73, 66, 106, 104, -61, -51, 57, -92, -21, -3, -29, 79, -61, -57, 58, 70, -13, 33, -115,\n            54, -25, -112, 71, -17, 127, 0, 0, -106, -38, -105, 65, 93, 64, -56, 0, 108, 33, 76, -72,\n            -28, -113, -14, -20, -77, 59, 25, 108, 54, 4, -14, -63, -6, 59, -112, 123, -84, -4, -84, -114,\n            -117, 39, 96, -74, -17, -2, -12, -59, 91, 92, -66, -103, 117, -70, 126, 19, 117, 58, -80, 57,\n            -121, 60, 92, 127, 5, 0, -84, -106, -53, -3, 11, 93, -108, -96, 0, 96, 52, -124, 9, 96,\n            6, 116, -127, -33, -65, -66, -44, -51, 56, -117, -121, -73, 62, 3, -7, 56, -74, 123, 126, -11,\n            -83, -24, 100, -23, -99, -6, -43, -67, -5, -119, -96, -119, -66, 80, 1, -128, 49, 0, -128, -25,\n            31, -98, -108, 65, 103, -93, 46, -62, -36, 24, -58, -27, -37, -6, -65, -39, -66, -108, -41, 63,\n            -13, 86, -40, 65, 107, 84, -7, 43, 64, -103, 70, 92, 108, 17, -45, 82, -94, -115, -47, 77,\n            -40, -22, 35, -108, 53, 32, 34, 56, 34, 112, -49, -14, 30, 4, 90, 66, -2, -119, 99, -34,\n            2, -69, -23, -33, -119, -10, -32, 18, -66, -37, -63, 114, 16, 99, -122, 2, -64, 87, 127, 97,\n            -56, -61, -53, 24, -96, -121, -14, -107, 35, 103, 11, -120, -91, 116, 27, -25, -45, -15, 96, 9,\n            80, -95, 97, -56, -61, 114, 127, 14, -10, 102, 125, 27, 36, -128, 86, -56, -70, 34, -52, 50,\n            -128, 109, -78, 77, 40, 118, -82, -73, 77, -65, -93, 98, 26, -128, -111, -62, 41, 62, -101, 67,\n            116, -111, 110, -41, 34, -53, 2, -26, 22, -9, 3, 29, 53, -11, -111, -109, -39, -94, -4, -83,\n            0, 85, -74, -120, -41, -25, 72, -22, 4, -27, -86, -58, -119, 45, -102, -119, 3, 52, -36, -124,\n            61, 40, 65, 81, -58, 55, -5, -117, 99, -80, 115, -89, 115, 52, 9, 93, 65, -90, -36, 76,\n            65, 94, 87, -67, -96, 74, -52, -2, 52, -46, 54, -91, -95, -109, -119, 108, 87, -13, -59, 126,\n            -9, 74, 40, -109, 27, 58, 106, 124, 6, -79, 40, -117, -7, 33, 100, 0, 23, -71, -72, -37,\n            -1, -85, 105, 31, -61, 77, 96, -24, -44, -109, 1, 40, -19, 70, 50, 113, -126, -75, -39, -22,\n            -26, 53, -85, -61, 113, 89, 127, 8, 23, 78, 119, 80, 55, -5, -36, -12, 117, 34, -11, 23,\n            -4, 78, 80, -14, 10, 112, 22, -30, 92, -5, -6, 28, 2, 25, -70, -103, 112, -46, -75, -19,\n            98, 67, 65, 57, 60, -110, -11, 13, 118, 4, -92, -9, 2, 79, 4, -94, 17, 118, 38, 97,\n            6, -104, 64, -108, -34, -103, -124, -35, -63, 115, -20, -68, -46, 58, 124, 2, 0, 56, 91, -18,\n            118, -123, -74, -48, 122, 88, -78, 117, -126, 90, 95, 102, -80, 49, 5, -88, 26, 80, -101, -46,\n            33, 83, -24, -42, 126, 53, 116, 42, 97, -104, -22, 2, -97, 111, 115, 108, -13, 17, 64, 1,\n            -64, 85, 103, 71, 50, 84, 1, -106, 110, 88, 106, 95, 10, 93, -5, -67, -81, 62, -44, 22,\n            26, 84, 1, -33, -67, -77, -24, 5, -19, -67, -116, 93, -84, 87, 2, 32, -70, 66, -104, -32,\n            -112, -53, 94, -63, -113, 112, 1, 125, 119, -15, -17, -92, -73, -40, 73, -109, 38, 77, -70, -19,\n            -6, 15, -2, -54, -98, -96, -19, -118, -95, -10, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66,\n            96, -126};\n    private static final byte[] HOME = {\n            -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,\n            0, 0, 0, 40, 8, 4, 0, 0, 0, 34, 2, -96, -37, 0, 0, 0, 1, 115, 82, 71,\n            66, 0, -82, -50, 28, -23, 0, 0, 0, 2, 98, 75, 71, 68, 0, -1, -121, -113, -52, -65,\n            0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 46, 35, 0, 0, 46, 35, 1, 120, -91, 63,\n            118, 0, 0, 0, 7, 116, 73, 77, 69, 7, -37, 8, 4, 10, 36, 16, -22, -9, -18, -50,\n            0, 0, 2, -84, 73, 68, 65, 84, 72, -57, -19, -106, 91, 72, 20, 97, 20, -57, 127, -77,\n            -18, -106, -41, 54, -14, -106, -23, -102, 4, 21, 97, 5, 5, 61, 70, 74, -76, -122, -94, -11,\n            18, -108, -92, 121, 41, 16, -124, 96, -95, 48, -118, -108, -94, 18, -70, 96, 33, 68, -81, -127,\n            15, 25, 68, 15, 21, 89, 80, 42, 25, 42, -91, 97, -106, -103, -122, 107, 41, -19, -86, -37,\n            -22, 122, -39, 77, -73, -103, -81, 7, -105, 84, -36, -85, -26, -125, -44, -127, -31, 59, -52, -103,\n            -7, 113, -50, -103, -17, -4, -65, -127, -1, -74, 44, 76, -102, 113, -29, 22, -119, 50, -3, -15,\n            84, 75, -111, -87, -38, -81, -89, 84, 24, -120, 1, -96, -106, -102, -65, 83, -66, -118, 59, -119,\n            39, 54, 97, 69, -31, -109, 115, -14, 8, 15, 124, -107, -17, 27, 42, 113, 123, 125, 81, 46,\n            102, 28, 8, -84, -68, 116, 78, -26, 80, 77, 5, -79, 104, -48, -48, 70, -39, 124, -88, -38,\n            103, 37, -107, -70, -94, 28, -6, 105, 7, 34, 89, 67, -86, -90, -74, 106, 82, -51, 65, 125,\n            -110, 13, 27, -99, 43, 2, -17, -87, -60, -51, -124, -30, 99, -104, -88, -63, -12, 20, -93, -90,\n            96, 111, -80, -106, 20, 117, -3, -35, -97, -65, 54, -13, 29, 21, 8, -9, -3, -14, -122, -68,\n            17, 127, 50, 15, 51, -49, 49, 85, -109, 73, -79, 51, -81, 94, -103, 96, 21, 123, -126, 66,\n            86, 10, 64, 4, 12, -107, -72, -70, -50, -112, -57, 0, 47, -24, 123, 66, 46, 50, 80, -19,\n            40, 121, -59, 20, -31, -20, 102, 0, 121, 1, -48, -14, -72, 83, -7, 12, 81, -121, -79, -106,\n            67, 76, -71, -18, 94, -73, 85, 54, 34, 19, -122, 115, -102, 23, 16, -12, 114, 92, 73, 1,\n            22, 26, -24, 110, -26, 0, -114, 89, 17, -61, -32, -61, 22, 4, 2, -127, -30, -31, 125, -9,\n            -48, -117, -79, 103, -13, -79, -48, 68, -57, 123, -46, 25, -101, 19, -109, 57, -38, -41, -40, -127,\n            64, 16, -127, 70, -113, -34, 63, 104, 105, -52, -7, 2, -84, -76, -48, -42, 69, 26, -42, 121,\n            113, 59, 89, 93, -35, -67, 8, -126, -39, -87, 81, -35, 103, -69, 111, -24, -71, -24, 11, -123,\n            12, -45, -58, -37, -81, -20, -61, -20, -74, 18, 11, -23, -19, -125, 102, 20, -76, 36, 107, 121,\n            76, -68, 119, -24, -103, -88, 75, -123, -116, -16, -111, 38, 51, 122, -66, 121, -4, -116, 95, 68,\n            -42, 59, -5, 8, -126, 24, 54, -24, 120, 68, -124, 103, -24, -23, -88, -14, -29, -40, -24, -28,\n            -75, 85, -92, -47, -27, 117, 48, -102, -27, -20, 86, 121, 28, 5, 29, 107, 119, 112, 111, -10,\n            24, 5, -51, -72, 17, -122, -32, 107, -39, -110, -99, 30, -22, -58, -108, -3, -76, -6, 20, -93,\n            -49, -78, -59, -102, 17, -50, 40, -126, -47, -115, 66, 59, 94, -29, 78, 80, -86, -40, 74, -108,\n            20, -119, 34, 50, -88, -13, 83, 58, -81, -112, 73, 47, 70, -116, 52, -104, -34, 120, 86, 41,\n            -119, 4, 10, 1, 24, -26, -106, 71, 88, 10, 41, 0, 60, -93, -47, 31, -107, 18, -124, -123,\n            -106, -19, -30, 7, 31, 122, -68, 64, 83, 19, 75, -75, -12, 51, 108, -101, -127, -6, -40, -4,\n            97, -92, -110, -20, 67, 18, -109, -40, -58, 106, -113, -86, -66, 96, 83, -36, 15, -2, 34, -96,\n            -110, 88, 8, 84, -8, 60, -37, 60, 67, -43, -18, -127, 2, 1, 26, -74, 120, -124, 70, 11,\n            20, -108, 64, -113, 104, -119, 88, -99, -24, 112, -105, -71, 112, 117, -44, 22, 88, -90, 32, 8,\n            -27, 48, 33, 76, 119, 78, -52, 89, 101, 20, -100, -12, 49, 21, 24, 84, 97, 12, 59, 19,\n            46, -44, -36, 75, 65, 70, 70, 118, -83, -2, 67, 101, -21, 80, -123, -65, -69, -64, -79, -68,\n            127, -48, -106, 4, -6, -113, -37, 111, 38, -57, 11, 112, 71, 102, 113, -50, 0, 0, 0, 0,\n            73, 69, 78, 68, -82, 66, 96, -126};\n    private static final byte[] UPDIR = {\n            -119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 42,\n            0, 0, 0, 40, 8, 6, 0, 0, 0, -120, 11, 104, 80, 0, 0, 0, 6, 98, 75, 71,\n            68, 0, -1, 0, -1, 0, -1, -96, -67, -89, -109, 0, 0, 0, 9, 112, 72, 89, 115, 0,\n            0, 14, -60, 0, 0, 14, -60, 1, -107, 43, 14, 27, 0, 0, 2, -111, 73, 68, 65, 84,\n            88, -123, -19, -106, -51, 74, -21, 64, 20, -57, -1, -87, -47, 7, -15, 41, 92, 118, -47, 82,\n            10, -59, 110, 20, -95, 32, 77, -101, 44, -92, -37, -66, 80, -23, 3, 8, -126, -76, -48, -115,\n            -120, 43, 117, -19, -109, -44, -113, -50, -57, -103, 115, 87, 103, 76, 106, -117, 77, 19, -17, -107,\n            75, -2, 48, -52, -112, 100, 38, -65, 57, -25, -52, 57, 3, 84, -86, 84, -87, -46, -1, -83,\n            36, 73, 56, 73, 18, -2, -87, -11, -61, 50, 22, -119, -29, -104, 47, 47, 47, 97, -116, -127,\n            -75, -106, 39, -109, 73, 80, -58, -70, 105, 21, 94, 48, -114, 99, 78, -110, 4, 68, 4, -91,\n            20, -116, 49, 120, 121, 121, -63, 120, 60, 46, 21, -74, 86, 100, -14, 112, 56, -28, -85, -85,\n            43, 0, -128, -75, 22, -50, 57, 48, 51, -114, -113, -113, -47, -21, -11, 74, 13, -125, -67, 65,\n            -121, -61, 33, -113, 70, 35, 4, 65, 0, 99, -116, 111, -42, 90, 16, 17, 58, -99, 14, 46,\n            46, 46, 74, -125, -35, 11, 84, 44, -23, -100, -61, -57, -57, 7, 86, -85, 21, -116, 49, 32,\n            34, 111, 89, 107, 45, 90, -83, 22, -50, -50, -50, 74, -127, -51, 125, -104, 6, -125, 1, -57,\n            113, -20, -83, -89, -108, -126, -75, 22, 0, -32, -100, -13, 45, 8, 2, 48, 51, -102, -51, 38,\n            -100, 115, 124, 125, 125, 93, 40, 102, 115, -127, 14, 6, 3, -114, -94, -56, -69, 87, 107, 13,\n            34, 2, 0, -1, 44, 8, -78, 60, 68, -124, 70, -93, 1, 34, -30, -101, -101, -101, -67, 97,\n            119, 6, -115, -94, -120, 123, -67, -98, -121, 19, 43, 10, -88, -12, -58, 24, 56, -25, -4, 55,\n            -78, -127, 122, -67, 14, 34, -30, -37, -37, -37, -67, 96, 119, -102, 20, 69, 17, 119, -69, 93,\n            15, 98, -83, -59, -63, -63, 1, 0, 32, 12, -61, 12, -88, -124, -61, -37, -37, 27, -34, -33,\n            -33, -3, -122, 68, 15, 15, 15, -104, -49, -25, -71, 97, -65, -99, -48, -17, -9, -71, -35, 110,\n            -5, 83, 45, 58, 58, 58, -14, -112, 34, -79, -30, -21, -21, 43, -106, -53, 37, -76, -42, 0,\n            0, -26, -49, -13, 20, -122, 33, -18, -17, -17, -79, 88, 44, 114, -63, 126, -21, 122, 102, -58,\n            108, 54, -13, 22, 19, 32, 0, 56, 61, 61, -3, -14, 76, 107, -115, -43, 106, 5, 34, -62,\n            -45, -45, 83, -26, -35, -6, -72, 84, -48, -23, 116, -70, 113, -25, -25, -25, -25, -20, -100, -53,\n            -4, 60, 29, -109, 114, -6, 103, -77, 89, 41, 21, 106, -17, 90, 47, -112, 2, 8, -64, 3,\n            -118, -85, -45, -33, 20, -43, -34, -96, -52, -100, 57, -35, -64, 103, -116, -82, 3, -1, 83, 80,\n            -87, 62, 2, -72, -98, 79, 5, -74, 44, -107, 2, 42, 112, 0, 50, -15, -7, 43, 64, -103,\n            121, 35, -24, -81, 118, -67, -28, -41, -76, -53, -91, -43, -21, 117, -106, 119, -23, 94, -58, -113,\n            -113, -113, 59, 101, -123, -62, -96, -101, -54, -87, 88, -14, -28, -28, 4, -121, -121, -121, 25, 48,\n            -71, 35, 40, -91, -16, -4, -4, -68, -13, -1, 10, -71, 62, 125, -75, 91, 7, -83, -43, 106,\n            30, 74, 42, -104, -28, 89, -83, 53, -76, -42, -71, 98, -72, -112, 69, -91, 68, 110, -69, -96,\n            -92, 123, -7, 70, 41, 5, -91, 84, -18, 10, 85, 10, 104, 58, 70, -45, 125, -6, -80, 73,\n            47, 22, -51, -85, 66, -82, 87, 74, 1, -128, -17, 55, -127, 109, 2, -1, -85, -96, 98, -47,\n            -76, 91, -73, 1, -82, -113, 1, -8, 107, -30, -113, -125, -34, -35, -35, 5, 68, -28, 19, -27,\n            -74, -12, 83, 102, -46, -81, 84, -87, 82, -91, 95, -88, 63, 49, -122, -88, 68, 127, -55, -90,\n            73, 0, 0, 0, 0, 73, 69, 78, 68, -82, 66, 96, -126};\n    private static final byte[] ARROW = {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 48,\n            0, 0, 0, 117, 8, 3, 0, 0, 0, 63, 73, -110, 106, 0, 0, 2, -9, 80, 76, 84,\n            69, -46, -46, -46, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -65, -65, -65, 0,\n            0, 0, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -54, -54, -54,\n            -48, -48, -48, -48, -48, -48, -47, -47, -47, -47, -47, -47, -49, -49, -49, -86, -86, -86, -48, -48,\n            -48, -48, -48, -48, -47, -47, -47, -52, -52, -52, -49, -49, -49, -48, -48, -48, -48, -48, -48, -1,\n            -1, -1, -49, -49, -49, -48, -48, -48, -48, -48, -48, -51, -51, -51, -50, -50, -50, -48, -48, -48,\n            -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52, -52, -52, -48, -48,\n            -48, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47, -52, -52, -52, -48, -48, -48, -48,\n            -48, -48, -1, -1, -1, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48,\n            -48, -48, -48, -41, -41, -41, -48, -48, -48, -49, -49, -49, -43, -43, -43, -47, -47, -47, -49, -49,\n            -49, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49, -47,\n            -47, -47, -49, -49, -49, -47, -47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -49, -49, -49,\n            -47, -47, -47, -44, -44, -44, -48, -48, -48, -48, -48, -48, -1, -1, -1, -46, -46, -46, -48, -48,\n            -48, -49, -49, -49, -47, -47, -47, -49, -49, -49, -35, -35, -35, -49, -49, -49, -49, -49, -49, -49,\n            -49, -49, -47, -47, -47, -40, -40, -40, -49, -49, -49, -48, -48, -48, -48, -48, -48, -47, -47, -47,\n            -47, -47, -47, -47, -47, -47, -48, -48, -48, -60, -60, -60, -53, -53, -53, -48, -48, -48, -46, -46,\n            -46, -65, -65, -65, -48, -48, -48, -54, -54, -54, -45, -45, -45, -47, -47, -47, -49, -49, -49, -48,\n            -48, -48, -49, -49, -49, -51, -51, -51, -48, -48, -48, -48, -48, -48, -46, -46, -46, -47, -47, -47,\n            -47, -47, -47, -37, -37, -37, -47, -47, -47, -49, -49, -49, -50, -50, -50, -48, -48, -48, -128, -128,\n            -128, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47,\n            -47, -47, -48, -48, -48, -48, -48, -48, -48, -48, -48, -43, -43, -43, -47, -47, -47, -48, -48, -48,\n            -50, -50, -50, -49, -49, -49, -48, -48, -48, -48, -48, -48, -33, -33, -33, -48, -48, -48, -52, -52,\n            -52, -51, -51, -51, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -52,\n            -52, -52, -48, -48, -48, -46, -46, -46, -48, -48, -48, -37, -37, -37, -29, -29, -29, -48, -48, -48,\n            -48, -48, -48, -47, -47, -47, -48, -48, -48, -49, -49, -49, -46, -46, -46, -48, -48, -48, -49, -49,\n            -49, -47, -47, -47, -58, -58, -58, -49, -49, -49, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47,\n            -47, -47, -50, -50, -50, -48, -48, -48, -48, -48, -48, -50, -50, -50, -48, -48, -48, -48, -48, -48,\n            -48, -48, -48, -55, -55, -55, -45, -45, -45, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48, -48,\n            -48, -46, -46, -46, -47, -47, -47, -48, -48, -48, -47, -47, -47, -51, -51, -51, -47, -47, -47, -51,\n            -51, -51, -48, -48, -48, -47, -47, -47, -48, -48, -48, -65, -65, -65, -48, -48, -48, -50, -50, -50,\n            -48, -48, -48, -47, -47, -47, -49, -49, -49, -47, -47, -47, -47, -47, -47, -48, -48, -48, -48, -48,\n            -48, -50, -50, -50, -47, -47, -47, -49, -49, -49, -49, -49, -49, -47, -47, -47, -48, -48, -48, -47,\n            -47, -47, -49, -49, -49, -39, -39, -39, -50, -50, -50, -47, -47, -47, -48, -48, -48, -42, -42, -42,\n            -47, -47, -47, -46, -46, -46, -47, -47, -47, -47, -47, -47, -48, -48, -48, -49, -49, -49, -49, -49,\n            -49, -47, -47, -47, -48, -48, -48, -47, -47, -47, -48, -48, -48, -50, -50, -50, -47, -47, -47, -48,\n            -48, -48, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49, -50, -50, -50, -47, -47, -47,\n            -56, -56, -56, -48, -48, -48, -48, -48, -48, -40, -40, -40, -45, -45, -45, -47, -47, -47, -47, -47,\n            -47, -49, -49, -49, -48, -48, -48, -43, -43, -43, -52, -52, -52, -49, -49, -49, -47, -47, -47, -47,\n            -47, -47, -50, -50, -50, -49, -49, -49, -47, -47, -47, -49, -49, -49, -48, -48, -48, -49, -49, -49,\n            -115, 52, -27, 40, 0, 0, 0, -3, 116, 82, 78, 83, 34, -37, -1, -7, 120, 4, 0, 5,\n            118, -9, -41, 38, 24, -46, -2, -100, 78, 69, 3, -98, -54, 28, 40, -37, -8, 115, 3, 122,\n            -6, -43, 36, 31, -49, -109, 78, 70, -93, -50, 20, 43, -38, -10, 113, 123, 30, -45, -115, 2,\n            86, -5, -9, 65, -94, -55, 19, -29, 111, 6, 127, 32, -39, -3, -114, 92, -4, 58, -86, -63,\n            22, -32, 109, -126, -5, -51, 36, -44, -121, 1, 90, -11, 59, -79, -59, 15, 48, -11, 106, -123,\n            26, 37, -40, 98, -3, -14, -80, -61, 13, 44, -25, 103, 8, -120, 29, 35, -35, -4, -127, 101,\n            46, -76, -71, 17, -24, 99, 7, -118, -58, 42, 124, 2, 103, -13, 47, -67, -65, 12, 55, -27,\n            -116, -57, 24, 44, -35, 119, 112, -21, 49, 8, -70, 10, 51, -19, -14, 93, -111, -62, 25, -30,\n            119, -78, 14, 9, -108, 113, 117, -18, -53, 63, -23, -15, 89, 9, -106, -2, -66, 21, 50, -125,\n            -33, -28, 57, 97, -97, -73, 19, 69, -12, -17, 81, -87, 57, 94, -99, -73, 72, -18, 82, -88,\n            -20, 27, 12, -100, 67, -16, 83, 16, -91, -122, 54, -26, 104, -101, 64, 85, 11, -82, 100, -101,\n            20, 84, -47, 125, 31, -102, 62, -57, -88, -125, 107, -102, -69, -15, -53, -77, 52, -31, -105, 66,\n            -21, 87, 121, 110, -67, 23, -60, -84, 13, 46, 111, 88, -75, 119, 42, 50, -32, 106, -107, 63,\n            91, -78, 117, 114, -108, 41, -51, -28, -65, 0, 0, 3, -107, 73, 68, 65, 84, 88, -61, -107,\n            -40, 105, 84, -108, 85, 24, 7, 112, -4, -113, -13, -112, -37, -101, -111, -96, 78, 67, -94, 38,\n            -18, -66, 14, 46, 52, -38, -28, -96, 34, 90, -67, -31, -106, 75, -114, 70, 74, 42, 90, 86,\n            -109, 4, 38, -88, -111, -90, 50, -126, -72, -46, -54, -102, 38, -102, -53, 104, -101, 123, -88, -71,\n            78, -18, -91, -90, 88, 82, 46, 89, 90, -71, -107, -27, 7, 95, 62, 120, -114, -100, -93, -121,\n            -5, 127, 63, -65, -65, 15, -9, -36, 123, -97, -5, 127, -98, -96, -96, 106, -80, 84, -73, -118,\n            -14, 23, 20, -4, 64, 13, -44, -84, 69, 0, -87, 93, 7, -38, -125, 12, -112, -70, 22, -32,\n            33, 6, -124, 60, 12, -44, 11, 37, -128, -124, -43, 71, -125, -122, 33, 4, -80, 61, 98, 71,\n            -8, -93, 4, -112, 70, 17, -48, 26, 51, 64, -102, 0, 53, -102, 50, 64, 30, -45, -48, 44,\n            -110, 1, -51, 91, -96, 101, 43, 43, 1, -84, -83, -19, -88, -39, -122, 0, 18, -38, 22, 104,\n            -89, 19, 64, -38, 59, 16, -43, -127, 1, -42, -114, 64, -89, -50, 4, -112, -80, 104, 68, 61,\n            -82, 19, -64, -39, -59, -126, -120, -82, 4, -112, -48, 39, -32, 122, -110, 1, -46, -51, 1, 119,\n            12, 3, -84, -35, -127, 30, 61, 9, 32, -51, 99, 17, -43, 75, 39, 64, 112, 92, 111, -12,\n            105, 68, 0, 121, -22, 105, 104, -49, 48, -64, 120, 22, -120, -17, 75, 0, -111, 126, 64, -1,\n            1, 12, 24, 24, -117, -25, 6, -39, 8, -32, 28, -20, 64, -77, 33, 4, -112, -95, -49, 3,\n            113, 6, 1, 100, -104, 7, -61, 99, 24, -32, 28, 1, -68, -112, 64, 0, 121, 113, 36, -30,\n            71, -23, 4, 72, 124, -55, -114, 78, -93, 9, 32, 99, -58, 34, 105, 28, 3, 100, -68, 27,\n            13, 94, 102, -128, -13, 21, 96, -62, -85, 4, -112, -127, -81, 33, -2, 117, -125, 0, -34, 55,\n            -110, 48, 49, -108, 0, -110, -36, 2, -82, 55, 25, -112, -110, 10, 76, 122, -117, 0, 34, -109,\n            -127, -76, 116, 6, 76, -103, -118, 73, -61, 116, 2, 120, -89, -71, -15, 118, 6, 1, -28, -99,\n            -23, -64, 12, -125, 0, -58, -69, -64, -52, 89, 4, -112, -39, -26, -70, 51, 125, 4, -112, 57,\n            89, -56, -98, -101, 66, 0, 95, -114, 3, -13, -26, 19, 64, -110, 23, -64, -79, -112, 1, 82,\n            -35, -123, -8, -95, 12, -16, -103, -21, 94, -76, -104, 0, 50, 101, 38, -122, 119, 51, 8, -112,\n            -5, -98, 11, 105, -75, 9, 80, 81, 58, -19, -17, 51, 64, -1, 0, -8, 112, 12, 1, -60,\n            -6, -111, -71, -18, -39, 4, -112, -113, -13, -112, 95, -112, 66, -128, -62, 34, -83, -8, -109, 37,\n            4, -112, -91, -79, -16, -92, 50, -64, -8, 20, 88, -58, 0, -55, 1, 74, 24, -80, 124, 36,\n            44, 43, 8, -112, -16, -103, -85, 120, -27, 42, 2, -84, 94, 3, -1, 90, 67, 29, -92, -41,\n            1, -42, 89, -43, 55, -50, 54, 24, -120, 14, 34, -114, -58, -25, 37, 112, 127, 65, 28, 62,\n            -33, -105, -59, -38, 87, -99, 9, -16, -11, 122, -8, 55, 16, 23, -56, 23, 14, 108, -12, -86,\n            95, 81, 91, 28, -80, 105, 51, 81, 4, -52, 10, 110, 73, 37, -54, -52, -106, -83, 46, -84,\n            -116, 36, -64, 55, 107, 80, -70, -115, -88, -83, -37, -21, 1, 59, -104, 98, -4, -83, -122, -84,\n            -47, 68, -71, -33, -71, 11, -10, -35, -60, 11, -108, -66, -57, -115, -52, 72, 2, -20, -51, -122,\n            127, -97, -95, 14, 2, 17, -64, 119, 78, -11, 103, 87, -17, 98, -34, -29, 90, -60, -61, -66,\n            60, 26, -98, -3, 68, 116, -16, 29, -48, -76, -52, -125, 4, 88, -67, 9, -2, 67, 68, -4,\n            -15, 30, 6, -70, 39, -86, 7, 44, -37, 17, 32, -21, 40, 17, -31, 118, -106, -64, 50, -120,\n            8, -119, -127, -17, 93, -104, -80, -124, 0, 63, 100, 35, -65, 61, -111, -116, -73, 31, 3, -114,\n            7, 8, 112, -62, 60, -43, 109, -120, -80, -66, 116, 42, -20, 63, 18, -3, 67, -32, -92, 27,\n            -89, 14, 18, -96, 111, -39, -67, 79, -11, -3, -128, -17, 52, -16, -109, 83, -67, 105, -46, -51,\n            61, -34, 21, 70, -76, 101, 63, -97, -127, 103, 60, -47, -8, 21, -106, 87, 122, -110, -85, 6,\n            -65, -4, -118, -46, -77, -122, 58, -16, -98, -82, 28, 43, -86, 2, 33, -25, -52, -32, 114, -108,\n            104, -64, -25, -100, -127, -3, 60, -47, -30, -5, -54, 93, -72, -16, 27, 1, 98, -4, -56, 46,\n            32, -26, 26, 9, 23, -127, -33, -1, 32, -64, 17, 13, 101, 42, 73, -27, -50, 63, -105, 74,\n            -48, -5, 50, 49, -3, 9, 20, 37, -95, -86, 61, -82, 4, 98, 74, -111, -1, -89, -95, 14,\n            114, -51, 123, -4, 87, -94, -6, -56, -53, -8, 27, -56, -69, 66, 12, -43, -82, 94, -125, -25,\n            58, 49, -74, 43, -68, -95, 21, 87, 57, -109, -71, 27, -4, -109, -121, -78, 127, 83, -44, -127,\n            -43, 124, -113, 111, 22, -118, 50, -48, -1, 3, -4, -86, -109, -54, 10, 80, 17, -8, -1, 23,\n            117, -112, 123, -53, 108, 41, 50, -44, -63, 109, -80, -19, 79, -78, 17, 39, 44, -102, 0, 0,\n            0, 0, 73, 69, 78, 68, -82, 66, 96, -126};\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/picker/FilePicker.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.picker;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.graphics.drawable.Drawable;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.DividerItemDecoration;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.widget.filepicker.adapter.FileAdapter;\nimport com.kunfei.bookshelf.widget.filepicker.adapter.PathAdapter;\nimport com.kunfei.bookshelf.widget.filepicker.entity.FileItem;\nimport com.kunfei.bookshelf.widget.filepicker.popup.ConfirmPopup;\nimport com.kunfei.bookshelf.widget.filepicker.util.StorageUtils;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n\n/**\n * 文件目录选择器\n *\n * @author 李玉江[QQ:1032694760]\n * @since 2015/9/29, 2017/01/01, 2017/01/08\n */\npublic class FilePicker extends ConfirmPopup<LinearLayout> implements FileAdapter.CallBack, PathAdapter.CallBack {\n    public static final int DIRECTORY = 0;\n    public static final int FILE = 1;\n\n    private String initPath;\n    private FileAdapter adapter = new FileAdapter();\n    private PathAdapter pathAdapter = new PathAdapter();\n    private TextView emptyView;\n    private OnFilePickListener onFilePickListener;\n    private int mode;\n    private CharSequence emptyHint = java.util.Locale.getDefault().getDisplayLanguage().contains(\"中文\") ? \"<空>\" : \"<Empty>\";\n\n    @IntDef(value = {DIRECTORY, FILE})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Mode {\n    }\n\n    /**\n     * @see #FILE\n     * @see #DIRECTORY\n     */\n    public FilePicker(Activity activity, @Mode int mode) {\n        super(activity);\n        setHalfScreen(true);\n        try {\n            this.initPath = StorageUtils.getDownloadPath();\n        } catch (RuntimeException e) {\n            this.initPath = StorageUtils.getInternalRootPath(activity);\n        }\n        this.mode = mode;\n        adapter.setOnlyListDir(mode == DIRECTORY);\n        adapter.setShowHideDir(false);\n        adapter.setShowHomeDir(false);\n        adapter.setShowUpDir(false);\n        adapter.setCallBack(this);\n        pathAdapter.setCallBack(this);\n    }\n\n    @Override\n    @NonNull\n    protected LinearLayout makeCenterView() {\n        @SuppressLint(\"InflateParams\") LinearLayout rootLayout = (LinearLayout) LayoutInflater.from(activity).inflate(R.layout.view_file_picker, null);\n\n        RecyclerView recyclerView = rootLayout.findViewById(R.id.rv_file);\n        recyclerView.addItemDecoration(new DividerItemDecoration(activity, LinearLayout.VERTICAL));\n        recyclerView.setLayoutManager(new LinearLayoutManager(activity));\n        recyclerView.setAdapter(adapter);\n\n        emptyView = rootLayout.findViewById(R.id.tv_empty);\n\n        RecyclerView pathView = rootLayout.findViewById(R.id.rv_path);\n        pathView.setLayoutManager(new LinearLayoutManager(activity, RecyclerView.HORIZONTAL, false));\n        pathView.setAdapter(pathAdapter);\n\n        return rootLayout;\n    }\n\n    public void setRootPath(String initPath) {\n        this.initPath = initPath;\n    }\n\n    public void setAllowExtensions(String[] allowExtensions) {\n        adapter.setAllowExtensions(allowExtensions);\n    }\n\n    public void setShowUpDir(boolean showUpDir) {\n        adapter.setShowUpDir(showUpDir);\n    }\n\n    public void setShowHomeDir(boolean showHomeDir) {\n        adapter.setShowHomeDir(showHomeDir);\n    }\n\n    public void setShowHideDir(boolean showHideDir) {\n        adapter.setShowHideDir(showHideDir);\n    }\n\n    public void setFileIcon(Drawable fileIcon) {\n        adapter.setFileIcon(fileIcon);\n    }\n\n    public void setFolderIcon(Drawable folderIcon) {\n        adapter.setFolderIcon(folderIcon);\n    }\n\n    public void setHomeIcon(Drawable homeIcon) {\n        adapter.setHomeIcon(homeIcon);\n    }\n\n    public void setUpIcon(Drawable upIcon) {\n        adapter.setUpIcon(upIcon);\n    }\n\n    public void setArrowIcon(Drawable arrowIcon) {\n        pathAdapter.setArrowIcon(arrowIcon);\n    }\n\n    public void setItemHeight(int itemHeight) {\n        adapter.setItemHeight(itemHeight);\n    }\n\n    public void setEmptyHint(CharSequence emptyHint) {\n        this.emptyHint = emptyHint;\n    }\n\n    @Override\n    protected void setContentViewBefore() {\n        boolean isPickFile = mode == FILE;\n        setCancelVisible(!isPickFile);\n        if (isPickFile) {\n            setSubmitText(activity.getString(android.R.string.cancel));\n        } else {\n            setSubmitText(activity.getString(android.R.string.ok));\n        }\n    }\n\n    @Override\n    protected void setContentViewAfter(View contentView) {\n        refreshCurrentDirPath(initPath);\n    }\n\n    @Override\n    protected void onSubmit() {\n        if (mode != FILE) {\n            String currentPath = adapter.getCurrentPath();\n            if (onFilePickListener != null) {\n                onFilePickListener.onFilePicked(currentPath);\n            }\n        }\n    }\n\n    @Override\n    public void dismiss() {\n        super.dismiss();\n    }\n\n    public FileAdapter getAdapter() {\n        return adapter;\n    }\n\n    public PathAdapter getPathAdapter() {\n        return pathAdapter;\n    }\n\n    public String getCurrentPath() {\n        return adapter.getCurrentPath();\n    }\n\n    /**\n     * 响应选择器的列表项点击事件\n     */\n    @Override\n    public void onFileClick(int position) {\n        FileItem fileItem = adapter.getItem(position);\n        if (fileItem.isDirectory()) {\n            refreshCurrentDirPath(fileItem.getPath());\n        } else {\n            String clickPath = fileItem.getPath();\n            if (mode != DIRECTORY) {\n                dismiss();\n                if (onFilePickListener != null) {\n                    onFilePickListener.onFilePicked(clickPath);\n                }\n            }\n        }\n    }\n\n    @Override\n    public void onPathClick(int position) {\n        refreshCurrentDirPath(pathAdapter.getItem(position));\n    }\n\n    private void refreshCurrentDirPath(String currentPath) {\n        if (currentPath.equals(\"/\")) {\n            pathAdapter.updatePath(\"/\");\n        } else {\n            pathAdapter.updatePath(currentPath);\n        }\n        adapter.loadData(currentPath);\n        int adapterCount = adapter.getItemCount();\n        if (adapter.isShowHomeDir()) {\n            adapterCount--;\n        }\n        if (adapter.isShowUpDir()) {\n            adapterCount--;\n        }\n        if (adapterCount < 1) {\n            emptyView.setVisibility(View.VISIBLE);\n            emptyView.setText(emptyHint);\n        } else {\n            emptyView.setVisibility(View.GONE);\n        }\n    }\n\n    public void setOnFilePickListener(OnFilePickListener listener) {\n        this.onFilePickListener = listener;\n    }\n\n    public interface OnFilePickListener {\n\n        void onFilePicked(String currentPath);\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/popup/BasicPopup.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.popup;\n\nimport android.app.Activity;\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.graphics.Color;\nimport android.graphics.drawable.ColorDrawable;\nimport android.util.DisplayMetrics;\nimport android.view.Gravity;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.Window;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.StyleRes;\n\nimport com.kunfei.bookshelf.widget.filepicker.util.ScreenUtils;\n\n\n/**\n * 弹窗基类\n *\n * @param <V> 弹窗的内容视图类型\n * @author 李玉江[QQ:1023694760]\n * @since 2015/7/19\n */\npublic abstract class BasicPopup<V extends View> implements DialogInterface.OnKeyListener,\n        DialogInterface.OnDismissListener {\n    public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;\n    public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;\n    protected Activity activity;\n    protected int screenWidthPixels;\n    protected int screenHeightPixels;\n    private Dialog dialog;\n    private FrameLayout contentLayout;\n    private boolean isPrepared = false;\n\n    public BasicPopup(Activity activity) {\n        this.activity = activity;\n        DisplayMetrics metrics = ScreenUtils.displayMetrics(activity);\n        screenWidthPixels = metrics.widthPixels;\n        screenHeightPixels = metrics.heightPixels;\n        initDialog();\n    }\n\n    private void initDialog() {\n        contentLayout = new FrameLayout(activity);\n        contentLayout.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));\n        contentLayout.setFocusable(true);\n        contentLayout.setFocusableInTouchMode(true);\n        dialog = new Dialog(activity);\n        dialog.setCanceledOnTouchOutside(true);//触摸屏幕取消窗体\n        dialog.setCancelable(true);//按返回键取消窗体\n        dialog.setOnKeyListener(this);\n        dialog.setOnDismissListener(this);\n        Window window = dialog.getWindow();\n        if (window != null) {\n            window.setGravity(Gravity.BOTTOM);\n            window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));\n            //AndroidRuntimeException: requestFeature() must be called before adding content\n            window.requestFeature(Window.FEATURE_NO_TITLE);\n            window.setContentView(contentLayout);\n        }\n        setSize(screenWidthPixels, WRAP_CONTENT);\n    }\n\n    public int getScreenWidthPixels() {\n        return screenWidthPixels;\n    }\n\n    public int getScreenHeightPixels() {\n        return screenHeightPixels;\n    }\n\n    /**\n     * 创建弹窗的内容视图\n     *\n     * @return the view\n     */\n    protected abstract V makeContentView();\n\n    /**\n     * 固定高度为屏幕的高\n     *\n     * @param fillScreen true为全屏\n     */\n    public void setFillScreen(boolean fillScreen) {\n        if (fillScreen) {\n            setSize(screenWidthPixels, (int) (screenHeightPixels * 0.85f));\n        }\n    }\n\n    /**\n     * 固定高度为屏幕的一半\n     *\n     * @param halfScreen true为半屏\n     */\n    public void setHalfScreen(boolean halfScreen) {\n        if (halfScreen) {\n            setSize(screenWidthPixels, screenHeightPixels / 2);\n        }\n    }\n\n    /**\n     * 位于屏幕何处\n     *\n     * @see Gravity\n     */\n    public void setGravity(int gravity) {\n        Window window = dialog.getWindow();\n        if (window != null) {\n            window.setGravity(gravity);\n        }\n        if (gravity == Gravity.CENTER) {\n            //居于屏幕正中间时，宽度不允许填充屏幕\n            setWidth((int) (screenWidthPixels * 0.7f));\n        }\n    }\n\n    /**\n     * 设置弹窗的内容视图之前执行\n     */\n    protected void setContentViewBefore() {\n    }\n\n    /**\n     * 设置弹窗的内容视图之后执行\n     *\n     * @param contentView 弹窗的内容视图\n     */\n    protected void setContentViewAfter(V contentView) {\n    }\n\n    public void setContentView(View view) {\n        contentLayout.removeAllViews();\n        contentLayout.addView(view);\n    }\n\n    public void setFitsSystemWindows(boolean flag) {\n        contentLayout.setFitsSystemWindows(flag);\n    }\n\n    public void setAnimationStyle(@StyleRes int animRes) {\n        Window window = dialog.getWindow();\n        if (window != null) {\n            window.setWindowAnimations(animRes);\n        }\n    }\n\n    public void setCanceledOnTouchOutside(boolean flag) {\n        dialog.setCanceledOnTouchOutside(flag);\n    }\n\n    public void setCancelable(boolean flag) {\n        dialog.setCancelable(flag);\n    }\n\n    public void setOnDismissListener(final DialogInterface.OnDismissListener onDismissListener) {\n        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {\n            @Override\n            public void onDismiss(DialogInterface dialog) {\n                BasicPopup.this.onDismiss(dialog);\n                onDismissListener.onDismiss(dialog);\n            }\n        });\n    }\n\n    public void setOnKeyListener(final DialogInterface.OnKeyListener onKeyListener) {\n        dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {\n            @Override\n            public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {\n                BasicPopup.this.onKey(dialog, keyCode, event);\n                return onKeyListener.onKey(dialog, keyCode, event);\n            }\n        });\n    }\n\n    /**\n     * 设置弹窗的宽和高\n     *\n     * @param width  宽\n     * @param height 高\n     */\n    public void setSize(int width, int height) {\n        if (width == MATCH_PARENT) {\n            //360奇酷等手机对话框MATCH_PARENT时两边还会有边距，故强制填充屏幕宽\n            width = screenWidthPixels;\n        }\n        if (width == 0 && height == 0) {\n            width = screenWidthPixels;\n            height = WRAP_CONTENT;\n        } else if (width == 0) {\n            width = screenWidthPixels;\n        } else if (height == 0) {\n            height = WRAP_CONTENT;\n        }\n        ViewGroup.LayoutParams params = contentLayout.getLayoutParams();\n        if (params == null) {\n            params = new ViewGroup.LayoutParams(width, height);\n        } else {\n            params.width = width;\n            params.height = height;\n        }\n        contentLayout.setLayoutParams(params);\n    }\n\n    /**\n     * 设置弹窗的宽\n     *\n     * @param width 宽\n     * @see #setSize(int, int)\n     */\n    public void setWidth(int width) {\n        setSize(width, 0);\n    }\n\n    /**\n     * 设置弹窗的高\n     *\n     * @param height 高\n     * @see #setSize(int, int)\n     */\n    public void setHeight(int height) {\n        setSize(0, height);\n    }\n\n    /**\n     * 设置是否需要重新初始化视图，可用于数据刷新\n     */\n    public void setPrepared(boolean prepared) {\n        isPrepared = prepared;\n    }\n\n    public boolean isShowing() {\n        return dialog.isShowing();\n    }\n\n    public final void show() {\n        if (isPrepared) {\n            dialog.show();\n            showAfter();\n            return;\n        }\n        setContentViewBefore();\n        V view = makeContentView();\n        setContentView(view);// 设置弹出窗体的布局\n        setContentViewAfter(view);\n        isPrepared = true;\n        dialog.show();\n        showAfter();\n    }\n\n    protected void showAfter() {\n    }\n\n    public void dismiss() {\n        dismissImmediately();\n    }\n\n    protected final void dismissImmediately() {\n        dialog.dismiss();\n    }\n\n    public boolean onBackPress() {\n        dismiss();\n        return false;\n    }\n\n    @Override\n    public final boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {\n        if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {\n            onBackPress();\n        }\n        return false;\n    }\n\n    @Override\n    public void onDismiss(DialogInterface dialog) {\n        dismiss();\n    }\n\n    public Context getContext() {\n        return dialog.getContext();\n    }\n\n    public Window getWindow() {\n        return dialog.getWindow();\n    }\n\n    /**\n     * 弹框的内容视图\n     */\n    public View getContentView() {\n        // IllegalStateException: The specified child already has a parent.\n        // You must call removeView() on the child's parent first.\n        return contentLayout.getChildAt(0);\n    }\n\n    /**\n     * 弹框的根视图\n     */\n    public ViewGroup getRootView() {\n        return contentLayout;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/popup/ConfirmPopup.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.popup;\n\nimport android.app.Activity;\nimport android.graphics.Color;\nimport android.text.TextUtils;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.LinearLayout;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.IntRange;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.StringRes;\n\nimport com.kunfei.bookshelf.widget.filepicker.util.ConvertUtils;\n\n/**\n * 带确定及取消按钮的弹窗\n *\n * @author 李玉江[QQ:1032694760]\n * @since 2015/10/21\n */\n@SuppressWarnings(\"WeakerAccess\")\npublic abstract class ConfirmPopup<V extends View> extends BasicPopup<View> {\n    protected boolean topLineVisible = true;\n    protected int topLineColor = 0xFF33B5E5;\n    protected int topLineHeightPixels = 1;//px\n    protected int topBackgroundColor = Color.WHITE;\n    protected int topHeight = 40;//dp\n    protected int topPadding = 15;//dp\n    protected int contentLeftAndRightPadding = 0;//dp\n    protected int contentTopAndBottomPadding = 0;//dp\n    protected boolean cancelVisible = true;\n    protected CharSequence cancelText = \"\";\n    protected CharSequence submitText = \"\";\n    protected CharSequence titleText = \"\";\n    protected int cancelTextColor = 0xFF33B5E5;\n    protected int submitTextColor = 0xFF33B5E5;\n    protected int titleTextColor = Color.BLACK;\n    protected int pressedTextColor = 0XFF0288CE;\n    protected int cancelTextSize = 0;\n    protected int submitTextSize = 0;\n    protected int titleTextSize = 0;\n    protected int backgroundColor = Color.WHITE;\n    protected TextView cancelButton, submitButton;\n    protected View titleView;\n    protected View headerView, centerView, footerView;\n\n    public ConfirmPopup(Activity activity) {\n        super(activity);\n        cancelText = activity.getString(android.R.string.cancel);\n        submitText = activity.getString(android.R.string.ok);\n    }\n\n    /**\n     * 设置顶部标题栏下划线颜色\n     */\n    public void setTopLineColor(@ColorInt int topLineColor) {\n        this.topLineColor = topLineColor;\n    }\n\n    /**\n     * 设置顶部标题栏下划线高度，单位为px\n     */\n    public void setTopLineHeight(int topLineHeightPixels) {\n        this.topLineHeightPixels = topLineHeightPixels;\n    }\n\n    /**\n     * 设置顶部标题栏背景颜色\n     */\n    public void setTopBackgroundColor(@ColorInt int topBackgroundColor) {\n        this.topBackgroundColor = topBackgroundColor;\n    }\n\n    /**\n     * 设置顶部标题栏高度（单位为dp）\n     */\n    public void setTopHeight(@IntRange(from = 10, to = 80) int topHeight) {\n        this.topHeight = topHeight;\n    }\n\n    /**\n     * 设置顶部按钮左边及右边边距（单位为dp）\n     */\n    public void setTopPadding(int topPadding) {\n        this.topPadding = topPadding;\n    }\n\n    /**\n     * 设置顶部标题栏下划线是否显示\n     */\n    public void setTopLineVisible(boolean topLineVisible) {\n        this.topLineVisible = topLineVisible;\n    }\n\n    /**\n     * 设置内容上下左右边距（单位为dp）\n     */\n    public void setContentPadding(int leftAndRight, int topAndBottom) {\n        this.contentLeftAndRightPadding = leftAndRight;\n        this.contentTopAndBottomPadding = topAndBottom;\n    }\n\n    /**\n     * 设置顶部标题栏取消按钮是否显示\n     */\n    public void setCancelVisible(boolean cancelVisible) {\n        if (null != cancelButton) {\n            cancelButton.setVisibility(cancelVisible ? View.VISIBLE : View.GONE);\n        } else {\n            this.cancelVisible = cancelVisible;\n        }\n    }\n\n    /**\n     * 设置顶部标题栏取消按钮文字\n     */\n    public void setCancelText(CharSequence cancelText) {\n        if (null != cancelButton) {\n            cancelButton.setText(cancelText);\n        } else {\n            this.cancelText = cancelText;\n        }\n    }\n\n    /**\n     * 设置顶部标题栏取消按钮文字\n     */\n    public void setCancelText(@StringRes int textRes) {\n        setCancelText(activity.getString(textRes));\n    }\n\n    /**\n     * 设置顶部标题栏确定按钮文字\n     */\n    public void setSubmitText(CharSequence submitText) {\n        if (null != submitButton) {\n            submitButton.setText(submitText);\n        } else {\n            this.submitText = submitText;\n        }\n    }\n\n    /**\n     * 设置顶部标题栏确定按钮文字\n     */\n    public void setSubmitText(@StringRes int textRes) {\n        setSubmitText(activity.getString(textRes));\n    }\n\n    /**\n     * 设置顶部标题栏标题文字\n     */\n    public void setTitleText(CharSequence titleText) {\n        if (titleView != null && titleView instanceof TextView) {\n            ((TextView) titleView).setText(titleText);\n        } else {\n            this.titleText = titleText;\n        }\n    }\n\n    /**\n     * 设置顶部标题栏标题文字\n     */\n    public void setTitleText(@StringRes int textRes) {\n        setTitleText(activity.getString(textRes));\n    }\n\n    /**\n     * 设置顶部标题栏取消按钮文字颜色\n     */\n    public void setCancelTextColor(@ColorInt int cancelTextColor) {\n        if (null != cancelButton) {\n            cancelButton.setTextColor(cancelTextColor);\n        } else {\n            this.cancelTextColor = cancelTextColor;\n        }\n    }\n\n    /**\n     * 设置顶部标题栏确定按钮文字颜色\n     */\n    public void setSubmitTextColor(@ColorInt int submitTextColor) {\n        if (null != submitButton) {\n            submitButton.setTextColor(submitTextColor);\n        } else {\n            this.submitTextColor = submitTextColor;\n        }\n    }\n\n    /**\n     * 设置顶部标题栏标题文字颜色\n     */\n    public void setTitleTextColor(@ColorInt int titleTextColor) {\n        if (null != titleView && titleView instanceof TextView) {\n            ((TextView) titleView).setTextColor(titleTextColor);\n        } else {\n            this.titleTextColor = titleTextColor;\n        }\n    }\n\n    /**\n     * 设置按下时的文字颜色\n     */\n    public void setPressedTextColor(int pressedTextColor) {\n        this.pressedTextColor = pressedTextColor;\n    }\n\n    /**\n     * 设置顶部标题栏取消按钮文字大小（单位为sp）\n     */\n    public void setCancelTextSize(@IntRange(from = 10, to = 40) int cancelTextSize) {\n        this.cancelTextSize = cancelTextSize;\n    }\n\n    /**\n     * 设置顶部标题栏确定按钮文字大小（单位为sp）\n     */\n    public void setSubmitTextSize(@IntRange(from = 10, to = 40) int submitTextSize) {\n        this.submitTextSize = submitTextSize;\n    }\n\n    /**\n     * 设置顶部标题栏标题文字大小（单位为sp）\n     */\n    public void setTitleTextSize(@IntRange(from = 10, to = 40) int titleTextSize) {\n        this.titleTextSize = titleTextSize;\n    }\n\n    /**\n     * 设置选择器主体背景颜色\n     */\n    public void setBackgroundColor(@ColorInt int backgroundColor) {\n        this.backgroundColor = backgroundColor;\n    }\n\n    public TextView getCancelButton() {\n        if (null == cancelButton) {\n            throw new NullPointerException(\"please call show at first\");\n        }\n        return cancelButton;\n    }\n\n    public TextView getSubmitButton() {\n        if (null == submitButton) {\n            throw new NullPointerException(\"please call show at first\");\n        }\n        return submitButton;\n    }\n\n    /**\n     * @see #makeHeaderView()\n     * @see #makeCenterView()\n     * @see #makeFooterView()\n     */\n    @Override\n    protected final View makeContentView() {\n        LinearLayout rootLayout = new LinearLayout(activity);\n        rootLayout.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));\n        rootLayout.setBackgroundColor(backgroundColor);\n        rootLayout.setOrientation(LinearLayout.VERTICAL);\n        rootLayout.setGravity(Gravity.CENTER);\n        rootLayout.setPadding(0, 0, 0, 0);\n        rootLayout.setClipToPadding(false);\n        View headerView = makeHeaderView();\n        if (headerView != null) {\n            rootLayout.addView(headerView);\n        }\n        if (topLineVisible) {\n            View lineView = new View(activity);\n            lineView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, topLineHeightPixels));\n            lineView.setBackgroundColor(topLineColor);\n            rootLayout.addView(lineView);\n        }\n        if (centerView == null) {\n            centerView = makeCenterView();\n        }\n        int lr = 0;\n        int tb = 0;\n        if (contentLeftAndRightPadding > 0) {\n            lr = ConvertUtils.toPx(activity, contentLeftAndRightPadding);\n        }\n        if (contentTopAndBottomPadding > 0) {\n            tb = ConvertUtils.toPx(activity, contentTopAndBottomPadding);\n        }\n        centerView.setPadding(lr, tb, lr, tb);\n        ViewGroup vg = (ViewGroup) centerView.getParent();\n        if (vg != null) {\n            //IllegalStateException: The specified child already has a parent\n            vg.removeView(centerView);\n        }\n        rootLayout.addView(centerView, new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1.0f));\n        View footerView = makeFooterView();\n        if (footerView != null) {\n            rootLayout.addView(footerView);\n        }\n        return rootLayout;\n    }\n\n    @Nullable\n    protected View makeHeaderView() {\n        if (null != headerView) {\n            return headerView;\n        }\n        RelativeLayout topButtonLayout = new RelativeLayout(activity);\n        int height = ConvertUtils.toPx(activity, topHeight);\n        topButtonLayout.setLayoutParams(new RelativeLayout.LayoutParams(MATCH_PARENT, height));\n        topButtonLayout.setBackgroundColor(topBackgroundColor);\n        topButtonLayout.setGravity(Gravity.CENTER_VERTICAL);\n\n        cancelButton = new TextView(activity);\n        cancelButton.setVisibility(cancelVisible ? View.VISIBLE : View.GONE);\n        RelativeLayout.LayoutParams cancelParams = new RelativeLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT);\n        cancelParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);\n        cancelParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);\n        cancelButton.setLayoutParams(cancelParams);\n        cancelButton.setBackgroundColor(Color.TRANSPARENT);\n        cancelButton.setGravity(Gravity.CENTER);\n        int padding = ConvertUtils.toPx(activity, topPadding);\n        cancelButton.setPadding(padding, 0, padding, 0);\n        if (!TextUtils.isEmpty(cancelText)) {\n            cancelButton.setText(cancelText);\n        }\n        cancelButton.setTextColor(ConvertUtils.toColorStateList(cancelTextColor, pressedTextColor));\n        if (cancelTextSize != 0) {\n            cancelButton.setTextSize(cancelTextSize);\n        }\n        cancelButton.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                dismiss();\n                onCancel();\n            }\n        });\n        topButtonLayout.addView(cancelButton);\n\n        if (null == titleView) {\n            TextView textView = new TextView(activity);\n            RelativeLayout.LayoutParams titleParams = new RelativeLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);\n            int margin = ConvertUtils.toPx(activity, topPadding);\n            titleParams.leftMargin = margin;\n            titleParams.rightMargin = margin;\n            titleParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);\n            titleParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);\n            textView.setLayoutParams(titleParams);\n            textView.setGravity(Gravity.CENTER);\n            if (!TextUtils.isEmpty(titleText)) {\n                textView.setText(titleText);\n            }\n            textView.setTextColor(titleTextColor);\n            if (titleTextSize != 0) {\n                textView.setTextSize(titleTextSize);\n            }\n            titleView = textView;\n        }\n        topButtonLayout.addView(titleView);\n\n        submitButton = new TextView(activity);\n        RelativeLayout.LayoutParams submitParams = new RelativeLayout.LayoutParams(WRAP_CONTENT, MATCH_PARENT);\n        submitParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);\n        submitParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);\n        submitButton.setLayoutParams(submitParams);\n        submitButton.setBackgroundColor(Color.TRANSPARENT);\n        submitButton.setGravity(Gravity.CENTER);\n        submitButton.setPadding(padding, 0, padding, 0);\n        if (!TextUtils.isEmpty(submitText)) {\n            submitButton.setText(submitText);\n        }\n        submitButton.setTextColor(ConvertUtils.toColorStateList(submitTextColor, pressedTextColor));\n        if (submitTextSize != 0) {\n            submitButton.setTextSize(submitTextSize);\n        }\n        submitButton.setOnClickListener(v -> {\n            dismiss();\n            onSubmit();\n        });\n        topButtonLayout.addView(submitButton);\n\n        return topButtonLayout;\n    }\n\n    @NonNull\n    protected abstract V makeCenterView();\n\n    @Nullable\n    protected View makeFooterView() {\n        if (null != footerView) {\n            return footerView;\n        }\n        return null;\n    }\n\n    protected void onSubmit() {\n\n    }\n\n    protected void onCancel() {\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/ConvertUtils.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.util;\n\nimport android.annotation.TargetApi;\nimport android.content.ContentUris;\nimport android.content.Context;\nimport android.content.res.ColorStateList;\nimport android.content.res.Resources;\nimport android.database.Cursor;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.ColorDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.NinePatchDrawable;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.provider.DocumentsContract;\nimport android.provider.MediaStore;\nimport android.text.TextUtils;\nimport android.view.View;\nimport android.widget.ListView;\nimport android.widget.ScrollView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.FloatRange;\n\nimport java.io.BufferedReader;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.ByteBuffer;\nimport java.text.DecimalFormat;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 数据类型转换、单位转换\n *\n * @author 李玉江[QQ:1023694760]\n * @since 2014-4-18\n */\npublic class ConvertUtils {\n    public static final long GB = 1073741824;\n    public static final long MB = 1048576;\n    public static final long KB = 1024;\n\n    public static int toInt(Object obj) {\n        try {\n            return Integer.parseInt(obj.toString());\n        } catch (NumberFormatException e) {\n            return -1;\n        }\n    }\n\n    public static int toInt(byte[] bytes) {\n        int result = 0;\n        byte abyte;\n        for (int i = 0; i < bytes.length; i++) {\n            abyte = bytes[i];\n            result += (abyte & 0xFF) << (8 * i);\n        }\n        return result;\n    }\n\n    public static int toShort(byte first, byte second) {\n        return (first << 8) + (second & 0xFF);\n    }\n\n    public static long toLong(Object obj) {\n        try {\n            return Long.parseLong(obj.toString());\n        } catch (NumberFormatException e) {\n            return -1L;\n        }\n    }\n\n    public static float toFloat(Object obj) {\n        try {\n            return Float.parseFloat(obj.toString());\n        } catch (NumberFormatException e) {\n            return -1f;\n        }\n    }\n\n    /**\n     * int占4字节\n     *\n     * @param i the\n     * @return byte [ ]\n     */\n    public static byte[] toByteArray(int i) {\n        // byte[] bytes = new byte[4];\n        // bytes[0] = (byte) (0xff & i);\n        // bytes[1] = (byte) ((0xff00 & i) >> 8);\n        // bytes[2] = (byte) ((0xff0000 & i) >> 16);\n        // bytes[3] = (byte) ((0xff000000 & i) >> 24);\n        // return bytes;\n        return ByteBuffer.allocate(4).putInt(i).array();\n    }\n\n    public static byte[] toByteArray(String hexData, boolean isHex) {\n        if (hexData == null || hexData.equals(\"\")) {\n            return null;\n        }\n        if (!isHex) {\n            return hexData.getBytes();\n        }\n        hexData = hexData.replaceAll(\"\\\\s+\", \"\");\n        String hexDigits = \"0123456789ABCDEF\";\n        ByteArrayOutputStream baos = new ByteArrayOutputStream(\n                hexData.length() / 2);\n        // 将每2位16进制整数组装成一个字节\n        for (int i = 0; i < hexData.length(); i += 2) {\n            baos.write((hexDigits.indexOf(hexData.charAt(i)) << 4 | hexDigits\n                    .indexOf(hexData.charAt(i + 1))));\n        }\n        byte[] bytes = baos.toByteArray();\n        try {\n            baos.close();\n        } catch (IOException e) {\n        }\n        return bytes;\n    }\n\n    public static String toHexString(String str) {\n        if (TextUtils.isEmpty(str))\n            return \"\";\n        StringBuilder builder = new StringBuilder();\n        byte[] bytes = str.getBytes();\n        for (byte aByte : bytes) {\n            builder.append(Integer.toHexString(0xFF & aByte));\n            builder.append(\" \");\n        }\n        return builder.toString();\n    }\n\n    /**\n     * To hex string string.\n     *\n     * @param bytes the bytes\n     * @return the string\n     */\n    public static String toHexString(byte... bytes) {\n        char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6',\n                '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};\n        // 参见：http://www.oschina.net/code/snippet_116768_9019\n        char[] buffer = new char[bytes.length * 2];\n        for (int i = 0, j = 0; i < bytes.length; ++i) {\n            int u = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];//转无符号整型\n            buffer[j++] = DIGITS[u >>> 4];\n            buffer[j++] = DIGITS[u & 0xf];\n        }\n        return new String(buffer);\n    }\n\n    /**\n     * To hex string string.\n     *\n     * @param num the num\n     * @return the string\n     */\n    public static String toHexString(int num) {\n        String hexString = Integer.toHexString(num);\n        return hexString;\n    }\n\n    /**\n     * To binary string string.\n     *\n     * @param bytes the bytes\n     * @return the string\n     */\n    public static String toBinaryString(byte... bytes) {\n        char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6',\n                '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};\n        // 参见：http://www.oschina.net/code/snippet_116768_9019\n        char[] buffer = new char[bytes.length * 8];\n        for (int i = 0, j = 0; i < bytes.length; ++i) {\n            int u = bytes[i] < 0 ? bytes[i] + 256 : bytes[i];//转无符号整型\n            buffer[j++] = DIGITS[(u >>> 7) & 0x1];\n            buffer[j++] = DIGITS[(u >>> 6) & 0x1];\n            buffer[j++] = DIGITS[(u >>> 5) & 0x1];\n            buffer[j++] = DIGITS[(u >>> 4) & 0x1];\n            buffer[j++] = DIGITS[(u >>> 3) & 0x1];\n            buffer[j++] = DIGITS[(u >>> 2) & 0x1];\n            buffer[j++] = DIGITS[(u >>> 1) & 0x1];\n            buffer[j++] = DIGITS[u & 0x1];\n        }\n        return new String(buffer);\n    }\n\n    /**\n     * To binary string string.\n     *\n     * @param num the num\n     * @return the string\n     */\n    public static String toBinaryString(int num) {\n        String binaryString = Integer.toBinaryString(num);\n        return binaryString;\n    }\n\n    public static String toSlashString(String str) {\n        String result = \"\";\n        char[] chars = str.toCharArray();\n        for (char chr : chars) {\n            if (chr == '\"' || chr == '\\'' || chr == '\\\\') {\n                result += \"\\\\\";//符合“\"”“'”“\\”这三个符号的前面加一个“\\”\n            }\n            result += chr;\n        }\n        return result;\n    }\n\n    public static <T> List<T> toList(T[] array) {\n        return Arrays.asList(array);\n    }\n\n    public static String toString(Object[] objects) {\n        return Arrays.deepToString(objects);\n    }\n\n    public static String toString(Object[] objects, String tag) {\n        StringBuilder sb = new StringBuilder();\n        for (Object object : objects) {\n            sb.append(object);\n            sb.append(tag);\n        }\n        return sb.toString();\n    }\n\n    public static byte[] toByteArray(InputStream is) {\n        if (is == null) {\n            return null;\n        }\n        try {\n            ByteArrayOutputStream os = new ByteArrayOutputStream();\n            byte[] buff = new byte[100];\n            while (true) {\n                int len = is.read(buff, 0, 100);\n                if (len == -1) {\n                    break;\n                } else {\n                    os.write(buff, 0, len);\n                }\n            }\n            byte[] bytes = os.toByteArray();\n            os.close();\n            is.close();\n            return bytes;\n        } catch (IOException e) {\n        }\n        return null;\n    }\n\n    public static byte[] toByteArray(Bitmap bitmap) {\n        if (bitmap == null) {\n            return null;\n        }\n        ByteArrayOutputStream os = new ByteArrayOutputStream();\n        // 将Bitmap压缩成PNG编码，质量为100%存储，除了PNG还有很多常见格式，如jpeg等。\n        bitmap.compress(Bitmap.CompressFormat.PNG, 100, os);\n        byte[] bytes = os.toByteArray();\n        try {\n            os.close();\n        } catch (IOException e) {\n        }\n        return bytes;\n    }\n\n    public static Bitmap toBitmap(byte[] bytes, int width, int height) {\n        Bitmap bitmap = null;\n        if (bytes.length != 0) {\n            try {\n                BitmapFactory.Options options = new BitmapFactory.Options();\n                // 不进行图片抖动处理\n                options.inDither = false;\n                // 设置让解码器以最佳方式解码\n                options.inPreferredConfig = null;\n                if (width > 0 && height > 0) {\n                    options.outWidth = width;\n                    options.outHeight = height;\n                }\n                bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);\n                bitmap.setDensity(96);// 96 dpi\n            } catch (Exception e) {\n            }\n        }\n        return bitmap;\n    }\n\n    public static Bitmap toBitmap(byte[] bytes) {\n        return toBitmap(bytes, -1, -1);\n    }\n\n    /**\n     * 将Drawable转换为Bitmap\n     * 参考：http://kylines.iteye.com/blog/1660184\n     */\n    public static Bitmap toBitmap(Drawable drawable) {\n        if (drawable instanceof BitmapDrawable) {\n            return ((BitmapDrawable) drawable).getBitmap();\n        } else if (drawable instanceof ColorDrawable) {\n            //color\n            Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888);\n            Canvas canvas = new Canvas(bitmap);\n            canvas.drawColor(((ColorDrawable) drawable).getColor());\n            return bitmap;\n        } else if (drawable instanceof NinePatchDrawable) {\n            //.9.png\n            Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),\n                    drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);\n            Canvas canvas = new Canvas(bitmap);\n            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());\n            drawable.draw(canvas);\n            return bitmap;\n        }\n        return null;\n    }\n\n    /**\n     * 从第三方文件选择器获取路径。\n     * 参见：http://blog.csdn.net/zbjdsbj/article/details/42387551\n     */\n    @TargetApi(Build.VERSION_CODES.KITKAT)\n    public static String toPath(Context context, Uri uri) {\n        if (uri == null) {\n            return \"\";\n        }\n        String path = uri.getPath();\n        String scheme = uri.getScheme();\n        String authority = uri.getAuthority();\n        //是否是4.4及以上版本\n        boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;\n        // DocumentProvider\n        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {\n            String docId = DocumentsContract.getDocumentId(uri);\n            String[] split = docId.split(\":\");\n            String type = split[0];\n            Uri contentUri = null;\n            switch (authority) {\n                // ExternalStorageProvider\n                case \"com.android.externalstorage.documents\":\n                    if (\"primary\".equalsIgnoreCase(type)) {\n                        return Environment.getExternalStorageDirectory() + \"/\" + split[1];\n                    }\n                    break;\n                // DownloadsProvider\n                case \"com.android.providers.downloads.documents\":\n                    contentUri = ContentUris.withAppendedId(\n                            Uri.parse(\"content://downloads/public_downloads\"), Long.valueOf(docId));\n                    return _queryPathFromMediaStore(context, contentUri, null, null);\n                // MediaProvider\n                case \"com.android.providers.media.documents\":\n                    if (\"image\".equals(type)) {\n                        contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;\n                    } else if (\"video\".equals(type)) {\n                        contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;\n                    } else if (\"audio\".equals(type)) {\n                        contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;\n                    }\n                    String selection = \"_id=?\";\n                    String[] selectionArgs = new String[]{split[1]};\n                    return _queryPathFromMediaStore(context, contentUri, selection, selectionArgs);\n            }\n        }\n        // MediaStore (and general)\n        else {\n            if (\"content\".equalsIgnoreCase(scheme)) {\n                // Return the remote address\n                if (authority.equals(\"com.google.android.apps.photos.content\")) {\n                    return uri.getLastPathSegment();\n                }\n                return _queryPathFromMediaStore(context, uri, null, null);\n            }\n            // File\n            else if (\"file\".equalsIgnoreCase(scheme)) {\n                return uri.getPath();\n            }\n        }\n        return path;\n    }\n\n    private static String _queryPathFromMediaStore(Context context, Uri uri, String selection, String[] selectionArgs) {\n        String filePath = null;\n        try {\n            String[] projection = {MediaStore.Images.Media.DATA};\n            Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);\n            if (cursor != null) {\n                int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);\n                cursor.moveToFirst();\n                filePath = cursor.getString(column_index);\n                cursor.close();\n            }\n        } catch (IllegalArgumentException e) {\n        }\n        return filePath;\n    }\n\n    /**\n     * 把view转化为bitmap（截图）\n     * 参见：http://www.cnblogs.com/lee0oo0/p/3355468.html\n     */\n    public static Bitmap toBitmap(View view) {\n        int width = view.getWidth();\n        int height = view.getHeight();\n        if (view instanceof ListView) {\n            height = 0;\n            // 获取listView实际高度\n            ListView listView = (ListView) view;\n            for (int i = 0; i < listView.getChildCount(); i++) {\n                height += listView.getChildAt(i).getHeight();\n            }\n        } else if (view instanceof ScrollView) {\n            height = 0;\n            // 获取scrollView实际高度\n            ScrollView scrollView = (ScrollView) view;\n            for (int i = 0; i < scrollView.getChildCount(); i++) {\n                height += scrollView.getChildAt(i).getHeight();\n            }\n        }\n        view.setDrawingCacheEnabled(true);\n        view.clearFocus();\n        view.setPressed(false);\n        boolean willNotCache = view.willNotCacheDrawing();\n        view.setWillNotCacheDrawing(false);\n        // Reset the drawing cache background color to fully transparent for the duration of this operation\n        int color = view.getDrawingCacheBackgroundColor();\n        view.setDrawingCacheBackgroundColor(Color.WHITE);//截图去黑色背景(透明像素)\n        if (color != Color.WHITE) {\n            view.destroyDrawingCache();\n        }\n        view.buildDrawingCache();\n        Bitmap cacheBitmap = view.getDrawingCache();\n        if (cacheBitmap == null) {\n            return null;\n        }\n        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);\n        Canvas canvas = new Canvas(bitmap);\n        canvas.drawBitmap(cacheBitmap, 0, 0, null);\n        canvas.save();\n        canvas.restore();\n        if (!bitmap.isRecycled()) {\n            bitmap.recycle();\n        }\n        // Restore the view\n        view.destroyDrawingCache();\n        view.setWillNotCacheDrawing(willNotCache);\n        view.setDrawingCacheBackgroundColor(color);\n        return bitmap;\n    }\n\n    public static Drawable toDrawable(Bitmap bitmap) {\n        return bitmap == null ? null : new BitmapDrawable(Resources.getSystem(), bitmap);\n    }\n\n    public static byte[] toByteArray(Drawable drawable) {\n        return toByteArray(toBitmap(drawable));\n    }\n\n    public static Drawable toDrawable(byte[] bytes) {\n        return toDrawable(toBitmap(bytes));\n    }\n\n    /**\n     * dp转换为px\n     */\n    public static int toPx(Context context, float dpValue) {\n        final float scale = context.getResources().getDisplayMetrics().density;\n        int pxValue = (int) (dpValue * scale + 0.5f);\n        return pxValue;\n    }\n\n    /**\n     * px转换为dp\n     */\n    public static int toDp(Context context, float pxValue) {\n        final float scale = context.getResources().getDisplayMetrics().density;\n        int dpValue = (int) (pxValue / scale + 0.5f);\n        return dpValue;\n    }\n\n    /**\n     * px转换为sp\n     */\n    public static int toSp(Context context, float pxValue) {\n        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;\n        int spValue = (int) (pxValue / fontScale + 0.5f);\n        return spValue;\n    }\n\n    public static String toGbk(String str) {\n        try {\n            return new String(str.getBytes(\"utf-8\"), \"gbk\");\n        } catch (UnsupportedEncodingException e) {\n            return str;\n        }\n    }\n\n    public static String toFileSizeString(long fileSize) {\n        DecimalFormat df = new DecimalFormat(\"0.00\");\n        String fileSizeString;\n        if (fileSize < KB) {\n            fileSizeString = fileSize + \"B\";\n        } else if (fileSize < MB) {\n            fileSizeString = df.format((double) fileSize / KB) + \"K\";\n        } else if (fileSize < GB) {\n            fileSizeString = df.format((double) fileSize / MB) + \"M\";\n        } else {\n            fileSizeString = df.format((double) fileSize / GB) + \"G\";\n        }\n        return fileSizeString;\n    }\n\n    public static String toString(InputStream is, String charset) {\n        StringBuilder sb = new StringBuilder();\n        try {\n            BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset));\n            while (true) {\n                String line = reader.readLine();\n                if (line == null) {\n                    break;\n                } else {\n                    sb.append(line).append(\"\\n\");\n                }\n            }\n            reader.close();\n            is.close();\n        } catch (IOException e) {\n        }\n        return sb.toString();\n    }\n\n    public static String toString(InputStream is) {\n        return toString(is, \"utf-8\");\n    }\n\n    public static int toDarkenColor(@ColorInt int color) {\n        return toDarkenColor(color, 0.8f);\n    }\n\n    public static int toDarkenColor(@ColorInt int color, @FloatRange(from = 0f, to = 1f) float value) {\n        float[] hsv = new float[3];\n        Color.colorToHSV(color, hsv);\n        hsv[2] *= value;//HSV指Hue、Saturation、Value，即色调、饱和度和亮度，此处表示修改亮度\n        return Color.HSVToColor(hsv);\n    }\n\n    /**\n     * 转换为6位十六进制颜色代码，不含“#”\n     */\n    public static String toColorString(@ColorInt int color) {\n        return toColorString(color, false);\n    }\n\n    /**\n     * 转换为6位十六进制颜色代码，不含“#”\n     */\n    public static String toColorString(@ColorInt int color, boolean includeAlpha) {\n        String alpha = Integer.toHexString(Color.alpha(color));\n        String red = Integer.toHexString(Color.red(color));\n        String green = Integer.toHexString(Color.green(color));\n        String blue = Integer.toHexString(Color.blue(color));\n        if (alpha.length() == 1) {\n            alpha = \"0\" + alpha;\n        }\n        if (red.length() == 1) {\n            red = \"0\" + red;\n        }\n        if (green.length() == 1) {\n            green = \"0\" + green;\n        }\n        if (blue.length() == 1) {\n            blue = \"0\" + blue;\n        }\n        String colorString;\n        if (includeAlpha) {\n            colorString = alpha + red + green + blue;\n        } else {\n            colorString = red + green + blue;\n        }\n        return colorString;\n    }\n\n    /**\n     * 对TextView、Button等设置不同状态时其文字颜色。\n     * 参见：http://blog.csdn.net/sodino/article/details/6797821\n     * Modified by liyujiang at 2015.08.13\n     */\n    public static ColorStateList toColorStateList(@ColorInt int normalColor, @ColorInt int pressedColor,\n                                                  @ColorInt int focusedColor, @ColorInt int unableColor) {\n        int[] colors = new int[]{pressedColor, focusedColor, normalColor, focusedColor, unableColor, normalColor};\n        int[][] states = new int[6][];\n        states[0] = new int[]{android.R.attr.state_pressed, android.R.attr.state_enabled};\n        states[1] = new int[]{android.R.attr.state_enabled, android.R.attr.state_focused};\n        states[2] = new int[]{android.R.attr.state_enabled};\n        states[3] = new int[]{android.R.attr.state_focused};\n        states[4] = new int[]{android.R.attr.state_window_focused};\n        states[5] = new int[]{};\n        return new ColorStateList(states, colors);\n    }\n\n    public static ColorStateList toColorStateList(@ColorInt int normalColor, @ColorInt int pressedColor) {\n        return toColorStateList(normalColor, pressedColor, pressedColor, normalColor);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/DateUtils.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.util;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Arrays;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Locale;\n\n/**\n * 日期时间工具类\n *\n * @author 李玉江[QQ:1023694760]\n * @since 2015/8/5\n */\npublic class DateUtils extends android.text.format.DateUtils {\n    public static final int Second = 0;\n    public static final int Minute = 1;\n    public static final int Hour = 2;\n    public static final int Day = 3;\n\n    @IntDef(value = {Second, Minute, Hour, Day})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface DifferenceMode {\n    }\n\n    public static long calculateDifferentSecond(Date startDate, Date endDate) {\n        return calculateDifference(startDate, endDate, Second);\n    }\n\n    public static long calculateDifferentMinute(Date startDate, Date endDate) {\n        return calculateDifference(startDate, endDate, Minute);\n    }\n\n    public static long calculateDifferentHour(Date startDate, Date endDate) {\n        return calculateDifference(startDate, endDate, Hour);\n    }\n\n    public static long calculateDifferentDay(Date startDate, Date endDate) {\n        return calculateDifference(startDate, endDate, Day);\n    }\n\n    public static long calculateDifferentSecond(long startTimeMillis, long endTimeMillis) {\n        return calculateDifference(startTimeMillis, endTimeMillis, Second);\n    }\n\n    public static long calculateDifferentMinute(long startTimeMillis, long endTimeMillis) {\n        return calculateDifference(startTimeMillis, endTimeMillis, Minute);\n    }\n\n    public static long calculateDifferentHour(long startTimeMillis, long endTimeMillis) {\n        return calculateDifference(startTimeMillis, endTimeMillis, Hour);\n    }\n\n    public static long calculateDifferentDay(long startTimeMillis, long endTimeMillis) {\n        return calculateDifference(startTimeMillis, endTimeMillis, Day);\n    }\n\n    /**\n     * 计算两个时间戳之间相差的时间戳数\n     */\n    public static long calculateDifference(long startTimeMillis, long endTimeMillis, @DifferenceMode int mode) {\n        return calculateDifference(new Date(startTimeMillis), new Date(endTimeMillis), mode);\n    }\n\n    /**\n     * 计算两个日期之间相差的时间戳数\n     */\n    public static long calculateDifference(Date startDate, Date endDate, @DifferenceMode int mode) {\n        long[] different = calculateDifference(startDate, endDate);\n        if (mode == Minute) {\n            return different[2];\n        } else if (mode == Hour) {\n            return different[1];\n        } else if (mode == Day) {\n            return different[0];\n        } else {\n            return different[3];\n        }\n    }\n\n    private static long[] calculateDifference(Date startDate, Date endDate) {\n        return calculateDifference(endDate.getTime() - startDate.getTime());\n    }\n\n    private static long[] calculateDifference(long differentMilliSeconds) {\n        long secondsInMilli = 1000;//1s==1000ms\n        long minutesInMilli = secondsInMilli * 60;\n        long hoursInMilli = minutesInMilli * 60;\n        long daysInMilli = hoursInMilli * 24;\n        long elapsedDays = differentMilliSeconds / daysInMilli;\n        differentMilliSeconds = differentMilliSeconds % daysInMilli;\n        long elapsedHours = differentMilliSeconds / hoursInMilli;\n        differentMilliSeconds = differentMilliSeconds % hoursInMilli;\n        long elapsedMinutes = differentMilliSeconds / minutesInMilli;\n        differentMilliSeconds = differentMilliSeconds % minutesInMilli;\n        long elapsedSeconds = differentMilliSeconds / secondsInMilli;\n        return new long[]{elapsedDays, elapsedHours, elapsedMinutes, elapsedSeconds};\n    }\n\n    /**\n     * 计算每月的天数\n     */\n    public static int calculateDaysInMonth(int month) {\n        return calculateDaysInMonth(0, month);\n    }\n\n    /**\n     * 根据年份及月份计算每月的天数\n     */\n    public static int calculateDaysInMonth(int year, int month) {\n        // 添加大小月月份并将其转换为list,方便之后的判断\n        String[] bigMonths = {\"1\", \"3\", \"5\", \"7\", \"8\", \"10\", \"12\"};\n        String[] littleMonths = {\"4\", \"6\", \"9\", \"11\"};\n        List<String> bigList = Arrays.asList(bigMonths);\n        List<String> littleList = Arrays.asList(littleMonths);\n        // 判断大小月及是否闰年,用来确定\"日\"的数据\n        if (bigList.contains(String.valueOf(month))) {\n            return 31;\n        } else if (littleList.contains(String.valueOf(month))) {\n            return 30;\n        } else {\n            if (year <= 0) {\n                return 29;\n            }\n            // 是否闰年\n            if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {\n                return 29;\n            } else {\n                return 28;\n            }\n        }\n    }\n\n    /**\n     * 月日时分秒，0-9前补0\n     */\n    @NonNull\n    public static String fillZero(int number) {\n        return number < 10 ? \"0\" + number : \"\" + number;\n    }\n\n    /**\n     * 截取掉前缀0以便转换为整数\n     *\n     * @see #fillZero(int)\n     */\n    public static int trimZero(@NonNull String text) {\n        try {\n            if (text.startsWith(\"0\")) {\n                text = text.substring(1);\n            }\n            return Integer.parseInt(text);\n        } catch (NumberFormatException e) {\n            return 0;\n        }\n    }\n\n    /**\n     * 功能：判断日期是否和当前date对象在同一天。\n     * 参见：http://www.cnblogs.com/myzhijie/p/3330970.html\n     *\n     * @param date 比较的日期\n     * @return boolean 如果在返回true，否则返回false。\n     */\n    public static boolean isSameDay(Date date) {\n        if (date == null) {\n            throw new IllegalArgumentException(\"date is null\");\n        }\n        Calendar nowCalendar = Calendar.getInstance();\n        Calendar newCalendar = Calendar.getInstance();\n        newCalendar.setTime(date);\n        return (nowCalendar.get(Calendar.ERA) == newCalendar.get(Calendar.ERA) &&\n                nowCalendar.get(Calendar.YEAR) == newCalendar.get(Calendar.YEAR) &&\n                nowCalendar.get(Calendar.DAY_OF_YEAR) == newCalendar.get(Calendar.DAY_OF_YEAR));\n    }\n\n    /**\n     * 将yyyy-MM-dd HH:mm:ss字符串转换成日期<br/>\n     *\n     * @param dateStr    时间字符串\n     * @param dataFormat 当前时间字符串的格式。\n     * @return Date 日期 ,转换异常时返回null。\n     */\n    public static Date parseDate(String dateStr, String dataFormat) {\n        try {\n            SimpleDateFormat dateFormat = new SimpleDateFormat(dataFormat, Locale.PRC);\n            Date date = dateFormat.parse(dateStr);\n            return new Date(date.getTime());\n        } catch (ParseException e) {\n            return null;\n        }\n    }\n\n    /**\n     * 将yyyy-MM-dd HH:mm:ss字符串转换成日期<br/>\n     *\n     * @param dateStr yyyy-MM-dd HH:mm:ss字符串\n     * @return Date 日期 ,转换异常时返回null。\n     */\n    public static Date parseDate(String dateStr) {\n        return parseDate(dateStr, \"yyyy-MM-dd HH:mm:ss\");\n    }\n\n    /**\n     * 将指定的日期转换为一定格式的字符串\n     */\n    public static String formatDate(Date date, String format) {\n        SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.PRC);\n        return sdf.format(date);\n    }\n\n    /**\n     * 将当前日期转换为一定格式的字符串\n     */\n    public static String formatDate(String format) {\n        return formatDate(Calendar.getInstance(Locale.CHINA).getTime(), format);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/FileUtils.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.util;\n\nimport android.webkit.MimeTypeMap;\n\nimport androidx.annotation.IntDef;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.File;\nimport java.io.FileFilter;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.FileWriter;\nimport java.io.FilenameFilter;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.Calendar;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.regex.Pattern;\n\n/**\n * 文件处理\n * <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n * <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n *\n * @author 李玉江[QQ:1023694760]\n * @since 2014-4-18\n */\npublic final class FileUtils {\n    public static final int BY_NAME_ASC = 0;\n    public static final int BY_NAME_DESC = 1;\n    public static final int BY_TIME_ASC = 2;\n    public static final int BY_TIME_DESC = 3;\n    public static final int BY_SIZE_ASC = 4;\n    public static final int BY_SIZE_DESC = 5;\n    public static final int BY_EXTENSION_ASC = 6;\n    public static final int BY_EXTENSION_DESC = 7;\n\n    @IntDef(value = {\n            BY_NAME_ASC,\n            BY_NAME_DESC,\n            BY_TIME_ASC,\n            BY_TIME_DESC,\n            BY_SIZE_ASC,\n            BY_SIZE_DESC,\n            BY_EXTENSION_ASC,\n            BY_EXTENSION_DESC\n    })\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface SortType {\n    }\n\n    /**\n     * 将目录分隔符统一为平台默认的分隔符，并为目录结尾添加分隔符\n     */\n    public static String separator(String path) {\n        String separator = File.separator;\n        path = path.replace(\"\\\\\", separator);\n        if (!path.endsWith(separator)) {\n            path += separator;\n        }\n        return path;\n    }\n\n    public static void closeSilently(Closeable c) {\n        if (c == null) {\n            return;\n        }\n        try {\n            c.close();\n        } catch (IOException ignored) {\n        }\n    }\n\n    /**\n     * 列出指定目录下的所有子目录\n     */\n    public static File[] listDirs(String startDirPath, String[] excludeDirs, @SortType int sortType) {\n        ArrayList<File> dirList = new ArrayList<File>();\n        File startDir = new File(startDirPath);\n        if (!startDir.isDirectory()) {\n            return new File[0];\n        }\n        File[] dirs = startDir.listFiles(new FileFilter() {\n            public boolean accept(File f) {\n                if (f == null) {\n                    return false;\n                }\n                //noinspection RedundantIfStatement\n                if (f.isDirectory()) {\n                    return true;\n                }\n                return false;\n            }\n        });\n        if (dirs == null) {\n            return new File[0];\n        }\n        if (excludeDirs == null) {\n            excludeDirs = new String[0];\n        }\n        for (File dir : dirs) {\n            File file = dir.getAbsoluteFile();\n            if (!ConvertUtils.toString(excludeDirs).contains(file.getName())) {\n                dirList.add(file);\n            }\n        }\n        if (sortType == BY_NAME_ASC) {\n            Collections.sort(dirList, new SortByName());\n        } else if (sortType == BY_NAME_DESC) {\n            Collections.sort(dirList, new SortByName());\n            Collections.reverse(dirList);\n        } else if (sortType == BY_TIME_ASC) {\n            Collections.sort(dirList, new SortByTime());\n        } else if (sortType == BY_TIME_DESC) {\n            Collections.sort(dirList, new SortByTime());\n            Collections.reverse(dirList);\n        } else if (sortType == BY_SIZE_ASC) {\n            Collections.sort(dirList, new SortBySize());\n        } else if (sortType == BY_SIZE_DESC) {\n            Collections.sort(dirList, new SortBySize());\n            Collections.reverse(dirList);\n        } else if (sortType == BY_EXTENSION_ASC) {\n            Collections.sort(dirList, new SortByExtension());\n        } else if (sortType == BY_EXTENSION_DESC) {\n            Collections.sort(dirList, new SortByExtension());\n            Collections.reverse(dirList);\n        }\n        return dirList.toArray(new File[dirList.size()]);\n    }\n\n    /**\n     * 列出指定目录下的所有子目录\n     */\n    public static File[] listDirs(String startDirPath, String[] excludeDirs) {\n        return listDirs(startDirPath, excludeDirs, BY_NAME_ASC);\n    }\n\n    /**\n     * 列出指定目录下的所有子目录\n     */\n    public static File[] listDirs(String startDirPath) {\n        return listDirs(startDirPath, null, BY_NAME_ASC);\n    }\n\n    /**\n     * 列出指定目录下的所有子目录及所有文件\n     */\n    public static File[] listDirsAndFiles(String startDirPath, String[] allowExtensions) {\n        File[] dirs, files, dirsAndFiles;\n        dirs = listDirs(startDirPath);\n        if (allowExtensions == null) {\n            files = listFiles(startDirPath);\n        } else {\n            files = listFiles(startDirPath, allowExtensions);\n        }\n        if (dirs == null || files == null) {\n            return null;\n        }\n        dirsAndFiles = new File[dirs.length + files.length];\n        System.arraycopy(dirs, 0, dirsAndFiles, 0, dirs.length);\n        System.arraycopy(files, 0, dirsAndFiles, dirs.length, files.length);\n        return dirsAndFiles;\n    }\n\n    /**\n     * 列出指定目录下的所有子目录及所有文件\n     */\n    public static File[] listDirsAndFiles(String startDirPath) {\n        return listDirsAndFiles(startDirPath, null);\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    public static File[] listFiles(String startDirPath, final Pattern filterPattern, @SortType int sortType) {\n        ArrayList<File> fileList = new ArrayList<File>();\n        File f = new File(startDirPath);\n        if (!f.isDirectory()) {\n            return new File[0];\n        }\n        File[] files = f.listFiles(new FileFilter() {\n            public boolean accept(File f) {\n                if (f == null) {\n                    return false;\n                }\n                if (f.isDirectory()) {\n                    return false;\n                }\n                //noinspection SimplifiableIfStatement\n                if (filterPattern == null) {\n                    return true;\n                }\n                return filterPattern.matcher(f.getName()).find();\n            }\n        });\n        if (files == null) {\n            return new File[0];\n        }\n        for (File file : files) {\n            fileList.add(file.getAbsoluteFile());\n        }\n        if (sortType == BY_NAME_ASC) {\n            Collections.sort(fileList, new SortByName());\n        } else if (sortType == BY_NAME_DESC) {\n            Collections.sort(fileList, new SortByName());\n            Collections.reverse(fileList);\n        } else if (sortType == BY_TIME_ASC) {\n            Collections.sort(fileList, new SortByTime());\n        } else if (sortType == BY_TIME_DESC) {\n            Collections.sort(fileList, new SortByTime());\n            Collections.reverse(fileList);\n        } else if (sortType == BY_SIZE_ASC) {\n            Collections.sort(fileList, new SortBySize());\n        } else if (sortType == BY_SIZE_DESC) {\n            Collections.sort(fileList, new SortBySize());\n            Collections.reverse(fileList);\n        } else if (sortType == BY_EXTENSION_ASC) {\n            Collections.sort(fileList, new SortByExtension());\n        } else if (sortType == BY_EXTENSION_DESC) {\n            Collections.sort(fileList, new SortByExtension());\n            Collections.reverse(fileList);\n        }\n        return fileList.toArray(new File[fileList.size()]);\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    public static File[] listFiles(String startDirPath, Pattern filterPattern) {\n        return listFiles(startDirPath, filterPattern, BY_NAME_ASC);\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    public static File[] listFiles(String startDirPath) {\n        return listFiles(startDirPath, null, BY_NAME_ASC);\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    public static File[] listFiles(String startDirPath, final String[] allowExtensions) {\n        File file = new File(startDirPath);\n        return file.listFiles(new FilenameFilter() {\n\n            @Override\n            public boolean accept(File dir, String name) {\n                //返回当前目录所有以某些扩展名结尾的文件\n                String extension = FileUtils.getExtension(name);\n                return ConvertUtils.toString(allowExtensions).contains(extension);\n            }\n\n        });\n    }\n\n    /**\n     * 列出指定目录下的所有文件\n     */\n    public static File[] listFiles(String startDirPath, String allowExtension) {\n        return listFiles(startDirPath, new String[]{allowExtension});\n    }\n\n    /**\n     * 判断文件或目录是否存在\n     */\n    public static boolean exist(String path) {\n        File file = new File(path);\n        return file.exists();\n    }\n\n    /**\n     * 删除文件或目录\n     */\n    public static boolean delete(File file, boolean deleteRootDir) {\n        boolean result = false;\n        if (file.isFile()) {\n            //是文件\n            result = deleteResolveEBUSY(file);\n        } else {\n            //是目录\n            File[] files = file.listFiles();\n            if (files == null) {\n                return false;\n            }\n            if (files.length == 0) {\n                result = deleteRootDir && deleteResolveEBUSY(file);\n            } else {\n                for (File f : files) {\n                    delete(f, deleteRootDir);\n                    result = deleteResolveEBUSY(f);\n                }\n            }\n            if (deleteRootDir) {\n                result = deleteResolveEBUSY(file);\n            }\n        }\n        return result;\n    }\n\n    /**\n     * bug: open failed: EBUSY (Device or resource busy)\n     * fix: http://stackoverflow.com/questions/11539657/open-failed-ebusy-device-or-resource-busy\n     */\n    private static boolean deleteResolveEBUSY(File file) {\n        // Before you delete a Directory or File: rename it!\n        final File to = new File(file.getAbsolutePath() + System.currentTimeMillis());\n        //noinspection ResultOfMethodCallIgnored\n        file.renameTo(to);\n        return to.delete();\n    }\n\n    /**\n     * 删除文件或目录\n     */\n    public static boolean delete(String path, boolean deleteRootDir) {\n        File file = new File(path);\n        //noinspection SimplifiableIfStatement\n        if (file.exists()) {\n            return delete(file, deleteRootDir);\n        }\n        return false;\n    }\n\n    /**\n     * 删除文件或目录, 不删除最顶层目录\n     */\n    public static boolean delete(String path) {\n        return delete(path, false);\n    }\n\n    /**\n     * 删除文件或目录, 不删除最顶层目录\n     */\n    public static boolean delete(File file) {\n        return delete(file, false);\n    }\n\n    /**\n     * 复制文件为另一个文件，或复制某目录下的所有文件及目录到另一个目录下\n     */\n    public static boolean copy(String src, String tar) {\n        File srcFile = new File(src);\n        return srcFile.exists() && copy(srcFile, new File(tar));\n    }\n\n    /**\n     * 复制文件或目录\n     */\n    public static boolean copy(File src, File tar) {\n        try {\n            if (src.isFile()) {\n                InputStream is = new FileInputStream(src);\n                OutputStream op = new FileOutputStream(tar);\n                BufferedInputStream bis = new BufferedInputStream(is);\n                BufferedOutputStream bos = new BufferedOutputStream(op);\n                byte[] bt = new byte[1024 * 8];\n                while (true) {\n                    int len = bis.read(bt);\n                    if (len == -1) {\n                        break;\n                    } else {\n                        bos.write(bt, 0, len);\n                    }\n                }\n                bis.close();\n                bos.close();\n            } else if (src.isDirectory()) {\n                File[] files = src.listFiles();\n                //noinspection ResultOfMethodCallIgnored\n                tar.mkdirs();\n                for (File file : files) {\n                    copy(file.getAbsoluteFile(), new File(tar.getAbsoluteFile(), file.getName()));\n                }\n            }\n            return true;\n        } catch (Exception e) {\n            return false;\n        }\n    }\n\n    /**\n     * 移动文件或目录\n     */\n    public static boolean move(String src, String tar) {\n        return move(new File(src), new File(tar));\n    }\n\n    /**\n     * 移动文件或目录\n     */\n    public static boolean move(File src, File tar) {\n        return rename(src, tar);\n    }\n\n    /**\n     * 文件重命名\n     */\n    public static boolean rename(String oldPath, String newPath) {\n        return rename(new File(oldPath), new File(newPath));\n    }\n\n    /**\n     * 文件重命名\n     */\n    public static boolean rename(File src, File tar) {\n        return src.renameTo(tar);\n    }\n\n    /**\n     * 读取文本文件, 失败将返回空串\n     */\n    public static String readText(String filepath, String charset) {\n        try {\n            byte[] data = readBytes(filepath);\n            if (data != null) {\n                return new String(data, charset).trim();\n            }\n        } catch (UnsupportedEncodingException ignored) {\n        }\n        return \"\";\n    }\n\n    /**\n     * 读取文本文件, 失败将返回空串\n     */\n    public static String readText(String filepath) {\n        return readText(filepath, \"utf-8\");\n    }\n\n    /**\n     * 读取文件内容, 失败将返回空串\n     */\n    public static byte[] readBytes(String filepath) {\n        FileInputStream fis = null;\n        try {\n            fis = new FileInputStream(filepath);\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            byte[] buffer = new byte[1024];\n            while (true) {\n                int len = fis.read(buffer, 0, buffer.length);\n                if (len == -1) {\n                    break;\n                } else {\n                    baos.write(buffer, 0, len);\n                }\n            }\n            byte[] data = baos.toByteArray();\n            baos.close();\n            return data;\n        } catch (IOException e) {\n            return null;\n        } finally {\n            closeSilently(fis);\n        }\n    }\n\n    /**\n     * 保存文本内容\n     */\n    public static boolean writeText(String filepath, String content, String charset) {\n        try {\n            writeBytes(filepath, content.getBytes(charset));\n            return true;\n        } catch (UnsupportedEncodingException e) {\n            return false;\n        }\n    }\n\n    /**\n     * 保存文本内容\n     */\n    public static boolean writeText(String filepath, String content) {\n        return writeText(filepath, content, \"utf-8\");\n    }\n\n    /**\n     * 保存文件内容\n     */\n    public static boolean writeBytes(String filepath, byte[] data) {\n        File file = new File(filepath);\n        FileOutputStream fos = null;\n        try {\n            if (!file.exists()) {\n                //noinspection ResultOfMethodCallIgnored\n                file.getParentFile().mkdirs();\n                //noinspection ResultOfMethodCallIgnored\n                file.createNewFile();\n            }\n            fos = new FileOutputStream(filepath);\n            fos.write(data);\n            return true;\n        } catch (IOException e) {\n            return false;\n        } finally {\n            closeSilently(fos);\n        }\n    }\n\n    /**\n     * 追加文本内容\n     */\n    public static boolean appendText(String path, String content) {\n        File file = new File(path);\n        FileWriter writer = null;\n        try {\n            if (!file.exists()) {\n                //noinspection ResultOfMethodCallIgnored\n                file.createNewFile();\n            }\n            writer = new FileWriter(file, true);\n            writer.write(content);\n            return true;\n        } catch (IOException e) {\n            return false;\n        } finally {\n            closeSilently(writer);\n        }\n    }\n\n    /**\n     * 获取文件大小\n     */\n    public static long getLength(String path) {\n        File file = new File(path);\n        if (!file.isFile() || !file.exists()) {\n            return 0;\n        }\n        return file.length();\n    }\n\n    /**\n     * 获取文件或网址的名称（包括后缀）\n     */\n    public static String getName(String pathOrUrl) {\n        if (pathOrUrl == null) {\n            return \"\";\n        }\n        int pos = pathOrUrl.lastIndexOf('/');\n        if (0 <= pos) {\n            return pathOrUrl.substring(pos + 1);\n        } else {\n            return String.valueOf(System.currentTimeMillis()) + \".\" + getExtension(pathOrUrl);\n        }\n    }\n\n    /**\n     * 获取文件名（不包括扩展名）\n     */\n    public static String getNameExcludeExtension(String path) {\n        try {\n            String fileName = (new File(path)).getName();\n            int lastIndexOf = fileName.lastIndexOf(\".\");\n            if (lastIndexOf != -1) {\n                fileName = fileName.substring(0, lastIndexOf);\n            }\n            return fileName;\n        } catch (Exception e) {\n            return \"\";\n        }\n    }\n\n    /**\n     * 获取格式化后的文件大小\n     */\n    public static String getSize(String path) {\n        long fileSize = getLength(path);\n        return ConvertUtils.toFileSizeString(fileSize);\n    }\n\n    /**\n     * 获取文件后缀,不包括“.”\n     */\n    public static String getExtension(String pathOrUrl) {\n        int dotPos = pathOrUrl.lastIndexOf('.');\n        if (0 <= dotPos) {\n            return pathOrUrl.substring(dotPos + 1);\n        } else {\n            return \"ext\";\n        }\n    }\n\n    /**\n     * 获取文件的MIME类型\n     */\n    public static String getMimeType(String pathOrUrl) {\n        String ext = getExtension(pathOrUrl);\n        MimeTypeMap map = MimeTypeMap.getSingleton();\n        String mimeType;\n        if (map.hasExtension(ext)) {\n            mimeType = map.getMimeTypeFromExtension(ext);\n        } else {\n            mimeType = \"*/*\";\n        }\n        return mimeType;\n    }\n\n    /**\n     * 获取格式化后的文件/目录创建或最后修改时间\n     */\n    public static String getDateTime(String path) {\n        return getDateTime(path, \"yyyy年MM月dd日HH:mm\");\n    }\n\n    /**\n     * 获取格式化后的文件/目录创建或最后修改时间\n     */\n    public static String getDateTime(String path, String format) {\n        File file = new File(path);\n        return getDateTime(file, format);\n    }\n\n    /**\n     * 获取格式化后的文件/目录创建或最后修改时间\n     */\n    public static String getDateTime(File file, String format) {\n        Calendar cal = Calendar.getInstance();\n        cal.setTimeInMillis(file.lastModified());\n        return DateUtils.formatDate(cal.getTime(), format);\n    }\n\n    /**\n     * 比较两个文件的最后修改时间\n     */\n    public static int compareLastModified(String path1, String path2) {\n        long stamp1 = (new File(path1)).lastModified();\n        long stamp2 = (new File(path2)).lastModified();\n        if (stamp1 > stamp2) {\n            return 1;\n        } else if (stamp1 < stamp2) {\n            return -1;\n        } else {\n            return 0;\n        }\n    }\n\n    /**\n     * 创建多级别的目录\n     */\n    public static boolean makeDirs(String path) {\n        return makeDirs(new File(path));\n    }\n\n    /**\n     * 创建多级别的目录\n     */\n    public static boolean makeDirs(File file) {\n        return file.mkdirs();\n    }\n\n    public static class SortByExtension implements Comparator<File> {\n\n        public SortByExtension() {\n            super();\n        }\n\n        public int compare(File f1, File f2) {\n            if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    return -1;\n                } else {\n                    return 1;\n                }\n            } else {\n                if (f1.isDirectory() && f2.isFile()) {\n                    return -1;\n                } else if (f1.isFile() && f2.isDirectory()) {\n                    return 1;\n                } else {\n                    return f1.getName().compareToIgnoreCase(f2.getName());\n                }\n            }\n        }\n\n    }\n\n    public static class SortByName implements Comparator<File> {\n        private boolean caseSensitive;\n\n        public SortByName(boolean caseSensitive) {\n            this.caseSensitive = caseSensitive;\n        }\n\n        public SortByName() {\n            this.caseSensitive = false;\n        }\n\n        public int compare(File f1, File f2) {\n            if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    return -1;\n                } else {\n                    return 1;\n                }\n            } else {\n                if (f1.isDirectory() && f2.isFile()) {\n                    return -1;\n                } else if (f1.isFile() && f2.isDirectory()) {\n                    return 1;\n                } else {\n                    String s1 = f1.getName();\n                    String s2 = f2.getName();\n                    if (caseSensitive) {\n                        return s1.compareTo(s2);\n                    } else {\n                        return s1.compareToIgnoreCase(s2);\n                    }\n                }\n            }\n        }\n\n    }\n\n    public static class SortBySize implements Comparator<File> {\n\n        public SortBySize() {\n            super();\n        }\n\n        public int compare(File f1, File f2) {\n            if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    return -1;\n                } else {\n                    return 1;\n                }\n            } else {\n                if (f1.isDirectory() && f2.isFile()) {\n                    return -1;\n                } else if (f1.isFile() && f2.isDirectory()) {\n                    return 1;\n                } else {\n                    if (f1.length() < f2.length()) {\n                        return -1;\n                    } else {\n                        return 1;\n                    }\n                }\n            }\n        }\n\n    }\n\n    public static class SortByTime implements Comparator<File> {\n\n        public SortByTime() {\n            super();\n        }\n\n        public int compare(File f1, File f2) {\n            if (f1 == null || f2 == null) {\n                if (f1 == null) {\n                    return -1;\n                } else {\n                    return 1;\n                }\n            } else {\n                if (f1.isDirectory() && f2.isFile()) {\n                    return -1;\n                } else if (f1.isFile() && f2.isDirectory()) {\n                    return 1;\n                } else {\n                    if (f1.lastModified() > f2.lastModified()) {\n                        return -1;\n                    } else {\n                        return 1;\n                    }\n                }\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/ScreenUtils.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.util;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.DisplayMetrics;\nimport android.view.Window;\nimport android.view.WindowManager;\n\n/**\n * 获取屏幕宽高等信息、全屏切换、保持屏幕常亮、截屏等\n *\n * @author liyujiang[QQ:1032694760]\n * @since 2015/11/26\n */\npublic final class ScreenUtils {\n    private static boolean isFullScreen = false;\n    private static DisplayMetrics dm = null;\n\n    public static DisplayMetrics displayMetrics(Context context) {\n        if (null != dm) {\n            return dm;\n        }\n        DisplayMetrics dm = new DisplayMetrics();\n        WindowManager windowManager = (WindowManager) context\n                .getSystemService(Context.WINDOW_SERVICE);\n        windowManager.getDefaultDisplay().getMetrics(dm);\n        return dm;\n    }\n\n    public static int widthPixels(Context context) {\n        return displayMetrics(context).widthPixels;\n    }\n\n    public static int heightPixels(Context context) {\n        return displayMetrics(context).heightPixels;\n    }\n\n    public static float density(Context context) {\n        return displayMetrics(context).density;\n    }\n\n    public static int densityDpi(Context context) {\n        return displayMetrics(context).densityDpi;\n    }\n\n    public static boolean isFullScreen() {\n        return isFullScreen;\n    }\n\n    public static void toggleFullScreen(Activity activity) {\n        Window window = activity.getWindow();\n        int flagFullscreen = WindowManager.LayoutParams.FLAG_FULLSCREEN;\n        if (isFullScreen) {\n            window.clearFlags(flagFullscreen);\n            isFullScreen = false;\n        } else {\n            window.setFlags(flagFullscreen, flagFullscreen);\n            isFullScreen = true;\n        }\n    }\n\n    /**\n     * 保持屏幕常亮\n     */\n    public static void keepBright(Activity activity) {\n        //需在setContentView前调用\n        int keepScreenOn = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;\n        activity.getWindow().setFlags(keepScreenOn, keepScreenOn);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/filepicker/util/StorageUtils.java",
    "content": "package com.kunfei.bookshelf.widget.filepicker.util;\n\nimport android.content.Context;\nimport android.os.Environment;\nimport android.text.TextUtils;\n\nimport java.io.File;\nimport java.io.IOException;\n\n/**\n * 存储设备工具类\n * <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n * <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n *\n * @author 李玉江[QQ:1023694760]\n * @since 2013-11-2\n */\npublic final class StorageUtils {\n\n    /**\n     * 判断外置存储是否可用\n     *\n     * @return the boolean\n     */\n    public static boolean externalMounted() {\n        String state = Environment.getExternalStorageState();\n        if (state.equals(Environment.MEDIA_MOUNTED)) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * 返回以“/”结尾的内部存储根目录\n     */\n    public static String getInternalRootPath(Context context, String type) {\n        File file;\n        if (TextUtils.isEmpty(type)) {\n            file = context.getFilesDir();\n        } else {\n            file = new File(FileUtils.separator(context.getFilesDir().getAbsolutePath()) + type);\n            //noinspection ResultOfMethodCallIgnored\n            file.mkdirs();\n        }\n        String path = \"\";\n        if (file != null) {\n            path = FileUtils.separator(file.getAbsolutePath());\n        }\n        return path;\n    }\n\n    public static String getInternalRootPath(Context context) {\n        return getInternalRootPath(context, null);\n    }\n\n    /**\n     * 返回以“/”结尾的外部存储根目录，外置卡不可用则返回空字符串\n     */\n    public static String getExternalRootPath(String type) {\n        File file = null;\n        if (externalMounted()) {\n            file = Environment.getExternalStorageDirectory();\n        }\n        if (file != null && !TextUtils.isEmpty(type)) {\n            file = new File(file, type);\n            //noinspection ResultOfMethodCallIgnored\n            file.mkdirs();\n        }\n        String path = \"\";\n        if (file != null) {\n            path = FileUtils.separator(file.getAbsolutePath());\n        }\n        return path;\n    }\n\n    public static String getExternalRootPath() {\n        return getExternalRootPath(null);\n    }\n\n    /**\n     * 各种类型的文件的专用的保存路径，以“/”结尾\n     *\n     * @return 诸如：/mnt/sdcard/Android/data/[package]/files/[type]/\n     */\n    public static String getExternalPrivatePath(Context context, String type) {\n        File file = null;\n        if (externalMounted()) {\n            file = context.getExternalFilesDir(type);\n        }\n        //高频触发java.lang.NullPointerException，是SD卡不可用或暂时繁忙么？\n        String path = \"\";\n        if (file != null) {\n            path = FileUtils.separator(file.getAbsolutePath());\n        }\n        return path;\n    }\n\n    public static String getExternalPrivatePath(Context context) {\n        return getExternalPrivatePath(context, null);\n    }\n\n    /**\n     * 下载的文件的保存路径，必须为外部存储，以“/”结尾\n     *\n     * @return 诸如 ：/mnt/sdcard/Download/\n     */\n    public static String getDownloadPath() throws RuntimeException {\n        File file;\n        if (externalMounted()) {\n            file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);\n        } else {\n            throw new RuntimeException(\"外置存储不可用！\");\n        }\n        return FileUtils.separator(file.getAbsolutePath());\n    }\n\n    /**\n     * 各种类型的文件的专用的缓存存储保存路径，优先使用外置存储，以“/”结尾\n     */\n    public static String getCachePath(Context context, String type) {\n        File file;\n        if (externalMounted()) {\n            file = context.getExternalCacheDir();\n        } else {\n            file = context.getCacheDir();\n        }\n        if (!TextUtils.isEmpty(type)) {\n            file = new File(file, type);\n            //noinspection ResultOfMethodCallIgnored\n            file.mkdirs();\n        }\n        String path = \"\";\n        if (file != null) {\n            path = FileUtils.separator(file.getAbsolutePath());\n        }\n        return path;\n    }\n\n    public static String getCachePath(Context context) {\n        return getCachePath(context, null);\n    }\n\n    /**\n     * 返回以“/”结尾的临时存储目录\n     */\n    public static String getTempDirPath(Context context) {\n        return getExternalPrivatePath(context, \"temporary\");\n    }\n\n    /**\n     * 返回临时存储文件路径\n     */\n    public static String getTempFilePath(Context context) {\n        try {\n            return File.createTempFile(\"lyj_\", \".tmp\", context.getCacheDir()).getAbsolutePath();\n        } catch (IOException e) {\n            return getTempDirPath(context) + \"lyj.tmp\";\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/font/FontAdapter.java",
    "content": "package com.kunfei.bookshelf.widget.font;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Typeface;\nimport android.os.Build;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.FileDoc;\nimport com.kunfei.bookshelf.utils.RealPathUtil;\n\nimport java.io.File;\nimport java.io.FileDescriptor;\nimport java.net.URLDecoder;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class FontAdapter extends RecyclerView.Adapter<FontAdapter.MyViewHolder> {\n    private final List<FileDoc> docList = new ArrayList<>();\n    private final FontSelector.OnThisListener thisListener;\n    private final Context context;\n    private String selectName;\n\n    FontAdapter(Context context, String selectPath, FontSelector.OnThisListener thisListener) {\n        this.context = context;\n        try {\n            String[] x = URLDecoder.decode(selectPath, \"utf-8\")\n                    .split(File.separator);\n            this.selectName = x[x.length - 1];\n        } catch (Exception e) {\n            this.selectName = \"\";\n        }\n        this.thisListener = thisListener;\n    }\n\n    @NonNull\n    @Override\n    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_font, parent, false));\n    }\n\n    @Override\n    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {\n        if (docList.size() > 0) {\n            FileDoc docItem = docList.get(position);\n            try {\n                Typeface typeface;\n                if (docItem.isContentScheme()) {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                        FileDescriptor fd = context.getContentResolver().openFileDescriptor(docItem.getUri(), \"r\")\n                                .getFileDescriptor();\n                        typeface = new Typeface.Builder(fd).build();\n                    } else {\n                        typeface = Typeface.createFromFile(RealPathUtil.getPath(context, docItem.getUri()));\n                    }\n                } else {\n                    typeface = Typeface.createFromFile(docItem.getUri().toString());\n                }\n                holder.tvFont.setTypeface(typeface);\n            } catch (Exception ignored) {\n\n            }\n            holder.tvFont.setText(docItem.getName());\n\n            if (docItem.getName().equals(selectName)) {\n                holder.ivChecked.setVisibility(View.VISIBLE);\n            } else {\n                holder.ivChecked.setVisibility(View.INVISIBLE);\n            }\n            holder.tvFont.setOnClickListener(view -> {\n                if (thisListener != null) {\n                    thisListener.setFontPath(docItem);\n                }\n            });\n        } else {\n            holder.tvFont.setText(R.string.fonts_folder);\n        }\n    }\n\n    @Override\n    public int getItemCount() {\n        return docList.size() == 0 ? 1 : docList.size();\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    void upData(List<FileDoc> docItems) {\n        if (docItems != null) {\n            docList.clear();\n            docList.addAll(docItems);\n        }\n        notifyDataSetChanged();\n    }\n\n    static class MyViewHolder extends RecyclerView.ViewHolder {\n        TextView tvFont;\n        ImageView ivChecked;\n\n        MyViewHolder(View itemView) {\n            super(itemView);\n            tvFont = itemView.findViewById(R.id.tv_font);\n            ivChecked = itemView.findViewById(R.id.iv_checked);\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/font/FontSelector.java",
    "content": "package com.kunfei.bookshelf.widget.font;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\n\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.FileDoc;\nimport com.kunfei.bookshelf.utils.theme.ATH;\n\nimport java.util.List;\n\nimport kotlin.text.Regex;\n\npublic class FontSelector {\n    private final AlertDialog.Builder builder;\n    private final FontAdapter adapter;\n    private OnThisListener thisListener;\n    private AlertDialog alertDialog;\n    public static Regex fontRegex = new Regex(\"(?i).*\\\\.[ot]tf\");\n\n    public FontSelector(Context context, String selectPath) {\n        builder = new AlertDialog.Builder(context, R.style.alertDialogTheme);\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.view_recycler_font, null);\n        RecyclerView recyclerView = view.findViewById(R.id.recycler_view);\n        builder.setView(view);\n        builder.setTitle(R.string.select_font);\n        builder.setNegativeButton(R.string.cancel, null);\n        adapter = new FontAdapter(context, selectPath,\n                new OnThisListener() {\n                    @Override\n                    public void setDefault() {\n                        if (thisListener != null) {\n                            thisListener.setDefault();\n                        }\n                        alertDialog.dismiss();\n                    }\n\n                    @Override\n                    public void setFontPath(FileDoc fileDoc) {\n                        if (thisListener != null) {\n                            thisListener.setFontPath(fileDoc);\n                        }\n                        alertDialog.dismiss();\n                    }\n                });\n        recyclerView.setAdapter(adapter);\n        recyclerView.setLayoutManager(new LinearLayoutManager(context));\n    }\n\n    public FontSelector setListener(OnThisListener thisListener) {\n        this.thisListener = thisListener;\n        builder.setPositiveButton(R.string.default_font, ((dialogInterface, i) -> thisListener.setDefault()));\n        return this;\n    }\n\n    public FontSelector create(List<FileDoc> docItems) {\n        adapter.upData(docItems);\n        builder.create();\n        return this;\n    }\n\n    public void show() {\n        alertDialog = builder.show();\n        ATH.setAlertDialogTint(alertDialog);\n    }\n\n    public interface OnThisListener {\n        void setDefault();\n\n        void setFontPath(FileDoc fileDoc);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/image/CoverImageView.kt",
    "content": "package com.kunfei.bookshelf.widget.image\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.*\nimport android.graphics.drawable.Drawable\nimport android.text.TextPaint\nimport android.util.AttributeSet\nimport com.bumptech.glide.load.DataSource\nimport com.bumptech.glide.load.engine.GlideException\nimport com.bumptech.glide.request.RequestListener\nimport com.bumptech.glide.request.target.Target\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.help.glide.ImageLoader\n\n\nclass CoverImageView : androidx.appcompat.widget.AppCompatImageView {\n    internal var width: Float = 0.toFloat()\n    internal var height: Float = 0.toFloat()\n    private var nameHeight = 0f\n    private var authorHeight = 0f\n    private val namePaint = TextPaint()\n    private val authorPaint = TextPaint()\n    private var name: String? = null\n    private var author: String? = null\n    private var loadFailed = false\n\n    constructor(context: Context) : super(context)\n\n    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)\n\n    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(\n            context,\n            attrs,\n            defStyleAttr\n    )\n\n    init {\n        namePaint.typeface = Typeface.DEFAULT_BOLD\n        namePaint.isAntiAlias = true\n        namePaint.textAlign = Paint.Align.CENTER\n        namePaint.textSkewX = -0.2f\n        authorPaint.typeface = Typeface.DEFAULT\n        authorPaint.isAntiAlias = true\n        authorPaint.textAlign = Paint.Align.CENTER\n        authorPaint.textSkewX = -0.1f\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)\n        val measuredHeight = measuredWidth * 7 / 5\n        super.onMeasure(\n                widthMeasureSpec,\n                MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)\n        )\n    }\n\n    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {\n        super.onLayout(changed, left, top, right, bottom)\n        width = getWidth().toFloat()\n        height = getHeight().toFloat()\n        namePaint.textSize = width / 6\n        namePaint.strokeWidth = namePaint.textSize / 10\n        authorPaint.textSize = width / 9\n        authorPaint.strokeWidth = authorPaint.textSize / 10\n        nameHeight = height / 2\n        authorHeight = nameHeight + authorPaint.fontSpacing\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        if (width >= 10 && height > 10) {\n            @SuppressLint(\"DrawAllocation\")\n            val path = Path()\n            //四个圆角\n            path.moveTo(10f, 0f)\n            path.lineTo(width - 10, 0f)\n            path.quadTo(width, 0f, width, 10f)\n            path.lineTo(width, height - 10)\n            path.quadTo(width, height, width - 10, height)\n            path.lineTo(10f, height)\n            path.quadTo(0f, height, 0f, height - 10)\n            path.lineTo(0f, 10f)\n            path.quadTo(0f, 0f, 10f, 0f)\n\n            canvas.clipPath(path)\n        }\n        super.onDraw(canvas)\n        if (!loadFailed) return\n        name?.let {\n            namePaint.color = Color.WHITE\n            namePaint.style = Paint.Style.STROKE\n            canvas.drawText(it, width / 2, nameHeight, namePaint)\n            namePaint.color = Color.RED\n            namePaint.style = Paint.Style.FILL\n            canvas.drawText(it, width / 2, nameHeight, namePaint)\n        }\n        author?.let {\n            authorPaint.color = Color.WHITE\n            authorPaint.style = Paint.Style.STROKE\n            canvas.drawText(it, width / 2, authorHeight, authorPaint)\n            authorPaint.color = Color.RED\n            authorPaint.style = Paint.Style.FILL\n            canvas.drawText(it, width / 2, authorHeight, authorPaint)\n        }\n    }\n\n    fun setHeight(height: Int) {\n        val width = height * 5 / 7\n        minimumWidth = width\n    }\n\n    private fun setText(name: String?, author: String?) {\n        this.name =\n                when {\n                    name == null -> null\n                    name.length > 5 -> name.substring(0, 4) + \"…\"\n                    else -> name\n                }\n        this.author =\n                when {\n                    author == null -> null\n                    author.length > 8 -> author.substring(0, 7) + \"…\"\n                    else -> author\n                }\n    }\n\n    fun load(path: String?, name: String?, author: String?) {\n        setText(name, author)\n        ImageLoader.load(context, path)//Glide自动识别http://和file://\n                .placeholder(R.drawable.image_cover_default)\n                .error(R.drawable.image_cover_default)\n                .listener(object : RequestListener<Drawable> {\n                    override fun onLoadFailed(\n                            e: GlideException?,\n                            model: Any?,\n                            target: Target<Drawable>?,\n                            isFirstResource: Boolean\n                    ): Boolean {\n                        e?.printStackTrace()\n                        loadFailed = true\n                        return false\n                    }\n\n                    override fun onResourceReady(\n                            resource: Drawable?,\n                            model: Any?,\n                            target: Target<Drawable>?,\n                            dataSource: DataSource?,\n                            isFirstResource: Boolean\n                    ): Boolean {\n                        loadFailed = false\n                        return false\n                    }\n\n                })\n                .centerCrop()\n                .into(this)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/image/FilletImageView.java",
    "content": "package com.kunfei.bookshelf.widget.image;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Path;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatImageView;\n\nimport com.kunfei.bookshelf.R;\n\npublic class FilletImageView extends AppCompatImageView {\n    float width, height;\n    private int leftTopRadius;\n    private int rightTopRadius;\n    private int rightBottomRadius;\n    private int leftBottomRadius;\n\n    public FilletImageView(Context context) {\n        super(context);\n    }\n\n    public FilletImageView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public FilletImageView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        // 读取配置\n        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView);\n        int defaultRadius = 5;\n        int radius = array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius);\n        leftTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_top_radius, defaultRadius);\n        rightTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_top_radius, defaultRadius);\n        rightBottomRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_bottom_radius, defaultRadius);\n        leftBottomRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_bottom_radius, defaultRadius);\n\n        //如果四个角的值没有设置，那么就使用通用的radius的值。\n        if (defaultRadius == leftTopRadius) {\n            leftTopRadius = radius;\n        }\n        if (defaultRadius == rightTopRadius) {\n            rightTopRadius = radius;\n        }\n        if (defaultRadius == rightBottomRadius) {\n            rightBottomRadius = radius;\n        }\n        if (defaultRadius == leftBottomRadius) {\n            leftBottomRadius = radius;\n        }\n        array.recycle();\n\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        super.onLayout(changed, left, top, right, bottom);\n        width = getWidth();\n        height = getHeight();\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        //这里做下判断，只有图片的宽高大于设置的圆角距离的时候才进行裁剪\n        int maxLeft = Math.max(leftTopRadius, leftBottomRadius);\n        int maxRight = Math.max(rightTopRadius, rightBottomRadius);\n        int minWidth = maxLeft + maxRight;\n        int maxTop = Math.max(leftTopRadius, rightTopRadius);\n        int maxBottom = Math.max(leftBottomRadius, rightBottomRadius);\n        int minHeight = maxTop + maxBottom;\n        if (width >= minWidth && height > minHeight) {\n            @SuppressLint(\"DrawAllocation\") Path path = new Path();\n            //四个角：右上，右下，左下，左上\n            path.moveTo(leftTopRadius, 0);\n            path.lineTo(width - rightTopRadius, 0);\n            path.quadTo(width, 0, width, rightTopRadius);\n\n            path.lineTo(width, height - rightBottomRadius);\n            path.quadTo(width, height, width - rightBottomRadius, height);\n\n            path.lineTo(leftBottomRadius, height);\n            path.quadTo(0, height, 0, height - leftBottomRadius);\n\n            path.lineTo(0, leftTopRadius);\n            path.quadTo(0, 0, leftTopRadius, 0);\n\n            canvas.clipPath(path);\n        }\n        super.onDraw(canvas);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/itemdecoration/DividerGridItemDecoration.java",
    "content": "package com.kunfei.bookshelf.widget.itemdecoration;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.os.Build;\nimport android.view.View;\n\nimport androidx.annotation.DrawableRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.RequiresApi;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\n/**\n * Created by newbiechen on 2017/10/8.\n */\n\npublic class DividerGridItemDecoration extends RecyclerView.ItemDecoration {\n\n    private static final int[] ATTRS = new int[]{android.R.attr.listDivider};\n    private Drawable mWidthDivider;\n    private Drawable mHeightDivider;\n\n    public DividerGridItemDecoration(Context context) {\n        final TypedArray a = context.obtainStyledAttributes(ATTRS);\n        mWidthDivider = a.getDrawable(0);\n        mHeightDivider = mWidthDivider;\n        a.recycle();\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    public DividerGridItemDecoration(Context context, @DrawableRes int widthDividerRes, @DrawableRes int heightDividerRes) {\n        mWidthDivider = context.getDrawable(widthDividerRes);\n        mHeightDivider = context.getDrawable(heightDividerRes);\n    }\n\n    @Override\n    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {\n        drawHorizontal(c, parent);\n        drawVertical(c, parent);\n    }\n\n    private int getSpanCount(RecyclerView parent) {\n        // 列数\n        int spanCount = -1;\n        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();\n        if (layoutManager instanceof GridLayoutManager) {\n            spanCount = ((GridLayoutManager) layoutManager).getSpanCount();\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n            spanCount = ((StaggeredGridLayoutManager) layoutManager)\n                    .getSpanCount();\n        }\n        return spanCount;\n\n    }\n\n    public void drawHorizontal(Canvas c, RecyclerView parent) {\n        int childCount = parent.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            final View child = parent.getChildAt(i);\n            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child\n                    .getLayoutParams();\n            final int left = child.getLeft() - params.leftMargin;\n            final int right = child.getRight() + params.rightMargin\n                    + mWidthDivider.getIntrinsicWidth();\n            final int top = child.getBottom() + params.bottomMargin;\n            final int bottom = top + mWidthDivider.getIntrinsicHeight();\n            mWidthDivider.setBounds(left, top, right, bottom);\n            mWidthDivider.draw(c);\n        }\n\n\n    }\n\n    public void drawVertical(Canvas c, RecyclerView parent) {\n        final int childCount = parent.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            final View child = parent.getChildAt(i);\n\n            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child\n                    .getLayoutParams();\n            final int top = child.getTop() - params.topMargin;\n            final int bottom = child.getBottom() + params.bottomMargin;\n            final int left = child.getRight() + params.rightMargin;\n            final int right = left + mHeightDivider.getIntrinsicWidth();\n\n            mHeightDivider.setBounds(left, top, right, bottom);\n            mHeightDivider.draw(c);\n        }\n    }\n\n    private boolean isLastColumn(RecyclerView parent, int pos, int spanCount,\n                                 int childCount) {\n        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();\n        if (layoutManager instanceof GridLayoutManager) {\n            if ((pos + 1) % spanCount == 0) {\n                return true;\n            }\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n\n            int orientation = ((StaggeredGridLayoutManager) layoutManager)\n                    .getOrientation();\n            if (orientation == StaggeredGridLayoutManager.VERTICAL) {\n                // 如果是最后一列，则不需要绘制右边\n                if ((pos + 1) % spanCount == 0) {\n                    return true;\n                }\n            } else {\n                childCount = childCount - childCount % spanCount;\n                // 如果是最后一列，则不需要绘制右边\n                if (pos >= childCount)\n                    return true;\n            }\n        }\n        return false;\n    }\n\n    private boolean isLastRaw(RecyclerView parent, int pos, int spanCount,\n                              int childCount) {\n        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();\n        if (layoutManager instanceof GridLayoutManager) {\n            childCount = childCount - childCount % spanCount;\n            if (pos >= childCount)// 如果是最后一行，则不需要绘制底部\n                return true;\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n\n            int orientation = ((StaggeredGridLayoutManager) layoutManager)\n                    .getOrientation();\n            // StaggeredGridLayoutManager 且纵向滚动\n            if (orientation == StaggeredGridLayoutManager.VERTICAL) {\n                childCount = childCount - childCount % spanCount;\n                // 如果是最后一行，则不需要绘制底部\n                if (pos >= childCount)\n                    return true;\n            } else {\n                // 如果是最后一行，则不需要绘制底部\n                if ((pos + 1) % spanCount == 0) {\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n    @Override\n    public void getItemOffsets(Rect outRect, int itemPosition,\n                               RecyclerView parent) {\n        int spanCount = getSpanCount(parent);\n        int childCount = parent.getAdapter().getItemCount();\n        // 如果是最后一行，则不需要绘制底部\n        if (isLastRaw(parent, itemPosition, spanCount, childCount)) {\n            outRect.set(0, 0, mHeightDivider.getIntrinsicWidth(), 0);\n        }\n        // 如果是最后一列，则不需要绘制右边\n        else if (isLastColumn(parent, itemPosition, spanCount, childCount)) {\n            outRect.set(0, 0, 0, mWidthDivider.getIntrinsicHeight());\n        } else {\n            outRect.set(0, 0, mHeightDivider.getIntrinsicWidth(),\n                    mWidthDivider.getIntrinsicHeight());\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/itemdecoration/DividerItemDecoration.java",
    "content": "package com.kunfei.bookshelf.widget.itemdecoration;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.view.View;\n\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by newbiechen on 2017/10/8.\n */\n\npublic class DividerItemDecoration extends RecyclerView.ItemDecoration {\n    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;\n    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;\n    private static final String TAG = \"DividerItemDecoration\";\n    private static final int[] ATTRS = new int[]{\n            android.R.attr.listDivider\n    };\n    private Drawable mDrawable;\n\n    public DividerItemDecoration(Context context) {\n        final TypedArray a = context.obtainStyledAttributes(ATTRS);\n        mDrawable = a.getDrawable(0);\n        a.recycle();\n    }\n\n    @Override\n    public void onDraw(Canvas c, RecyclerView parent) {\n        if (getLayoutManagerType(parent) == VERTICAL_LIST) {\n            drawVertical(c, parent);\n        } else {\n            drawHorizontal(c, parent);\n        }\n    }\n\n    private int getLayoutManagerType(RecyclerView rv) {\n        RecyclerView.LayoutManager manager = rv.getLayoutManager();\n\n        if (!(manager instanceof LinearLayoutManager)) {\n            throw new IllegalArgumentException(\"only supply linearLayoutManager\");\n        }\n        return ((LinearLayoutManager) manager).getOrientation();\n    }\n\n    public void drawVertical(Canvas c, RecyclerView parent) {\n        final int left = parent.getPaddingLeft();\n        final int right = parent.getWidth() - parent.getPaddingRight();\n\n        final int childCount = parent.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            final View child = parent.getChildAt(i);\n            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child\n                    .getLayoutParams();\n            final int top = child.getBottom() + params.bottomMargin;\n            final int bottom = top + mDrawable.getIntrinsicHeight();\n            mDrawable.setBounds(left, top, right, bottom);\n            mDrawable.draw(c);\n        }\n    }\n\n    public void drawHorizontal(Canvas c, RecyclerView parent) {\n        final int top = parent.getPaddingTop();\n        final int bottom = parent.getHeight() - parent.getPaddingBottom();\n\n        final int childCount = parent.getChildCount();\n        for (int i = 0; i < childCount; i++) {\n            final View child = parent.getChildAt(i);\n            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child\n                    .getLayoutParams();\n            final int left = child.getRight() + params.rightMargin;\n            final int right = left + mDrawable.getIntrinsicHeight();\n            mDrawable.setBounds(left, top, right, bottom);\n            mDrawable.draw(c);\n        }\n    }\n\n    @Override\n    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {\n        if (getLayoutManagerType(parent) == VERTICAL_LIST) {\n            outRect.set(0, 0, 0, mDrawable.getIntrinsicHeight());\n        } else {\n            outRect.set(0, 0, mDrawable.getIntrinsicWidth(), 0);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/BaseDialog.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\n\npublic class BaseDialog extends Dialog {\n    public BaseDialog(@NonNull Context context) {\n        super(context);\n    }\n\n    public BaseDialog(@NonNull Context context, int themeResId) {\n        super(context, themeResId);\n    }\n\n    protected BaseDialog(@NonNull Context context, boolean cancelable, @Nullable OnCancelListener cancelListener) {\n        super(context, cancelable, cancelListener);\n    }\n\n    @Override\n    public void dismiss() {\n        View view = getCurrentFocus();\n        if (view instanceof TextView) {\n            SoftInputUtil.hideIMM(view);\n        }\n        super.dismiss();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/BookmarkDialog.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookmarkBean;\n\npublic class BookmarkDialog extends BaseDialog {\n    private Context context;\n    private TextView tvChapterName;\n    private EditText tvContent;\n    private View llEdit;\n    private View tvOk;\n    private BookmarkBean bookmarkBean;\n    private View tvSave;\n    private View tvDel;\n\n    public static BookmarkDialog builder(Context context, @NonNull BookmarkBean bookmarkBean, boolean isAdd) {\n        return new BookmarkDialog(context, bookmarkBean, isAdd);\n    }\n\n    private BookmarkDialog(Context context, @NonNull BookmarkBean bookmarkBean, boolean isAdd) {\n        super(context, R.style.alertDialogTheme);\n        this.context = context;\n        this.bookmarkBean = bookmarkBean;\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_bookmark, null);\n        bindView(view);\n        setContentView(view);\n        tvChapterName.setText(bookmarkBean.getChapterName());\n        tvContent.setText(bookmarkBean.getContent());\n        if (isAdd) {\n            llEdit.setVisibility(View.GONE);\n            tvOk.setVisibility(View.VISIBLE);\n        } else {\n            llEdit.setVisibility(View.VISIBLE);\n            tvOk.setVisibility(View.GONE);\n        }\n    }\n\n    private void bindView(View view) {\n        tvChapterName = view.findViewById(R.id.tvChapterName);\n        tvContent = view.findViewById(R.id.tie_content);\n        tvOk = view.findViewById(R.id.tv_ok);\n        tvSave = view.findViewById(R.id.tv_save);\n        tvDel = view.findViewById(R.id.tv_del);\n        llEdit = view.findViewById(R.id.llEdit);\n    }\n\n    public BookmarkDialog setPositiveButton(Callback callback) {\n        tvChapterName.setOnClickListener(v -> {\n            callback.openChapter(bookmarkBean.getChapterIndex(), bookmarkBean.getPageIndex());\n            dismiss();\n        });\n        tvOk.setOnClickListener(v -> {\n            bookmarkBean.setContent(tvContent.getText().toString());\n            callback.saveBookmark(bookmarkBean);\n            dismiss();\n        });\n        tvSave.setOnClickListener(v -> {\n            bookmarkBean.setContent(tvContent.getText().toString());\n            callback.saveBookmark(bookmarkBean);\n            dismiss();\n        });\n        tvDel.setOnClickListener(v -> {\n            callback.delBookmark(bookmarkBean);\n            dismiss();\n        });\n        return this;\n    }\n\n    public interface Callback {\n        void saveBookmark(BookmarkBean bookmarkBean);\n\n        void delBookmark(BookmarkBean bookmarkBean);\n\n        void openChapter(int chapterIndex, int pageIndex);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/ChangeSourceDialog.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.text.TextUtils;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.WindowManager;\nimport android.widget.ImageButton;\nimport android.widget.LinearLayout;\nimport android.widget.PopupMenu;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.recyclerview.widget.DividerItemDecoration;\nimport androidx.recyclerview.widget.LinearLayoutManager;\n\nimport com.hwangjr.rxbus.RxBus;\nimport com.hwangjr.rxbus.annotation.Subscribe;\nimport com.hwangjr.rxbus.annotation.Tag;\nimport com.hwangjr.rxbus.thread.EventThread;\nimport com.kunfei.bookshelf.DbHelper;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.BookSourceBean;\nimport com.kunfei.bookshelf.bean.SearchBookBean;\nimport com.kunfei.bookshelf.constant.RxBusTag;\nimport com.kunfei.bookshelf.dao.SearchBookBeanDao;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.BookSourceManager;\nimport com.kunfei.bookshelf.model.SearchBookModel;\nimport com.kunfei.bookshelf.model.UpLastChapterModel;\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.view.activity.SourceEditActivity;\nimport com.kunfei.bookshelf.view.adapter.ChangeSourceAdapter;\nimport com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerView;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Objects;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleObserver;\nimport io.reactivex.SingleOnSubscribe;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\npublic class ChangeSourceDialog extends BaseDialog implements ChangeSourceAdapter.CallBack {\n    private Context context;\n    private TextView atvTitle;\n    private ImageButton ibtStop;\n    private SearchView searchView;\n    private RefreshRecyclerView rvSource;\n    private Handler handler = new Handler(Looper.getMainLooper());\n    private ChangeSourceAdapter adapter;\n    private SearchBookModel searchBookModel;\n    private BookShelfBean book;\n    private String bookTag;\n    private String bookName;\n    private String bookAuthor;\n    private int shelfLastChapter;\n    private CompositeDisposable compositeDisposable;\n    private Callback callback;\n\n    public static ChangeSourceDialog builder(Context context, BookShelfBean bookShelfBean) {\n        return new ChangeSourceDialog(context, bookShelfBean);\n    }\n\n    private ChangeSourceDialog(@NonNull Context context, BookShelfBean bookShelfBean) {\n        super(context, R.style.alertDialogTheme);\n        this.context = context;\n        init(bookShelfBean);\n    }\n\n    private void init(BookShelfBean bookShelf) {\n        this.book = bookShelf;\n        compositeDisposable = new CompositeDisposable();\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_change_source, null);\n        bindView(view);\n        setContentView(view);\n        initData();\n    }\n\n    private void bindView(View view) {\n        View llContent = view.findViewById(R.id.ll_content);\n        llContent.setOnClickListener(null);\n        searchView = view.findViewById(R.id.searchView);\n        atvTitle = view.findViewById(R.id.atv_title);\n        ibtStop = view.findViewById(R.id.ibt_stop);\n        rvSource = view.findViewById(R.id.rf_rv_change_source);\n        ibtStop.setVisibility(View.INVISIBLE);\n\n        rvSource.addItemDecoration(new DividerItemDecoration(context, LinearLayout.VERTICAL));\n        rvSource.setBaseRefreshListener(this::reSearchBook);\n        ibtStop.setOnClickListener(v -> stopChangeSource());\n        searchView.onActionViewExpanded();\n        searchView.clearFocus();\n        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                return false;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                if (StringUtils.isTrimEmpty(newText)) {\n                    List<SearchBookBean> searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder()\n                            .where(SearchBookBeanDao.Properties.Name.eq(bookName), SearchBookBeanDao.Properties.Author.eq(bookAuthor))\n                            .build().list();\n                    adapter.reSetSourceAdapter();\n                    adapter.addAllSourceAdapter(searchBookBeans);\n                } else {\n                    List<SearchBookBean> searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder()\n                            .where(SearchBookBeanDao.Properties.Name.eq(bookName), SearchBookBeanDao.Properties.Author.eq(bookAuthor), SearchBookBeanDao.Properties.Origin.like(\"%\" + searchView.getQuery().toString() + \"%\"))\n                            .build().list();\n                    adapter.reSetSourceAdapter();\n                    adapter.addAllSourceAdapter(searchBookBeans);\n                }\n                return false;\n            }\n        });\n    }\n\n    @Override\n    public void changeTo(SearchBookBean searchBookBean) {\n        if (!Objects.equals(book.getNoteUrl(), searchBookBean.getNoteUrl())) {\n            callback.changeSource(searchBookBean);\n        }\n        dismiss();\n    }\n\n    @Override\n    public void showMenu(View view, SearchBookBean searchBookBean) {\n        final String url = searchBookBean.getTag();\n        final BookSourceBean sourceBean = BookSourceManager.getBookSourceByUrl(url);\n        PopupMenu popupMenu = new PopupMenu(context, view);\n        popupMenu.getMenu().add(0, R.id.menu_disable, 1, \"禁用书源\");\n        popupMenu.getMenu().add(0, R.id.menu_del, 2, \"删除书源\");\n        popupMenu.getMenu().add(0, R.id.menu_edit, 3, \"编辑书源\");\n        popupMenu.setOnMenuItemClickListener(menuItem -> {\n            if (sourceBean != null) {\n                switch (menuItem.getItemId()) {\n                    case R.id.menu_disable:\n                        sourceBean.setEnable(false);\n                        BookSourceManager.addBookSource(sourceBean);\n                        adapter.removeData(searchBookBean);\n                        Toast.makeText(context, String.format(\"%s已禁用\", sourceBean.getBookSourceName()), Toast.LENGTH_SHORT).show();\n                        break;\n                    case R.id.menu_del:\n                        BookSourceManager.removeBookSource(sourceBean);\n                        adapter.removeData(searchBookBean);\n                        Toast.makeText(context, String.format(\"%s已删除\", sourceBean.getBookSourceName()), Toast.LENGTH_SHORT).show();\n                        break;\n                    case R.id.menu_edit:\n                        SourceEditActivity.startThis(context, sourceBean);\n                        break;\n                }\n            }\n            return true;\n        });\n        popupMenu.show();\n    }\n\n    @SuppressLint(\"InflateParams\")\n    private void initData() {\n        adapter = new ChangeSourceAdapter(false);\n        rvSource.setRefreshRecyclerViewAdapter(adapter, new LinearLayoutManager(context));\n        adapter.setCallBack(this);\n        View viewRefreshError = LayoutInflater.from(context).inflate(R.layout.view_refresh_error, null);\n        viewRefreshError.setBackgroundResource(R.color.background_card);\n        viewRefreshError.findViewById(R.id.tv_refresh_again).setOnClickListener(v -> {\n            //刷新失败 ，重试\n            reSearchBook();\n        });\n        rvSource.setNoDataAndRefreshErrorView(LayoutInflater.from(context).inflate(R.layout.view_refresh_no_data, null),\n                viewRefreshError);\n\n        SearchBookModel.OnSearchListener searchListener = new SearchBookModel.OnSearchListener() {\n            @Override\n            public void refreshSearchBook() {\n                ibtStop.setVisibility(View.VISIBLE);\n                adapter.reSetSourceAdapter();\n            }\n\n            @Override\n            public void refreshFinish(Boolean value) {\n                ibtStop.setVisibility(View.INVISIBLE);\n                rvSource.finishRefresh(true, true);\n            }\n\n            @Override\n            public void loadMoreFinish(Boolean value) {\n                ibtStop.setVisibility(View.INVISIBLE);\n                rvSource.finishRefresh(true);\n            }\n\n            @Override\n            public void loadMoreSearchBook(List<SearchBookBean> value) {\n                addSearchBook(value);\n            }\n\n            @Override\n            public void searchBookError(Throwable throwable) {\n                ibtStop.setVisibility(View.INVISIBLE);\n                if (adapter.getICount() == 0) {\n                    rvSource.refreshError();\n                }\n            }\n\n            @Override\n            public int getItemCount() {\n                return 0;\n            }\n        };\n        searchBookModel = new SearchBookModel(searchListener);\n        bookTag = book.getTag();\n        bookName = book.getBookInfoBean().getName();\n        bookAuthor = book.getBookInfoBean().getAuthor();\n        shelfLastChapter = BookshelfHelp.guessChapterNum(book.getLastChapterName());\n        atvTitle.setText(String.format(\"%s (%s)\", bookName, bookAuthor));\n        rvSource.startRefresh();\n        getSearchBookInDb(book);\n        RxBus.get().register(this);\n        setOnDismissListener(dialog -> {\n            RxBus.get().unregister(ChangeSourceDialog.this);\n            compositeDisposable.dispose();\n            if (searchBookModel != null) {\n                searchBookModel.onDestroy();\n            }\n        });\n    }\n\n    public ChangeSourceDialog setCallback(Callback callback) {\n        this.callback = callback;\n        return this;\n    }\n\n    public void show() {\n        super.show();\n        WindowManager.LayoutParams params = Objects.requireNonNull(getWindow()).getAttributes();\n        params.height = ScreenUtils.getAppSize()[1] - 60;\n        params.width = ScreenUtils.getAppSize()[0] - 60;\n        getWindow().setAttributes(params);\n    }\n\n    private void getSearchBookInDb(BookShelfBean bookShelf) {\n        Single.create((SingleOnSubscribe<List<SearchBookBean>>) e -> {\n            List<SearchBookBean> searchBookBeans = DbHelper.getDaoSession().getSearchBookBeanDao().queryBuilder()\n                    .where(SearchBookBeanDao.Properties.Name.eq(bookName), SearchBookBeanDao.Properties.Author.eq(bookAuthor)).build().list();\n            if (searchBookBeans == null) searchBookBeans = new ArrayList<>();\n            List<SearchBookBean> searchBookList = new ArrayList<>();\n            List<BookSourceBean> bookSourceList = new ArrayList<>(BookSourceManager.getSelectedBookSource());\n            if (bookSourceList.size() > 0) {\n                for (BookSourceBean bookSourceBean : BookSourceManager.getSelectedBookSource()) {\n                    boolean hasSource = false;\n                    for (SearchBookBean searchBookBean : new ArrayList<>(searchBookBeans)) {\n                        if (Objects.equals(searchBookBean.getTag(), bookSourceBean.getBookSourceUrl())) {\n                            bookSourceList.remove(bookSourceBean);\n                            searchBookList.add(searchBookBean);\n                            hasSource = true;\n                            break;\n                        }\n                    }\n                    if (hasSource) {\n                        bookSourceList.remove(bookSourceBean);\n                    }\n                }\n                searchBookModel.searchReNew();\n                searchBookModel.initSearchEngineS(bookSourceList);\n                long startThisSearchTime = System.currentTimeMillis();\n                searchBookModel.setSearchTime(startThisSearchTime);\n                List<BookShelfBean> bookList = new ArrayList<>();\n                bookList.add(book);\n                searchBookModel.search(bookName, startThisSearchTime, bookList, false);\n                UpLastChapterModel.getInstance().startUpdate(searchBookList);\n            }\n            if (searchBookList.size() > 0) {\n                for (SearchBookBean searchBookBean : searchBookList) {\n                    if (searchBookBean.getTag().equals(bookShelf.getTag())) {\n                        searchBookBean.setIsCurrentSource(true);\n                    } else {\n                        searchBookBean.setIsCurrentSource(false);\n                    }\n                }\n                Collections.sort(searchBookList, this::compareSearchBooks);\n            }\n            e.onSuccess(searchBookList);\n        }).subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new SingleObserver<List<SearchBookBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onSuccess(List<SearchBookBean> searchBookBeans) {\n                        if (searchBookBeans.size() > 0) {\n                            adapter.addAllSourceAdapter(searchBookBeans);\n                            ibtStop.setVisibility(View.INVISIBLE);\n                            rvSource.finishRefresh(true, true);\n                        } else {\n                            reSearchBook();\n                        }\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        reSearchBook();\n                    }\n                });\n    }\n\n    private void reSearchBook() {\n        rvSource.startRefresh();\n        searchBookModel.initSearchEngineS(BookSourceManager.getSelectedBookSource());\n        searchBookModel.searchReNew();\n        long startThisSearchTime = System.currentTimeMillis();\n        searchBookModel.setSearchTime(startThisSearchTime);\n        List<BookShelfBean> bookList = new ArrayList<>();\n        bookList.add(book);\n        searchBookModel.search(bookName, startThisSearchTime, bookList, false);\n    }\n\n    private synchronized void addSearchBook(List<SearchBookBean> value) {\n        if (value.size() > 0) {\n            Collections.sort(value, this::compareSearchBooks);\n            for (SearchBookBean searchBookBean : value) {\n                if (searchBookBean.getName().equals(bookName)\n                        && (searchBookBean.getAuthor().equals(bookAuthor) || TextUtils.isEmpty(searchBookBean.getAuthor()) || TextUtils.isEmpty(bookAuthor))) {\n                    if (searchBookBean.getTag().equals(bookTag)) {\n                        searchBookBean.setIsCurrentSource(true);\n                    } else {\n                        searchBookBean.setIsCurrentSource(false);\n                    }\n                    boolean saveBookSource = false;\n                    BookSourceBean bookSourceBean = BookSourceManager.getBookSourceByUrl(searchBookBean.getTag());\n                    if (searchBookBean.getSearchTime() < 60 && bookSourceBean != null) {\n                        bookSourceBean.increaseWeight(100 / (10 + searchBookBean.getSearchTime()));\n                        saveBookSource = true;\n                    }\n                    if (shelfLastChapter > 0 && bookSourceBean != null) {\n                        int lastChapter = BookshelfHelp.guessChapterNum(searchBookBean.getLastChapter());\n                        if (lastChapter > shelfLastChapter) {\n                            bookSourceBean.increaseWeight(100);\n                            saveBookSource = true;\n                        }\n                    }\n                    if (saveBookSource) {\n                        DbHelper.getDaoSession().getBookSourceBeanDao().insertOrReplace(bookSourceBean);\n                    }\n                    DbHelper.getDaoSession().getSearchBookBeanDao().insertOrReplace(searchBookBean);\n                    if (StringUtils.isTrimEmpty(searchView.getQuery().toString()) || searchBookBean.getOrigin().equals(searchView.getQuery().toString())) {\n                        handler.post(() -> adapter.addSourceAdapter(searchBookBean));\n                    }\n                    break;\n                }\n            }\n        }\n    }\n\n    private int compareSearchBooks(SearchBookBean s1, SearchBookBean s2) {\n        boolean s1tag = s1.getTag().equals(bookTag);\n        boolean s2tag = s2.getTag().equals(bookTag);\n        if (s2tag && !s1tag)\n            return 1;\n        else if (s1tag && !s2tag)\n            return -1;\n        int result = Long.compare(s2.getAddTime(), s1.getAddTime());\n        if (result != 0)\n            return result;\n        result = Integer.compare(s2.getLastChapterNum(), s1.getLastChapterNum());\n        if (result != 0)\n            return result;\n        return Integer.compare(s2.getWeight(), s1.getWeight());\n    }\n\n    private void stopChangeSource() {\n        compositeDisposable.dispose();\n        if (searchBookModel != null) {\n            searchBookModel.stopSearch();\n        }\n    }\n\n    public interface Callback {\n        void changeSource(SearchBookBean searchBookBean);\n    }\n\n    @Subscribe(thread = EventThread.MAIN_THREAD, tags = {@Tag(RxBusTag.UP_SEARCH_BOOK)})\n    public void upSearchBook(SearchBookBean searchBookBean) {\n        if (!Objects.equals(book.getBookInfoBean().getName(), searchBookBean.getName())\n                || !Objects.equals(book.getBookInfoBean().getAuthor(), searchBookBean.getAuthor())) {\n            return;\n        }\n        for (int i = 0; i < adapter.getSearchBookBeans().size(); i++) {\n            if (adapter.getSearchBookBeans().get(i).getTag().equals(searchBookBean.getTag())\n                    && !adapter.getSearchBookBeans().get(i).getLastChapter().equals(searchBookBean.getLastChapter())) {\n                adapter.getSearchBookBeans().get(i).setLastChapter(searchBookBean.getLastChapter());\n                adapter.notifyItemChanged(i);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/DownLoadDialog.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport com.kunfei.bookshelf.R;\n\npublic class DownLoadDialog extends BaseDialog {\n    private Context context;\n    private EditText edtStart;\n    private EditText edtEnd;\n    private TextView tvCancel;\n    private TextView tvDownload;\n\n    public static DownLoadDialog builder(Context context, int startIndex, int endIndex, final int all) {\n        return new DownLoadDialog(context, startIndex, endIndex, all);\n    }\n\n    private DownLoadDialog(Context context, int startIndex, int endIndex, final int all) {\n        super(context, R.style.alertDialogTheme);\n        this.context = context;\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_download_choice, null);\n        bindView(view, startIndex, endIndex, all);\n        setContentView(view);\n    }\n\n    private void bindView(View view, int startIndex, int endIndex, final int all) {\n        View llContent = view.findViewById(R.id.ll_content);\n        llContent.setOnClickListener(null);\n        edtStart = view.findViewById(R.id.edt_start);\n        edtEnd = view.findViewById(R.id.edt_end);\n        tvCancel = view.findViewById(R.id.tv_cancel);\n        tvDownload = view.findViewById(R.id.tv_download);\n        edtStart.setText(String.valueOf(startIndex + 1));\n        edtEnd.setText(String.valueOf(endIndex + 1));\n        edtStart.addTextChangedListener(new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n\n            }\n\n            @Override\n            public void onTextChanged(CharSequence s, int start, int before, int count) {\n\n            }\n\n            @Override\n            public void afterTextChanged(Editable s) {\n                if (edtStart.getText().length() > 0) {\n                    try {\n                        int temp = Integer.parseInt(edtStart.getText().toString().trim());\n                        if (temp > all) {\n                            edtStart.setText(String.valueOf(all));\n                            edtStart.setSelection(edtStart.getText().length());\n                            Toast.makeText(context, \"超过总章节\", Toast.LENGTH_SHORT).show();\n                        } else if (temp <= 0) {\n                            edtStart.setText(String.valueOf(1));\n                            edtStart.setSelection(edtStart.getText().length());\n                        }\n                    } catch (Exception e) {\n                        e.printStackTrace();\n                    }\n                }\n            }\n        });\n        edtEnd.addTextChangedListener(new TextWatcher() {\n            @Override\n            public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n\n            }\n\n            @Override\n            public void onTextChanged(CharSequence s, int start, int before, int count) {\n\n            }\n\n            @Override\n            public void afterTextChanged(Editable s) {\n                if (edtEnd.getText().length() > 0) {\n                    try {\n                        int temp = Integer.parseInt(edtEnd.getText().toString().trim());\n                        if (temp > all) {\n                            edtEnd.setText(String.valueOf(all));\n                            edtEnd.setSelection(edtEnd.getText().length());\n                            Toast.makeText(context, \"超过总章节\", Toast.LENGTH_SHORT).show();\n                        } else if (temp <= 0) {\n                            edtEnd.setText(String.valueOf(1));\n                            edtEnd.setSelection(edtEnd.getText().length());\n                        }\n                    } catch (Exception e) {\n                        e.printStackTrace();\n                    }\n                }\n            }\n        });\n        tvCancel.setOnClickListener(v -> dismiss());\n    }\n\n    public DownLoadDialog setPositiveButton(Callback callback) {\n        tvDownload.setOnClickListener(v -> {\n            if (edtStart.getText().length() > 0 && edtEnd.getText().length() > 0) {\n                if (Integer.parseInt(edtStart.getText().toString()) > Integer.parseInt(edtEnd.getText().toString())) {\n                    Toast.makeText(context, \"输入错误\", Toast.LENGTH_SHORT).show();\n                } else {\n                    callback.download(Integer.parseInt(edtStart.getText().toString()) - 1, Integer.parseInt(edtEnd.getText().toString()) - 1);\n                }\n                dismiss();\n            } else {\n                Toast.makeText(context, \"请输入要离线的章节\", Toast.LENGTH_SHORT).show();\n            }\n        });\n        return this;\n    }\n\n    public interface Callback {\n        void download(int start, int end);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/InputDialog.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.ImageView;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.widget.views.ATEAutoCompleteTextView;\n\nimport java.util.List;\n\n/**\n * 输入框\n */\npublic class InputDialog extends BaseDialog {\n    private boolean showDel = false;\n    private TextView tvTitle;\n    private ATEAutoCompleteTextView etInput;\n    private TextView tvOk;\n    private Callback callback = null;\n    private final Context context;\n\n    public static InputDialog builder(Context context) {\n        return new InputDialog(context);\n    }\n\n    private InputDialog(Context context) {\n        super(context, R.style.alertDialogTheme);\n        this.context = context;\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_input, null);\n        setContentView(view);\n        bindView(view);\n    }\n\n    public InputDialog setShowDel(boolean showDel) {\n        this.showDel = showDel;\n        return this;\n    }\n\n    public InputDialog setDefaultValue(String defaultValue) {\n        if (defaultValue != null) {\n            etInput.setTextSize(2, 16); // 2 --> sp\n            etInput.setText(defaultValue);\n            etInput.setSelectAllOnFocus(true);\n        }\n        return this;\n    }\n\n    public InputDialog setTitle(String title) {\n        tvTitle.setText(title);\n        return this;\n    }\n\n    public InputDialog setAdapterValues(List<String> adapterValues) {\n        if (adapterValues != null) {\n            MyAdapter mAdapter = new MyAdapter(context, adapterValues);\n            etInput.setAdapter(mAdapter);\n        }\n        return this;\n    }\n\n    private void bindView(View view) {\n        View llContent = view.findViewById(R.id.ll_content);\n        llContent.setOnClickListener(null);\n        tvTitle = view.findViewById(R.id.tv_title);\n        etInput = view.findViewById(R.id.et_input);\n        tvOk = view.findViewById(R.id.tv_ok);\n    }\n\n    public InputDialog setCallback(Callback callback) {\n        this.callback = callback;\n        tvOk.setOnClickListener(view -> {\n            callback.setInputText(etInput.getText().toString());\n            dismiss();\n        });\n        return this;\n    }\n\n    class MyAdapter extends ArrayAdapter<String> {\n\n        MyAdapter(@NonNull Context context, @NonNull List<String> objects) {\n            super(context, R.layout.item_1line_text_and_del, objects);\n        }\n\n        @NonNull\n        @Override\n        public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {\n            View view;\n            if (convertView == null) {\n                view = LayoutInflater.from(context).inflate(R.layout.item_1line_text_and_del, parent, false);\n            } else {\n                view = convertView;\n            }\n            TextView tv = view.findViewById(R.id.text);\n            ImageView iv = view.findViewById(R.id.iv_del);\n            if (showDel) {\n                iv.setVisibility(View.VISIBLE);\n            } else {\n                iv.setVisibility(View.GONE);\n            }\n            String value = String.valueOf(getItem(position));\n            tv.setText(value);\n            iv.setOnClickListener(v -> {\n                remove(value);\n                if (callback != null) {\n                    callback.delete(value);\n                }\n                etInput.showDropDown();\n            });\n            return view;\n        }\n    }\n\n\n    /**\n     * 输入book地址确定\n     */\n    public interface Callback {\n        void setInputText(String inputText);\n\n        void delete(String value);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/MoDialogHUD.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.os.Handler;\nimport android.view.Gravity;\nimport android.view.KeyEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.Animation;\nimport android.view.animation.AnimationUtils;\nimport android.widget.FrameLayout;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\n\n/**\n * 对话框\n */\npublic class MoDialogHUD {\n    private Boolean isFinishing = false;\n\n    private Context context;\n    private ViewGroup decorView;//activity的根View\n    private ViewGroup rootView;// mSharedView 的 根View\n    private MoDialogView mSharedView;\n\n    private OnDismissListener dismissListener;\n    private Animation inAnim;\n    private Animation outAnim;\n\n    private Boolean canBack = false;\n    private Animation.AnimationListener outAnimListener = new Animation.AnimationListener() {\n        @Override\n        public void onAnimationStart(Animation animation) {\n            isFinishing = true;\n        }\n\n        @Override\n        public void onAnimationEnd(Animation animation) {\n            dismissImmediately();\n        }\n\n        @Override\n        public void onAnimationRepeat(Animation animation) {\n\n        }\n    };\n\n    public MoDialogHUD(Context context) {\n        this.context = context;\n        initViews();\n        initCenter();\n        initAnimation();\n    }\n\n    private void initAnimation() {\n        inAnim = getInAnimation();\n        outAnim = getOutAnimation();\n    }\n\n    private void initFromTopRight() {\n        inAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_in_top_right);\n        outAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_out_top_right);\n    }\n\n    private void initFromBottomRight() {\n        inAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_in_bottom_right);\n        outAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_out_bottom_right);\n    }\n\n    private void initFromBottomAnimation() {\n        inAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_bottom_in);\n        outAnim = AnimationUtils.loadAnimation(context, R.anim.moprogress_bottom_out);\n    }\n\n    private void initCenter() {\n        mSharedView.setGravity(Gravity.CENTER);\n        if (mSharedView != null) {\n            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mSharedView.getLayoutParams();\n            if (layoutParams != null) {\n                layoutParams.setMargins(0, 0, 0, 0);\n                mSharedView.setLayoutParams(layoutParams);\n            }\n            mSharedView.setPadding(0, 0, 0, 0);\n        }\n    }\n\n    private void initBottom() {\n        mSharedView.setGravity(Gravity.BOTTOM);\n        if (mSharedView != null) {\n            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mSharedView.getLayoutParams();\n            if (layoutParams != null) {\n                layoutParams.setMargins(0, 0, 0, 0);\n                mSharedView.setLayoutParams(layoutParams);\n            }\n            mSharedView.setPadding(0, 0, 0, 0);\n        }\n    }\n\n    private void initMarRightTop() {\n        mSharedView.setGravity(Gravity.RIGHT | Gravity.TOP);\n        if (mSharedView != null) {\n            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mSharedView.getLayoutParams();\n            if (layoutParams != null) {\n                layoutParams.setMargins(0, 0, 0, 0);\n                mSharedView.setLayoutParams(layoutParams);\n            }\n            mSharedView.setPadding(0, 0, 0, 0);\n        }\n    }\n\n    private void initViews() {\n        decorView = ((Activity) context).getWindow().getDecorView().findViewById(android.R.id.content);\n        rootView = new FrameLayout(context);\n        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT\n        );\n        rootView.setLayoutParams(layoutParams);\n        rootView.setClickable(true);\n        rootView.setBackgroundColor(context.getResources().getColor(R.color.btn_bg_press_tp));\n\n        mSharedView = new MoDialogView(context);\n\n    }\n\n    private Animation getInAnimation() {\n        return AnimationUtils.loadAnimation(context, R.anim.moprogress_in);\n    }\n\n    private Animation getOutAnimation() {\n        return AnimationUtils.loadAnimation(context, R.anim.moprogress_out);\n    }\n\n    private boolean isShowing() {\n        return rootView.getParent() != null;\n    }\n\n    private void onAttached() {\n        decorView.addView(rootView);\n        if (mSharedView.getParent() != null)\n            ((ViewGroup) mSharedView.getParent()).removeView(mSharedView);\n        rootView.addView(mSharedView);\n\n        isFinishing = false;\n    }\n\n    public void dismiss() {\n        //消失动画\n        if (mSharedView != null && rootView != null && mSharedView.getParent() != null) {\n            SoftInputUtil.hideIMM(rootView);\n            if (!isFinishing) {\n                new Handler().post(() -> {\n                    outAnim.setAnimationListener(outAnimListener);\n                    mSharedView.getChildAt(0).startAnimation(outAnim);\n                });\n            }\n        }\n    }\n\n    public Boolean isShow() {\n        return (mSharedView != null && mSharedView.getParent() != null);\n    }\n\n    private void dismissImmediately() {\n        if (dismissListener != null) {\n            dismissListener.onDismiss();\n            dismissListener = null;\n        }\n        if (mSharedView != null && rootView != null && mSharedView.getParent() != null) {\n            new Handler().post(() -> {\n                rootView.removeView(mSharedView);\n                decorView.removeView(rootView);\n            });\n        }\n        isFinishing = false;\n    }\n\n    /**\n     * 返回键事件\n     */\n    public Boolean onKeyDown(int keyCode, KeyEvent event) {\n        if (keyCode == KeyEvent.KEYCODE_BACK) {\n            if (isShowing()) {\n                if (canBack) {\n                    dismiss();\n                }\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * 加载动画\n     */\n    public void showLoading(String msg) {\n        initCenter();\n        initAnimation();\n        canBack = false;\n        rootView.setOnClickListener(null);\n        if (!isShowing()) {\n            onAttached();\n        }\n        mSharedView.showLoading(msg);\n        mSharedView.getChildAt(0).startAnimation(inAnim);\n    }\n\n    /**\n     * 单个按钮的提示信息\n     */\n    public void showInfo(String msg) {\n        initCenter();\n        initAnimation();\n        canBack = true;\n        rootView.setOnClickListener(null);\n        mSharedView.showInfo(msg, v -> dismiss());\n        if (!isShowing()) {\n            onAttached();\n        }\n        mSharedView.getChildAt(0).startAnimation(inAnim);\n    }\n\n    /**\n     * 单个按钮的提示信息\n     */\n    public void showInfo(String msg, String btnText, View.OnClickListener listener) {\n        initCenter();\n        initAnimation();\n        canBack = true;\n        rootView.setOnClickListener(null);\n        mSharedView.showInfo(msg, btnText, listener);\n        if (!isShowing()) {\n            onAttached();\n        }\n        mSharedView.getChildAt(0).startAnimation(inAnim);\n    }\n\n    /**\n     * 两个不同等级的按钮\n     */\n    public void showTwoButton(String msg, String b_f, View.OnClickListener c_f, String b_s, View.OnClickListener c_s, boolean canBack) {\n        initCenter();\n        initAnimation();\n        this.canBack = canBack;\n        rootView.setOnClickListener(v -> dismiss());\n        mSharedView.showTwoButton(msg, b_f, c_f, b_s, c_s);\n        if (!isShowing()) {\n            onAttached();\n        }\n        mSharedView.getChildAt(0).startAnimation(inAnim);\n    }\n\n    /**\n     * 显示一段文本\n     */\n    public void showText(String text) {\n        initCenter();\n        initAnimation();\n        canBack = true;\n        rootView.setOnClickListener(v -> dismiss());\n        mSharedView.showText(text);\n        if (!isShowing()) {\n            onAttached();\n        }\n        mSharedView.getChildAt(0).startAnimation(inAnim);\n    }\n\n    /**\n     * 显示asset Markdown\n     */\n    public void showAssetMarkdown(String assetFileName) {\n        initCenter();\n        initAnimation();\n        canBack = true;\n        rootView.setOnClickListener(v -> dismiss());\n        mSharedView.showAssetMarkdown(assetFileName);\n        if (!isShowing()) {\n            onAttached();\n        }\n        mSharedView.getChildAt(0).startAnimation(inAnim);\n    }\n\n    public void showImageText(Bitmap bitmap, String text) {\n        initCenter();\n        initAnimation();\n        canBack = true;\n        rootView.setOnClickListener(v -> dismiss());\n        mSharedView.showImageText(bitmap, text);\n        if (!isShowing()) {\n            onAttached();\n        }\n        mSharedView.getChildAt(0).startAnimation(inAnim);\n    }\n\n    private interface OnDismissListener {\n        void onDismiss();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/MoDialogView.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.TextView;\n\nimport androidx.cardview.widget.CardView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.ReadAssets;\nimport com.kunfei.bookshelf.widget.RotateLoading;\n\nimport ru.noties.markwon.Markwon;\n\n/**\n * 对话框\n */\npublic class MoDialogView extends LinearLayout {\n    private Context context;\n\n    public MoDialogView(Context context) {\n        this(context, null);\n    }\n\n    public MoDialogView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public MoDialogView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        this.context = context;\n        setOrientation(VERTICAL);\n    }\n\n    //转圈的载入\n    public void showLoading(String text) {\n        removeAllViews();\n        LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_loading, this, true);\n        TextView msgTv = findViewById(R.id.msg_tv);\n        if (text != null && text.length() > 0) {\n            msgTv.setText(text);\n        }\n\n        RotateLoading rlLoading = findViewById(R.id.rl_loading);\n        rlLoading.start();\n    }\n\n    //单个按钮的信息提示框\n    public void showInfo(String msg, final OnClickListener listener) {\n        removeAllViews();\n        LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_infor, this, true);\n        View llContent = findViewById(R.id.ll_content);\n        llContent.setOnClickListener(null);\n        TextView msgTv = findViewById(R.id.msg_tv);\n        msgTv.setText(msg);\n        TextView tvClose = findViewById(R.id.tv_close);\n        tvClose.setOnClickListener(listener);\n    }\n\n    //单个按钮的信息提示框\n    public void showInfo(String msg, String btnText, final OnClickListener listener) {\n        removeAllViews();\n        LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_infor, this, true);\n        View llContent = findViewById(R.id.ll_content);\n        llContent.setOnClickListener(null);\n        TextView msgTv = findViewById(R.id.msg_tv);\n        msgTv.setText(msg);\n        TextView tvClose = findViewById(R.id.tv_close);\n        tvClose.setText(btnText);\n        tvClose.setOnClickListener(listener);\n    }\n\n    //////////////////////两个不同等级的按钮//////////////////////\n    public void showTwoButton(String msg, String b_f, OnClickListener c_f, String b_s, OnClickListener c_s) {\n        removeAllViews();\n        LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_two, this, true);\n        TextView tvMsg = findViewById(R.id.tv_msg);\n        TextView tvCancel = findViewById(R.id.tv_cancel);\n        TextView tvDone = findViewById(R.id.tv_done);\n        tvMsg.setText(msg);\n        tvCancel.setText(b_f);\n        tvCancel.setOnClickListener(c_f);\n        tvDone.setText(b_s);\n        tvDone.setOnClickListener(c_s);\n    }\n\n    /**\n     * 显示一段文本\n     */\n    public void showText(String text) {\n        removeAllViews();\n        LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_text_large, this, true);\n        TextView textView = findViewById(R.id.tv_can_copy);\n        textView.setText(text);\n    }\n\n    /**\n     * 显示asset Markdown\n     */\n    public void showAssetMarkdown(String assetFileName) {\n        removeAllViews();\n        LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_markdown, this, true);\n        TextView tvMarkdown = findViewById(R.id.tv_markdown);\n        Markwon.create(tvMarkdown.getContext()).setMarkdown(tvMarkdown, ReadAssets.getText(context, assetFileName));\n    }\n\n    /**\n     * 显示图像和文本\n     */\n    public void showImageText(Bitmap bitmap, String text) {\n        removeAllViews();\n        LayoutInflater.from(getContext()).inflate(R.layout.mo_dialog_image_text, this, true);\n        CardView cardView = findViewById(R.id.cv_content);\n        cardView.setOnClickListener(null);\n        ImageView imageView = findViewById(R.id.image_view);\n        TextView tvCanCopy = findViewById(R.id.tv_can_copy);\n        int imageWidth = Math.min(cardView.getWidth(), cardView.getHeight());\n        imageView.setMaxWidth(imageWidth - 20);\n        imageView.setMaxHeight(imageWidth - 20);\n        imageView.setImageBitmap(bitmap);\n        tvCanCopy.setText(text);\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/PageKeyDialog.kt",
    "content": "package com.kunfei.bookshelf.widget.modialog\n\nimport android.content.Context\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport com.kunfei.bookshelf.MApplication\nimport com.kunfei.bookshelf.databinding.DialogPageKeyBinding\nimport com.kunfei.bookshelf.utils.SoftInputUtil\nimport org.jetbrains.anko.sdk27.listeners.onClick\n\n\nclass PageKeyDialog(context: Context) : BaseDialog(context) {\n\n    val binding = DialogPageKeyBinding.inflate(LayoutInflater.from(context))\n\n    init {\n        setContentView(binding.root)\n        binding.etPrev.setText(MApplication.getConfigPreferences().getInt(\"prevKeyCode\", 0).toString())\n        binding.etNext.setText(MApplication.getConfigPreferences().getInt(\"nextKeyCode\", 0).toString())\n        binding.tvOk.onClick {\n            val edit = MApplication.getConfigPreferences().edit()\n            binding.etPrev.text?.let {\n                edit.putInt(\"prevKeyCode\", it.toString().toInt())\n            }\n            binding.etNext.text?.let {\n                edit.putInt(\"nextKeyCode\", it.toString().toInt())\n            }\n            edit.apply()\n            dismiss()\n        }\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        if (keyCode != KeyEvent.KEYCODE_BACK) {\n            if (binding.etPrev.hasFocus()) {\n                binding.etPrev.setText(keyCode.toString())\n            } else if (binding.etNext.hasFocus()) {\n                binding.etNext.setText(keyCode.toString())\n            }\n            return true\n        }\n        return super.onKeyDown(keyCode, event)\n    }\n\n    override fun dismiss() {\n        super.dismiss()\n        SoftInputUtil.hideIMM(currentFocus)\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/ReplaceRuleDialog.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.text.Editable;\nimport android.text.TextWatcher;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.inputmethod.EditorInfo;\nimport android.widget.CheckBox;\nimport android.widget.TextView;\n\nimport androidx.appcompat.widget.AppCompatEditText;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.bean.ReplaceRuleBean;\n\npublic class ReplaceRuleDialog extends BaseDialog {\n    private Context context;\n    private AppCompatEditText tieReplaceSummary;\n    private AppCompatEditText tieReplaceRule;\n    private AppCompatEditText tieReplaceTo;\n    private AppCompatEditText tieUseTo;\n    private CheckBox cbUseRegex;\n    private TextView tvOk;\n\n    private ReplaceRuleBean replaceRuleBean;\n    private BookShelfBean bookShelfBean;\n\n    private TextView replace_ad_intro, tvtitle;\n    private View til_replace_to;\n\n    // 替换规则编辑UI的模式  1 默认  2 广告话术  3 添加广告话术\n    private int ReplaceUIMode = 1;\n    public static int DefaultUI = 1, AdUI = 2, AddAdUI = 3;\n    private String str_summary = \"\";\n\n\n    public static ReplaceRuleDialog builder(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean, int replaceUIMode) {\n        return new ReplaceRuleDialog(context, replaceRuleBean, bookShelfBean, replaceUIMode);\n    }\n\n\n    public static ReplaceRuleDialog builder(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean) {\n        return new ReplaceRuleDialog(context, replaceRuleBean, bookShelfBean);\n    }\n\n    private ReplaceRuleDialog(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean) {\n        super(context, R.style.alertDialogTheme);\n        this.context = context;\n        this.replaceRuleBean = replaceRuleBean;\n        this.bookShelfBean = bookShelfBean;\n\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_replace_rule, null);\n        bindView(view);\n        setContentView(view);\n\n    }\n\n    private ReplaceRuleDialog(Context context, ReplaceRuleBean replaceRuleBean, BookShelfBean bookShelfBean, int replaceUIMod) {\n        super(context, R.style.alertDialogTheme);\n        this.context = context;\n        this.replaceRuleBean = replaceRuleBean;\n        this.bookShelfBean = bookShelfBean;\n        this.ReplaceUIMode = replaceUIMod;\n\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_replace_rule, null);\n        bindView(view);\n        setContentView(view);\n    }\n\n    private void bindView(View view) {\n        View llContent = view.findViewById(R.id.ll_content);\n        llContent.setOnClickListener(null);\n\n        tieReplaceRule = view.findViewById(R.id.tie_replace_rule);\n        tieReplaceSummary = view.findViewById(R.id.tie_replace_summary);\n        tieReplaceTo = view.findViewById(R.id.tie_replace_to);\n        tieUseTo = view.findViewById(R.id.tie_use_to);\n        cbUseRegex = view.findViewById(R.id.cb_use_regex);\n        tvOk = view.findViewById(R.id.tv_ok);\n        replace_ad_intro = view.findViewById(R.id.replace_ad_intro);\n        tvtitle = view.findViewById(R.id.title);\n        til_replace_to=view.findViewById(R.id.til_replace_to);\n        if (replaceRuleBean != null) {\n            tieReplaceSummary.setText(replaceRuleBean.getReplaceSummary());\n            tieReplaceTo.setText(replaceRuleBean.getReplacement());\n            tieReplaceRule.setText(replaceRuleBean.getRegex());\n            tieUseTo.setText(replaceRuleBean.getUseTo());\n            cbUseRegex.setChecked(replaceRuleBean.getIsRegex());\n\n            // 初始化广告话术规则的UI\n            if (ReplaceUIMode == DefaultUI) {\n                if (replaceRuleBean.getReplaceSummary().matches(\"^\" + view.getContext().getString(R.string.replace_ad) + \".*\"))\n                    ReplaceUIMode = AdUI;\n            }\n            if (ReplaceUIMode > DefaultUI) {\n//                tieReplaceTo.setVisibility(View.GONE);\n                til_replace_to.setVisibility(View.GONE);\n                cbUseRegex.setVisibility(View.GONE);\n                replace_ad_intro.setVisibility(View.VISIBLE);\n                tieReplaceSummary.setInputType(EditorInfo.TYPE_NULL);\n                tieReplaceRule.setMaxLines(8);\n\n                if (ReplaceUIMode == AdUI) {\n                    tvtitle.setText(view.getContext().getString(R.string.replace_ad_title));\n                } else {\n                    tvtitle.setText(view.getContext().getString(R.string.replace_add_ad_title));\n                }\n                str_summary = view.getContext().getString(R.string.replace_ad);\n                TextWatcher mTextWatcher = new TextWatcher() {\n                    private CharSequence temp;\n\n                    @Override\n                    public void onTextChanged(CharSequence s, int start, int before, int count) {\n                        temp = s;\n                        String str=s.toString().trim();\n                        if (str.replaceAll(\"[\\\\s,]\", \"\").length() > 0)\n                            tieReplaceSummary.setText(str_summary + \"-\" + str);\n                        else\n                            tieReplaceSummary.setText(str_summary);\n                    }\n\n                    @Override\n                    public void beforeTextChanged(CharSequence s, int start, int count, int after) {\n\n                    }\n\n                    @Override\n                    public void afterTextChanged(Editable s) {\n/*                        if (s.toString().replaceAll(\"[\\\\s,]\", \"\").length() > 0)\n                            tieReplaceSummary.setText(str_summary + \"-\" + temp);\n                        else\n                            tieReplaceSummary.setText(str_summary);*/\n                    }\n                };\n                tieUseTo.addTextChangedListener(mTextWatcher);\n\n            }\n        } else {\n            replaceRuleBean = new ReplaceRuleBean();\n            replaceRuleBean.setEnable(true);\n            cbUseRegex.setChecked(MApplication.getConfigPreferences().getBoolean(\"useRegexInNewRule\", false));\n            if (bookShelfBean != null) {\n                tieUseTo.setText(String.format(\"%s,%s\", bookShelfBean.getBookInfoBean().getName(), bookShelfBean.getTag()));\n            }\n        }\n\n    }\n\n\n    public ReplaceRuleDialog setPositiveButton(Callback callback) {\n        tvOk.setOnClickListener(v -> {\n            replaceRuleBean.setReplaceSummary(getEditableText(tieReplaceSummary.getText()));\n            replaceRuleBean.setRegex(getEditableText(tieReplaceRule.getText()));\n            replaceRuleBean.setIsRegex(cbUseRegex.isChecked());\n            replaceRuleBean.setReplacement(getEditableText(tieReplaceTo.getText()));\n            replaceRuleBean.setUseTo(getEditableText(tieUseTo.getText()));\n            callback.onPositiveButton(replaceRuleBean);\n            dismiss();\n        });\n        return this;\n    }\n\n    private String getEditableText(Editable editable) {\n        if (editable == null) {\n            return \"\";\n        }\n        return editable.toString();\n    }\n\n    public interface Callback {\n        void onPositiveButton(ReplaceRuleBean replaceRuleBean);\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/modialog/TxtChapterRuleDialog.java",
    "content": "package com.kunfei.bookshelf.widget.modialog;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.text.Editable;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.TxtChapterRuleBean;\nimport com.kunfei.bookshelf.widget.views.ATEEditText;\n\npublic class TxtChapterRuleDialog {\n\n    private ATEEditText tieRuleName;\n    private ATEEditText tieRuleRegex;\n    private TextView tvOk;\n\n    private BaseDialog dialog;\n    private TxtChapterRuleBean txtChapterRuleBean;\n\n    public static TxtChapterRuleDialog builder(Context context, TxtChapterRuleBean txtChapterRuleBean) {\n        return new TxtChapterRuleDialog(context, txtChapterRuleBean);\n    }\n\n    private TxtChapterRuleDialog(Context context, TxtChapterRuleBean txtChapterRuleBean) {\n        if (txtChapterRuleBean != null) {\n            this.txtChapterRuleBean = txtChapterRuleBean.copy();\n        }\n        dialog = new BaseDialog(context, R.style.alertDialogTheme);\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_txt_chpater_rule, null);\n        bindView(view);\n        dialog.setContentView(view);\n    }\n\n    private void bindView(View view) {\n        tieRuleName = view.findViewById(R.id.tie_rule_name);\n        tieRuleRegex = view.findViewById(R.id.tie_rule_regex);\n        tvOk = view.findViewById(R.id.tv_ok);\n        if (txtChapterRuleBean != null) {\n            tieRuleName.setText(txtChapterRuleBean.getName());\n            tieRuleRegex.setText(txtChapterRuleBean.getRule());\n        }\n    }\n\n    public TxtChapterRuleDialog setPositiveButton(Callback callback) {\n        tvOk.setOnClickListener(v -> {\n            if (txtChapterRuleBean == null) {\n                txtChapterRuleBean = new TxtChapterRuleBean();\n            }\n            txtChapterRuleBean.setName(getEditableText(tieRuleName.getText()));\n            txtChapterRuleBean.setRule(getEditableText(tieRuleRegex.getText()));\n            callback.onPositiveButton(txtChapterRuleBean);\n            dialog.dismiss();\n        });\n        return this;\n    }\n\n    private String getEditableText(Editable editable) {\n        if (editable == null) {\n            return \"\";\n        }\n        return editable.toString();\n    }\n\n    public TxtChapterRuleDialog show() {\n        dialog.show();\n        return this;\n    }\n\n    public interface Callback {\n        void onPositiveButton(TxtChapterRuleBean txtChapterRuleBean);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/number/NumberButton.java",
    "content": "package com.kunfei.bookshelf.widget.number;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.FrameLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.kunfei.bookshelf.R;\n\nimport java.text.DecimalFormat;\n\npublic class NumberButton extends FrameLayout implements View.OnClickListener {\n    public static final int INT = 0;\n    public static final int FLOAT = 1;\n\n    private OnChangedListener onChangedListener;\n    private TextView tvNumber;\n    private DecimalFormat decimalFormat = new DecimalFormat(\"#\");\n    private int numberType = INT;\n    private float minNumber = 0;\n    private float maxNumber = 10;\n    private float stepNumber = 1;\n    private String tile = \"请选择\";\n\n    public NumberButton(Context context) {\n        this(context, null);\n    }\n\n    public NumberButton(Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    private void init(Context context) {\n        LayoutInflater.from(context).inflate(R.layout.view_number_buttom, this);\n\n        TextView addButton = findViewById(R.id.button_add);\n        addButton.setOnClickListener(this);\n        TextView subButton = findViewById(R.id.button_sub);\n        subButton.setOnClickListener(this);\n\n        tvNumber = findViewById(R.id.tv_number);\n        tvNumber.setOnClickListener(this);\n\n    }\n\n    public NumberButton setTitle(@NonNull String title) {\n        this.tile = title;\n        return this;\n    }\n\n    public void setOnChangedListener(OnChangedListener onChangedListener) {\n        this.onChangedListener = onChangedListener;\n    }\n\n    public float getNumber() {\n        try {\n            return Float.parseFloat(tvNumber.getText().toString());\n        } catch (NumberFormatException e) {\n            tvNumber.setText(decimalFormat.format(minNumber));\n            return minNumber;\n        }\n    }\n\n    public NumberButton setNumber(float number) {\n        tvNumber.setText(decimalFormat.format(number));\n        return this;\n    }\n\n    public NumberButton setFormat(String pattern) {\n        decimalFormat = new DecimalFormat(pattern);\n        return this;\n    }\n\n    public NumberButton setMinNumber(float minNumber) {\n        this.minNumber = minNumber;\n        return this;\n    }\n\n    public NumberButton setMaxNumber(float maxNumber) {\n        this.maxNumber = maxNumber;\n        return this;\n    }\n\n    public NumberButton setStepNumber(float stepNumber) {\n        this.stepNumber = stepNumber;\n        return this;\n    }\n\n    public NumberButton setNumberType(int numberType) {\n        this.numberType = numberType;\n        return this;\n    }\n\n    @Override\n    public void onClick(View view) {\n        float count = getNumber();\n        switch (view.getId()) {\n            case R.id.button_add:\n                if (count < maxNumber) {\n                    changeNumber(count + stepNumber);\n                }\n                break;\n            case R.id.button_sub:\n                if (count > minNumber) {\n                    changeNumber(count - stepNumber);\n                }\n                break;\n            case R.id.tv_number:\n                if (numberType == INT) {\n                    NumberPickerDialog npd = new NumberPickerDialog(getContext());\n                    npd.setTitle(tile)\n                            .setMaxValue((int) maxNumber)\n                            .setMinValue((int) minNumber)\n                            .setValue((int) getNumber())\n                            .setListener(this::changeNumber)\n                            .create()\n                            .show();\n                }\n                break;\n        }\n    }\n\n    private void changeNumber(float f) {\n        tvNumber.setText(decimalFormat.format(f));\n        if (onChangedListener != null) {\n            onChangedListener.numberChange(f);\n        }\n    }\n\n    public interface OnChangedListener {\n        void numberChange(float number);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/number/NumberPickerDialog.java",
    "content": "package com.kunfei.bookshelf.widget.number;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.NumberPicker;\n\nimport androidx.appcompat.app.AlertDialog;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\nimport com.kunfei.bookshelf.utils.theme.ATH;\n\npublic class NumberPickerDialog {\n    private AlertDialog.Builder builder;\n    private NumberPicker numberPicker;\n\n    NumberPickerDialog(Context context) {\n        builder = new AlertDialog.Builder(context);\n        @SuppressLint(\"InflateParams\") View view = LayoutInflater.from(context).inflate(R.layout.dialog_number_picker, null);\n        numberPicker = view.findViewById(R.id.number_picker);\n        builder.setView(view);\n    }\n\n    public NumberPickerDialog setTitle(String title) {\n        builder.setTitle(title);\n        return this;\n    }\n\n    public NumberPickerDialog setMaxValue(int value) {\n        numberPicker.setMaxValue(value);\n        return this;\n    }\n\n    public NumberPickerDialog setMinValue(int value) {\n        numberPicker.setMinValue(value);\n        return this;\n    }\n\n    public NumberPickerDialog setValue(int value) {\n        numberPicker.setValue(value);\n        return this;\n    }\n\n    public NumberPickerDialog create() {\n        builder.create();\n        return this;\n    }\n\n    public NumberPickerDialog setListener(OnClickListener onClickListener) {\n        builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {\n            numberPicker.clearFocus();\n            SoftInputUtil.hideIMM(numberPicker);\n            if (onClickListener != null) {\n                onClickListener.setNumber(numberPicker.getValue());\n            }\n        });\n        builder.setNegativeButton(R.string.cancel, null);\n        return this;\n    }\n\n    public void show() {\n        AlertDialog dialog = builder.show();\n        ATH.setAlertDialogTint(dialog);\n    }\n\n    public interface OnClickListener {\n        void setNumber(int i);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/number/NumberPickerPreference.java",
    "content": "package com.kunfei.bookshelf.widget.number;\n\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.content.res.TypedArray;\nimport android.preference.DialogPreference;\nimport android.util.AttributeSet;\nimport android.view.Gravity;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.NumberPicker;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.SoftInputUtil;\n\n/**\n * Displaying a NumberPicker in a DialogPreference\n */\npublic class NumberPickerPreference extends DialogPreference {\n    private static final String TAG = NumberPickerPreference.class.getSimpleName();\n\n    /**\n     * this variables will be initialised in 'init()'\n     */\n    //by default min value is 0\n    private int minValue = 0;\n    //by default max value is 10\n    private int maxValue = 10;\n    //get summary - hardcode if no summary is set\n    private String summaryPattern = \"number picked: %s\";\n\n    private NumberPicker numPicker;\n\n    private int numValue = minValue;\n\n    public NumberPickerPreference(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        // initializing attributes\n        init(attrs);\n    }\n\n    /**\n     * setting attributes from xml\n     * attr attributeset\n     */\n    private void init(AttributeSet attrs) {\n        TypedArray a = getContext().obtainStyledAttributes(\n                attrs,\n                R.styleable.NumberPickerPreference);\n\n        maxValue = a.getInt(R.styleable.NumberPickerPreference_MaxValue, maxValue);\n        minValue = a.getInt(R.styleable.NumberPickerPreference_MinValue, minValue);\n        summaryPattern = a.getString(R.styleable.NumberPickerPreference_android_summary);\n\n        a.recycle();\n    }\n\n    /**\n     * @return dialog view with picker inside\n     */\n    @Override\n    protected View onCreateDialogView() {\n        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(\n                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n        layoutParams.gravity = Gravity.CENTER;\n\n        numPicker = new NumberPicker(getContext());\n        numPicker.setLayoutParams(layoutParams);\n        numPicker.setMinValue(minValue);\n        numPicker.setMaxValue(maxValue);\n\n        FrameLayout dialogView = new FrameLayout(getContext());\n        dialogView.addView(numPicker);\n\n        return dialogView;\n    }\n\n\n    @Override\n    protected void onBindDialogView(@NonNull View view) {\n        super.onBindDialogView(view);\n        numPicker.setValue(getValue());\n    }\n\n    @Override\n    public void onClick(DialogInterface dialog, int which) {\n        numPicker.clearFocus();\n        SoftInputUtil.hideIMM(numPicker);\n        super.onClick(dialog, which);\n    }\n\n    /**\n     * update summary when dialog is closed\n     */\n    @Override\n    protected void onDialogClosed(boolean positiveResult) {\n        if (positiveResult) {\n            int pickerValue = numPicker.getValue();\n            updateSummary(pickerValue);\n            setValue(pickerValue);\n        }\n    }\n\n    /**\n     * if no default value is set - then set min value\n     */\n    @Override\n    protected Object onGetDefaultValue(TypedArray a, int index) {\n        return a.getInt(index, minValue);\n    }\n\n    /**\n     * SetInitialValue\n     */\n    @Override\n    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {\n        if (restorePersistedValue) {\n            setValue(getPersistedInt(minValue));\n        } else {\n            setValue((Integer) defaultValue);\n        }\n        updateSummary(getValue());\n    }\n\n    /**\n     * @return current value\n     */\n    public int getValue() {\n        return this.numValue;\n    }\n\n    /**\n     * @param value which will be stored in SharedPreferences\n     */\n    private void setValue(int value) {\n        this.numValue = value;\n        persistInt(this.numValue);\n    }\n\n    /**\n     * @return get summary pattern from xml file\n     */\n    private String getSummaryPattern() {\n        return this.summaryPattern;\n    }\n\n\n    /**\n     * value insert into summaryPattern\n     */\n    private void updateSummary(int val) {\n        setSummary(String.format(getSummaryPattern(), Integer.toString(val)));\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/ChapterProvider.java",
    "content": "package com.kunfei.bookshelf.widget.page;\n\nimport android.text.Layout;\nimport android.text.StaticLayout;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.help.ChapterContentHelp;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nclass ChapterProvider {\n    private PageLoader pageLoader;\n    private ChapterContentHelp contentHelper = new ChapterContentHelp();\n\n    ChapterProvider(PageLoader pageLoader) {\n        this.pageLoader = pageLoader;\n    }\n\n    TxtChapter dealLoadPageList(BookChapterBean chapter, boolean isPrepare) {\n        TxtChapter txtChapter = new TxtChapter(chapter.getDurChapterIndex());\n        // 判断章节是否存在\n        if (!isPrepare || pageLoader.noChapterData(chapter)) {\n            if (pageLoader instanceof PageLoaderNet && !NetworkUtils.isNetWorkAvailable()) {\n                txtChapter.setStatus(TxtChapter.Status.ERROR);\n                txtChapter.setMsg(\"网络连接不可用\");\n            }\n            return txtChapter;\n        }\n        String content;\n        try {\n            content = pageLoader.getChapterContent(chapter);\n        } catch (Exception e) {\n            txtChapter.setStatus(TxtChapter.Status.ERROR);\n            txtChapter.setMsg(\"读取内容出错\\n\" + e.getLocalizedMessage());\n            return txtChapter;\n        }\n        if (content == null) {\n            txtChapter.setStatus(TxtChapter.Status.ERROR);\n            txtChapter.setMsg(\"缓存文件不存在\");\n            return txtChapter;\n        }\n        return loadPageList(chapter, content);\n    }\n\n    /**\n     * 将章节数据，解析成页面列表\n     *\n     * @param chapter：章节信息\n     * @param content：章节的文本\n     */\n    private TxtChapter loadPageList(BookChapterBean chapter, @NonNull String content) {\n        //生成的页面\n        TxtChapter txtChapter = new TxtChapter(chapter.getDurChapterIndex());\n        if (pageLoader.book.isAudio()) {\n            txtChapter.setStatus(TxtChapter.Status.FINISH);\n            txtChapter.setMsg(content);\n            TxtPage page = new TxtPage(txtChapter.getTxtPageList().size());\n            page.setTitle(chapter.getDurChapterName());\n            page.addLine(chapter.getDurChapterName());\n            page.addLine(content);\n            page.setTitleLines(1);\n            txtChapter.addPage(page);\n            addTxtPageLength(txtChapter, page.getContent().length());\n            txtChapter.addPage(page);\n            return txtChapter;\n        }\n        Log.i(\"content-1\",chapter.getDurChapterName()+\"\\n\"+content.substring(content.length()/3*2));\n        content = contentHelper.replaceContent(pageLoader.book.getBookInfoBean().getName(), pageLoader.book.getTag(), content, pageLoader.book.getReplaceEnable());\n\n//        Log.i(\"chapterName\",chapter.getDurChapterName());\n//      方便debug\n//        if(chapter.getDurChapterName().matches(\".*幽魂.*\"))\n        {\n//               Log.i(\"content\",content);\n\n        content = contentHelper.LightNovelParagraph2(content,chapter.getDurChapterName());\n        }\n        String[] allLine = content.split(\"\\n\");\n        List<String> lines = new ArrayList<>();\n        List<TxtLine> txtLists = new ArrayList<>();//记录每个字的位置 //pzl\n        int rHeight = pageLoader.mVisibleHeight - pageLoader.contentMarginHeight * 2;\n        int titleLinesCount = 0;\n        boolean showTitle = pageLoader.readBookControl.getShowTitle(); // 是否展示标题\n        String paragraph = null;\n        if (showTitle) {\n            paragraph = contentHelper.replaceContent(pageLoader.book.getBookInfoBean().getName(), pageLoader.book.getTag(), chapter.getDurChapterName(), pageLoader.book.getReplaceEnable());\n            paragraph = paragraph.trim() + \"\\n\";\n        }\n        int i = 1;\n        while (showTitle || i < allLine.length) {\n            // 重置段落\n            if (!showTitle) {\n                paragraph = allLine[i].replaceAll(\"\\\\s\", \" \").trim();\n                i++;\n                if (paragraph.equals(\"\")) continue;\n                paragraph = pageLoader.indent + paragraph + \"\\n\";\n            }\n            addParagraphLength(txtChapter, paragraph.length());\n            int wordCount;\n            String subStr;\n            while (paragraph.length() > 0) {\n                //当前空间，是否容得下一行文字\n                if (showTitle) {\n                    rHeight -= pageLoader.mTitlePaint.getTextSize();\n                } else {\n                    rHeight -= pageLoader.mTextPaint.getTextSize();\n                }\n                // 一页已经填充满了，创建 TextPage\n                if (rHeight <= 0) {\n                    // 创建Page\n                    TxtPage page = new TxtPage(txtChapter.getTxtPageList().size());\n                    page.setTitle(chapter.getDurChapterName());\n                    page.addLines(lines);\n                    page.setTxtLists(new ArrayList<>(txtLists));\n                    page.setTitleLines(titleLinesCount);\n                    txtChapter.addPage(page);\n                    addTxtPageLength(txtChapter, page.getContent().length());\n                    // 重置Lines\n                    lines.clear();\n                    txtLists.clear();//pzl\n                    rHeight = pageLoader.mVisibleHeight - pageLoader.contentMarginHeight * 2;\n                    titleLinesCount = 0;\n\n                    continue;\n                }\n\n                //测量一行占用的字节数\n                if (showTitle) {\n                    Layout tempLayout = new StaticLayout(paragraph, pageLoader.mTitlePaint, pageLoader.mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false);\n                    wordCount = tempLayout.getLineEnd(0);\n                } else {\n                    Layout tempLayout = new StaticLayout(paragraph, pageLoader.mTextPaint, pageLoader.mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false);\n                    wordCount = tempLayout.getLineEnd(0);\n                }\n\n                subStr = paragraph.substring(0, wordCount);\n                if (!subStr.equals(\"\\n\")) {\n                    //将一行字节，存储到lines中\n                    lines.add(subStr);\n                    //begin pzl\n                    //记录每个字的位置\n                    char[] cs = subStr.toCharArray();\n                    TxtLine txtList = new TxtLine();//每一行\n                    txtList.setCharsData(new ArrayList<TxtChar>());\n                    for (char c : cs) {\n                        String mesasrustr = String.valueOf(c);\n                        float charwidth = pageLoader.mTextPaint.measureText(mesasrustr);\n                        if (showTitle) {\n                            charwidth = pageLoader.mTitlePaint.measureText(mesasrustr);\n                        }\n                        TxtChar txtChar = new TxtChar();\n                        txtChar.setChardata(c);\n                        txtChar.setCharWidth(charwidth);//字宽\n                        txtChar.setIndex(66);//每页每个字的位置\n                        txtList.getCharsData().add(txtChar);\n                    }\n                    txtLists.add(txtList);\n                    //end pzl\n                    //设置段落间距\n                    if (showTitle) {\n                        titleLinesCount += 1;\n                        rHeight -= pageLoader.mTitleInterval;\n                    } else {\n                        rHeight -= pageLoader.mTextInterval;\n                    }\n                }\n                //裁剪\n                paragraph = paragraph.substring(wordCount);\n            }\n\n            //增加段落的间距\n            if (!showTitle && lines.size() != 0) {\n                rHeight = rHeight - pageLoader.mTextPara + pageLoader.mTextInterval;\n            }\n\n            if (showTitle) {\n                rHeight = rHeight - pageLoader.mTitlePara + pageLoader.mTitleInterval;\n                showTitle = false;\n            }\n        }\n\n        if (lines.size() != 0) {\n            //创建Page\n            TxtPage page = new TxtPage(txtChapter.getTxtPageList().size());\n            page.setTitle(chapter.getDurChapterName());\n            page.addLines(lines);\n            page.setTxtLists(new ArrayList<>(txtLists));\n            page.setTitleLines(titleLinesCount);\n            txtChapter.addPage(page);\n            addTxtPageLength(txtChapter, page.getContent().length());\n            //重置Lines\n            lines.clear();\n            txtLists.clear();\n        }\n        if (txtChapter.getPageSize() > 0) {\n            txtChapter.setStatus(TxtChapter.Status.FINISH);\n        } else {\n            txtChapter.setStatus(TxtChapter.Status.ERROR);\n            txtChapter.setMsg(\"未加载到内容\");\n        }\n        return txtChapter;\n    }\n\n    private void addTxtPageLength(TxtChapter txtChapter, int length) {\n        if (txtChapter.getTxtPageLengthList().isEmpty()) {\n            txtChapter.addTxtPageLength(length);\n        } else {\n            txtChapter.addTxtPageLength(txtChapter.getTxtPageLengthList().get(txtChapter.getTxtPageLengthList().size() - 1) + length);\n        }\n    }\n\n    private void addParagraphLength(TxtChapter txtChapter, int length) {\n        if (txtChapter.getParagraphLengthList().isEmpty()) {\n            txtChapter.addParagraphLength(length);\n        } else {\n            txtChapter.addParagraphLength(txtChapter.getParagraphLengthList().get(txtChapter.getParagraphLengthList().size() - 1) + length);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoader.java",
    "content": "package com.kunfei.bookshelf.widget.page;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Point;\nimport android.graphics.Rect;\nimport android.graphics.Typeface;\nimport android.os.Build;\nimport android.text.Layout;\nimport android.text.StaticLayout;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.widget.Toast;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.constant.AppConstant;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.help.ChapterContentHelp;\nimport com.kunfei.bookshelf.help.ReadBookControl;\nimport com.kunfei.bookshelf.service.ReadAloudService;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\nimport com.kunfei.bookshelf.widget.page.animation.PageAnimation;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleObserver;\nimport io.reactivex.SingleOnSubscribe;\nimport io.reactivex.disposables.CompositeDisposable;\nimport io.reactivex.disposables.Disposable;\n\n/**\n * 页面加载器\n */\n\npublic abstract class PageLoader {\n    private static final String TAG = \"PageLoader\";\n\n    // 默认的显示参数配置\n    private static final int DEFAULT_MARGIN_HEIGHT = 20;\n    public static final int DEFAULT_MARGIN_WIDTH = 15;\n    private static final int DEFAULT_TIP_SIZE = 12;\n    private static final float MAX_SCROLL_OFFSET = 100;\n    private static final int TIP_ALPHA = 180;\n    // 监听器\n    Callback callback;\n    private Context mContext;\n    BookShelfBean book;\n    // 页面显示类\n    PageView mPageView;\n    private List<ChapterContainer> chapterContainers = new ArrayList<>();\n\n    // 绘制电池的画笔\n    private TextPaint mBatteryPaint;\n    // 绘制提示的画笔(章节名称和时间)\n    private TextPaint mTipPaint;\n    private float pageOffset = 0;\n    // 绘制标题的画笔\n    TextPaint mTitlePaint;\n    // 绘制小说内容的画笔\n    TextPaint mTextPaint;\n    // 绘制结束的画笔\n    private TextPaint mTextEndPaint;\n    // 阅读器的配置选项\n    ReadBookControl readBookControl = ReadBookControl.getInstance();\n    //缩进\n    String indent;\n    /*****************params**************************/\n    // 判断章节列表是否加载完成\n    boolean isChapterListPrepare;\n    private boolean isClose;\n    // 页面的翻页效果模式\n    private PageAnimation.Mode mPageMode;\n    //书籍绘制区域的宽高\n    int mVisibleWidth;\n    int mVisibleHeight;\n    //应用的宽高\n    int mDisplayWidth;\n    private int mDisplayHeight;\n    //间距\n    private int mMarginTop;\n    private int mMarginBottom;\n    private int mMarginLeft;\n    private int mMarginRight;\n    int contentMarginHeight;\n    private int tipMarginTop;\n    private int tipMarginBottom;\n    private int oneSpPx;\n    //标题的大小\n    private int mTitleSize;\n    //字体的大小\n    private int mTextSize;\n    private int mTextEndSize;\n    //行间距\n    int mTextInterval;\n    //标题的行间距\n    int mTitleInterval;\n    //段落距离(基于行间距的额外距离)\n    int mTextPara;\n    int mTitlePara;\n    private int textInterval;\n    private int textPara;\n    private int titleInterval;\n    private int titlePara;\n    private float tipBottomTop;\n    private float tipBottomBot;\n    private float tipDistance;\n    private float tipMarginLeft;\n    private float displayRightEnd;\n    private float tipVisibleWidth;\n\n    private boolean hideStatusBar;\n    private boolean showTimeBattery;\n\n    //电池的百分比\n    private int mBatteryLevel;\n\n    // 当前章\n    int mCurChapterPos;\n    private int mCurPagePos;\n    private int readTextLength; //已读字符数\n    private boolean resetReadAloud; //是否重新朗读\n    private int readAloudParagraph; //正在朗读章节\n\n    Bitmap cover;\n    private int linePos = 0;\n    private boolean isLastPage = false;\n\n    CompositeDisposable compositeDisposable;\n    //翻页时间\n    private long skipPageTime = 0;\n\n    /*****************************init params*******************************/\n    PageLoader(PageView pageView, BookShelfBean book, Callback callback) {\n        mPageView = pageView;\n        this.book = book;\n        this.callback = callback;\n        for (int i = 0; i < 3; i++) {\n            chapterContainers.add(new ChapterContainer());\n        }\n        mContext = pageView.getContext();\n        mCurChapterPos = book.getDurChapter();\n        mCurPagePos = book.getDurChapterPage();\n        compositeDisposable = new CompositeDisposable();\n        oneSpPx = ScreenUtils.spToPx(1);\n        // 初始化数据\n        initData();\n        // 初始化画笔\n        initPaint();\n    }\n\n    private void initData() {\n        // 获取配置参数\n        hideStatusBar = readBookControl.getHideStatusBar();\n        showTimeBattery = hideStatusBar && readBookControl.getShowTimeBattery();\n        mPageMode = PageAnimation.Mode.getPageMode(readBookControl.getPageMode());\n        // 初始化参数\n        indent = StringUtils.repeat(StringUtils.halfToFull(\" \"), readBookControl.getIndent());\n        // 配置文字有关的参数\n        setUpTextParams();\n    }\n\n    /**\n     * 屏幕大小变化处理\n     */\n    void prepareDisplay(int w, int h) {\n        // 获取PageView的宽高\n        mDisplayWidth = w;\n        mDisplayHeight = h;\n\n        // 设置边距\n        mMarginTop = hideStatusBar ?\n                ScreenUtils.dpToPx(readBookControl.getTipPaddingTop() + readBookControl.getPaddingTop() + DEFAULT_MARGIN_HEIGHT)\n                : ScreenUtils.dpToPx(readBookControl.getPaddingTop());\n        mMarginBottom = ScreenUtils.dpToPx(readBookControl.getTipPaddingBottom() + readBookControl.getPaddingBottom() + DEFAULT_MARGIN_HEIGHT);\n        mMarginLeft = ScreenUtils.dpToPx(readBookControl.getPaddingLeft());\n        mMarginRight = ScreenUtils.dpToPx(readBookControl.getPaddingRight());\n        contentMarginHeight = oneSpPx;\n        tipMarginTop = ScreenUtils.dpToPx(readBookControl.getTipPaddingTop() + DEFAULT_MARGIN_HEIGHT);\n        tipMarginBottom = ScreenUtils.dpToPx(readBookControl.getTipPaddingBottom() + DEFAULT_MARGIN_HEIGHT);\n\n        Paint.FontMetrics fontMetrics = mTipPaint.getFontMetrics();\n        float tipMarginTopHeight = (tipMarginTop + fontMetrics.top - fontMetrics.bottom) / 2;\n        float tipMarginBottomHeight = (tipMarginBottom + fontMetrics.top - fontMetrics.bottom) / 2;\n        tipBottomTop = tipMarginTopHeight - fontMetrics.top;\n        tipBottomBot = mDisplayHeight - fontMetrics.bottom - tipMarginBottomHeight;\n        tipDistance = ScreenUtils.dpToPx(DEFAULT_MARGIN_WIDTH);\n        tipMarginLeft = ScreenUtils.dpToPx(readBookControl.getTipPaddingLeft());\n        float tipMarginRight = ScreenUtils.dpToPx(readBookControl.getTipPaddingRight());\n        displayRightEnd = mDisplayWidth - tipMarginRight;\n        tipVisibleWidth = mDisplayWidth - tipMarginLeft - tipMarginRight;\n\n        // 获取内容显示位置的大小\n        mVisibleWidth = mDisplayWidth - mMarginLeft - mMarginRight;\n        mVisibleHeight = readBookControl.getHideStatusBar()\n                ? mDisplayHeight - mMarginTop - mMarginBottom\n                : mDisplayHeight - mMarginTop - mMarginBottom - mPageView.getStatusBarHeight();\n\n        // 设置翻页模式\n        mPageView.setPageMode(mPageMode, mMarginTop, mMarginBottom);\n        skipToChapter(mCurChapterPos, mCurPagePos);\n    }\n\n    /**\n     * 设置与文字相关的参数\n     */\n    private void setUpTextParams() {\n        // 文字大小\n        mTextSize = ScreenUtils.spToPx(readBookControl.getTextSize());\n        mTitleSize = mTextSize + oneSpPx;\n        mTextEndSize = mTextSize - oneSpPx;\n        // 行间距(大小为字体的一半)\n        mTextInterval = (int) (mTextSize * readBookControl.getLineMultiplier() / 2);\n        mTitleInterval = (int) (mTitleSize * readBookControl.getLineMultiplier() / 2);\n        // 段落间距(大小为字体的高度)\n        mTextPara = (int) (mTextSize * readBookControl.getLineMultiplier() * readBookControl.getParagraphSize() / 2);\n        mTitlePara = (int) (mTitleSize * readBookControl.getLineMultiplier() * readBookControl.getParagraphSize() / 2);\n    }\n\n    /**\n     * 初始化画笔\n     */\n    private void initPaint() {\n        Typeface typeface;\n        try {\n            if (!TextUtils.isEmpty(readBookControl.getFontPath())) {\n                typeface = Typeface.createFromFile(readBookControl.getFontPath());\n            } else {\n                typeface = Typeface.SANS_SERIF;\n            }\n        } catch (Exception e) {\n            Toast.makeText(mContext, \"字体文件未找,到恢复默认字体\", Toast.LENGTH_SHORT).show();\n            readBookControl.setReadBookFont(null);\n            typeface = Typeface.SANS_SERIF;\n        }\n        // 绘制提示的画笔\n        mTipPaint = new TextPaint();\n        mTipPaint.setColor(readBookControl.getTextColor());\n        mTipPaint.setTextAlign(Paint.Align.LEFT); // 绘制的起始点\n        mTipPaint.setTextSize(ScreenUtils.spToPx(DEFAULT_TIP_SIZE)); // Tip默认的字体大小\n        mTipPaint.setTypeface(Typeface.create(typeface, Typeface.NORMAL));\n        mTipPaint.setAntiAlias(true);\n        mTipPaint.setSubpixelText(true);\n\n        // 绘制标题的画笔\n        mTitlePaint = new TextPaint();\n        mTitlePaint.setColor(readBookControl.getTextColor());\n        mTitlePaint.setTextSize(mTitleSize);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            mTitlePaint.setLetterSpacing(readBookControl.getTextLetterSpacing());\n        }\n        mTitlePaint.setStyle(Paint.Style.FILL_AND_STROKE);\n        mTitlePaint.setTypeface(Typeface.create(typeface, Typeface.BOLD));\n        mTitlePaint.setTextAlign(Paint.Align.CENTER);\n        mTitlePaint.setAntiAlias(true);\n\n        // 绘制页面内容的画笔\n        mTextPaint = new TextPaint();\n        mTextPaint.setColor(readBookControl.getTextColor());\n        mTextPaint.setTextSize(mTextSize);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            mTextPaint.setLetterSpacing(readBookControl.getTextLetterSpacing());\n        }\n        int bold = readBookControl.getTextBold() ? Typeface.BOLD : Typeface.NORMAL;\n        mTextPaint.setTypeface(Typeface.create(typeface, bold));\n        mTextPaint.setAntiAlias(true);\n\n        // 绘制结束的画笔\n        mTextEndPaint = new TextPaint();\n        mTextEndPaint.setColor(readBookControl.getTextColor());\n        mTextEndPaint.setTextSize(mTextEndSize);\n        mTextEndPaint.setTypeface(Typeface.create(typeface, Typeface.NORMAL));\n        mTextEndPaint.setAntiAlias(true);\n        mTextEndPaint.setSubpixelText(true);\n        mTextEndPaint.setTextAlign(Paint.Align.CENTER);\n\n        // 绘制电池的画笔\n        mBatteryPaint = new TextPaint();\n        mBatteryPaint.setAntiAlias(true);\n        mBatteryPaint.setDither(true);\n        mBatteryPaint.setTextSize(ScreenUtils.spToPx(DEFAULT_TIP_SIZE - 3));\n        mBatteryPaint.setTypeface(Typeface.createFromAsset(mContext.getAssets(), \"number.ttf\"));\n\n        setupTextInterval();\n        // 初始化页面样式\n        initPageStyle();\n    }\n\n    /**\n     * 设置文字相关参数\n     */\n    public void setTextSize() {\n        // 设置文字相关参数\n        setUpTextParams();\n        initPaint();\n        skipToChapter(mCurChapterPos, mCurPagePos);\n    }\n\n    private void setupTextInterval() {\n        textInterval = mTextInterval + (int) mTextPaint.getTextSize();\n        textPara = mTextPara + (int) mTextPaint.getTextSize();\n        titleInterval = mTitleInterval + (int) mTitlePaint.getTextSize();\n        titlePara = mTitlePara + (int) mTextPaint.getTextSize();\n    }\n\n    /**\n     * 设置页面样式\n     */\n    private void initPageStyle() {\n        mTipPaint.setColor(readBookControl.getTextColor());\n        mTitlePaint.setColor(readBookControl.getTextColor());\n        mTextPaint.setColor(readBookControl.getTextColor());\n        mBatteryPaint.setColor(readBookControl.getTextColor());\n        mTextEndPaint.setColor(readBookControl.getTextColor());\n        mTipPaint.setAlpha(TIP_ALPHA);\n        mBatteryPaint.setAlpha(TIP_ALPHA);\n        mTextEndPaint.setAlpha(TIP_ALPHA);\n    }\n\n    /**\n     * 设置翻页动画\n     */\n    public void setPageMode(PageAnimation.Mode pageMode) {\n        mPageMode = pageMode;\n        mPageView.setPageMode(mPageMode, mMarginTop, mMarginBottom);\n        skipToChapter(mCurChapterPos, mCurPagePos);\n    }\n\n    /**\n     * 设置内容与屏幕的间距 单位为 px\n     */\n    public void upMargin() {\n        prepareDisplay(mDisplayWidth, mDisplayHeight);\n    }\n\n    /**\n     * 刷新界面\n     */\n    public void refreshUi() {\n        initData();\n        initPaint();\n        mPageView.setPageMode(mPageMode, mMarginTop, mMarginBottom);\n        skipToChapter(mCurChapterPos, mCurPagePos);\n    }\n\n    /**\n     * 跳转到上一章\n     */\n    public void skipPreChapter() {\n        if (mCurChapterPos <= 0) {\n            return;\n        }\n\n        // 载入上一章。\n        mCurChapterPos = mCurChapterPos - 1;\n        mCurPagePos = 0;\n        Collections.swap(chapterContainers, 2, 1);\n        Collections.swap(chapterContainers, 1, 0);\n        prevChapter().txtChapter = null;\n        parsePrevChapter();\n\n        chapterChangeCallback();\n        openChapter(mCurPagePos);\n        pagingEnd(PageAnimation.Direction.NONE);\n    }\n\n    /**\n     * 跳转到下一章\n     */\n    public boolean skipNextChapter() {\n        if (mCurChapterPos + 1 >= book.getChapterListSize()) {\n            return false;\n        }\n\n        //载入下一章\n        mCurChapterPos = mCurChapterPos + 1;\n        mCurPagePos = 0;\n        Collections.swap(chapterContainers, 0, 1);\n        Collections.swap(chapterContainers, 1, 2);\n        nextChapter().txtChapter = null;\n        parseNextChapter();\n\n        chapterChangeCallback();\n        openChapter(mCurPagePos);\n        pagingEnd(PageAnimation.Direction.NONE);\n        return true;\n    }\n\n    /**\n     * 跳转到指定章节页\n     */\n    public void skipToChapter(int chapterPos, int pagePos) {\n        // 设置参数\n        mCurChapterPos = chapterPos;\n        mCurPagePos = pagePos;\n\n        prevChapter().txtChapter = null;\n        curChapter().txtChapter = null;\n        nextChapter().txtChapter = null;\n\n        openChapter(pagePos);\n    }\n\n    /**\n     * 跳转到指定的页\n     */\n    public void skipToPage(int pos) {\n        if (!isChapterListPrepare) {\n            return;\n        }\n        openChapter(pos);\n    }\n\n    /**\n     * 翻到上一页\n     */\n    public void skipToPrePage() {\n        if ((System.currentTimeMillis() - skipPageTime) > 300) {\n            mPageView.autoPrevPage();\n            skipPageTime = System.currentTimeMillis();\n        }\n    }\n\n    /**\n     * 翻到下一页\n     */\n    public void skipToNextPage() {\n        if ((System.currentTimeMillis() - skipPageTime) > 300) {\n            mPageView.autoNextPage();\n            skipPageTime = System.currentTimeMillis();\n        }\n    }\n\n    /**\n     * 翻到下一页,无动画\n     */\n    private void noAnimationToNextPage() {\n        if (getCurPagePos() < curChapter().txtChapter.getPageSize() - 1) {\n            skipToPage(getCurPagePos() + 1);\n            return;\n        }\n        skipNextChapter();\n    }\n\n    /**\n     * 更新时间\n     */\n    public void updateTime() {\n        if (readBookControl.getHideStatusBar() && readBookControl.getShowTimeBattery()) {\n            if (mPageMode == PageAnimation.Mode.SCROLL) {\n                mPageView.drawBackground(0);\n            } else {\n                upPage();\n            }\n            mPageView.invalidate();\n        }\n    }\n\n    /**\n     * 更新电量\n     */\n    public boolean updateBattery(int level) {\n        if (mBatteryLevel == level) {\n            return false;\n        }\n        mBatteryLevel = level;\n        if (readBookControl.getHideStatusBar() && readBookControl.getShowTimeBattery()) {\n            if (mPageMode == PageAnimation.Mode.SCROLL) {\n                mPageView.drawBackground(0);\n            } else if (curChapter().txtChapter != null) {\n                upPage();\n            }\n            mPageView.invalidate();\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * 获取当前页的状态\n     */\n    public TxtChapter.Status getPageStatus() {\n        return curChapter().txtChapter != null ? curChapter().txtChapter.getStatus() : TxtChapter.Status.LOADING;\n    }\n\n    /**\n     * 获取当前页的页码\n     */\n    int getCurPagePos() {\n        return mCurPagePos;\n    }\n\n    /**\n     * 更新状态\n     */\n    public void setStatus(TxtChapter.Status status) {\n        curChapter().txtChapter.setStatus(status);\n        reSetPage();\n        mPageView.invalidate();\n    }\n\n    /**\n     * 加载错误\n     */\n    void durDhapterError(String msg) {\n        if (curChapter().txtChapter == null) {\n            curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n        }\n        if (curChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) return;\n        curChapter().txtChapter.setStatus(TxtChapter.Status.ERROR);\n        curChapter().txtChapter.setMsg(msg);\n        if (mPageMode != PageAnimation.Mode.SCROLL) {\n            upPage();\n        } else {\n            mPageView.drawPage(0);\n        }\n        mPageView.invalidate();\n    }\n\n    /**\n     * @return 当前章节所有内容\n     */\n    public String getAllContent() {\n        return getContentStartPage(0);\n    }\n\n    /**\n     * @return 本页未读内容\n     */\n    public String getContent() {\n        if (curChapter().txtChapter == null) return null;\n        if (curChapter().txtChapter.getPageSize() == 0) return null;\n        TxtPage txtPage = curChapter().txtChapter.getPage(mCurPagePos);\n        StringBuilder s = new StringBuilder();\n        int size = txtPage.size();\n        int start = mPageMode == PageAnimation.Mode.SCROLL ? Math.min(Math.max(0, linePos), size - 1) : 0;\n        for (int i = start; i < size; i++) {\n            s.append(txtPage.getLine(i));\n        }\n        return s.toString();\n    }\n\n    /**\n     * @return 本章未读内容\n     */\n    public String getUnReadContent() {\n        if (curChapter().txtChapter == null) return null;\n        if (book.isAudio()) return curChapter().txtChapter.getMsg();\n        if (curChapter().txtChapter.getTxtPageList().isEmpty()) return null;\n        StringBuilder s = new StringBuilder();\n        String content = getContent();\n        if (content != null) {\n            s.append(content);\n        }\n        content = getContentStartPage(mCurPagePos + 1);\n        if (content != null) {\n            s.append(content);\n        }\n        readTextLength = mCurPagePos > 0 ? curChapter().txtChapter.getPageLength(mCurPagePos - 1) : 0;\n        if (mPageMode == PageAnimation.Mode.SCROLL) {\n            for (int i = 0; i < Math.min(Math.max(0, linePos), curChapter().txtChapter.getPage(mCurPagePos).size() - 1); i++) {\n                readTextLength += curChapter().txtChapter.getPage(mCurPagePos).getLine(i).length();\n            }\n        }\n        return s.toString();\n    }\n\n    /**\n     * * @return curPageLength 当前页字数\n     */\n    public int curPageLength() {\n        if (curChapter().txtChapter == null) return 0;\n        if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) return 0;\n        String str;\n        int strLength = 0;\n        TxtPage txtPage = curChapter().txtChapter.getPage(mCurPagePos);\n        if (txtPage != null) {\n            for (int i = txtPage.getTitleLines(); i < txtPage.size(); ++i) {\n                str = txtPage.getLine(i);\n                strLength = strLength + str.length();\n            }\n        }\n        return strLength;\n    }\n\n    /**\n     * @param page 开始页数\n     * @return 从page页开始的的当前章节所有内容\n     */\n    private String getContentStartPage(int page) {\n        if (curChapter().txtChapter == null) return null;\n        if (curChapter().txtChapter.getTxtPageList().isEmpty()) return null;\n        StringBuilder s = new StringBuilder();\n        if (curChapter().txtChapter.getPageSize() > page) {\n            for (int i = page; i < curChapter().txtChapter.getPageSize(); i++) {\n                s.append(curChapter().txtChapter.getPage(i).getContent());\n            }\n        }\n        return s.toString();\n    }\n\n    /**\n     * @param start 开始朗读字数\n     */\n    public void readAloudStart(int start) {\n        start = readTextLength + start;\n        int x = curChapter().txtChapter.getParagraphIndex(start);\n        if (readAloudParagraph != x) {\n            readAloudParagraph = x;\n            mPageView.drawPage(0);\n            mPageView.invalidate();\n            mPageView.drawPage(-1);\n            mPageView.drawPage(1);\n            mPageView.invalidate();\n        }\n    }\n\n    /**\n     * @param readAloudLength 已朗读字数\n     */\n    public void readAloudLength(int readAloudLength) {\n        if (curChapter().txtChapter == null) return;\n        if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) return;\n        if (curChapter().txtChapter.getPageLength(mCurPagePos) < 0) return;\n        if (mPageView.isRunning()) return;\n        readAloudLength = readTextLength + readAloudLength;\n        if (readAloudLength >= curChapter().txtChapter.getPageLength(mCurPagePos)) {\n            resetReadAloud = false;\n            noAnimationToNextPage();\n            mPageView.invalidate();\n        }\n    }\n\n    /**\n     * 刷新章节列表\n     */\n    public abstract void refreshChapterList();\n\n    /**\n     * 获取章节的文本\n     */\n    protected abstract String getChapterContent(BookChapterBean chapter) throws Exception;\n\n    /**\n     * 章节数据是否存在\n     */\n    protected abstract boolean noChapterData(BookChapterBean chapter);\n\n    /**\n     * 打开当前章节指定页\n     */\n    void openChapter(int pagePos) {\n        mCurPagePos = pagePos;\n        if (!mPageView.isPrepare()) {\n            return;\n        }\n\n        if (curChapter().txtChapter == null) {\n            curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n            reSetPage();\n        } else if (curChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) {\n            reSetPage();\n            mPageView.invalidate();\n            pagingEnd(PageAnimation.Direction.NONE);\n            return;\n        }\n\n        // 如果章节目录没有准备好\n        if (!isChapterListPrepare) {\n            curChapter().txtChapter.setStatus(TxtChapter.Status.LOADING);\n            reSetPage();\n            mPageView.invalidate();\n            return;\n        }\n\n        // 如果获取到的章节目录为空\n        if (callback.getChapterList().isEmpty()) {\n            curChapter().txtChapter.setStatus(TxtChapter.Status.CATEGORY_EMPTY);\n            reSetPage();\n            mPageView.invalidate();\n            return;\n        }\n\n        parseCurChapter();\n        resetPageOffset();\n    }\n\n    /**\n     * 重置页面\n     */\n    private void reSetPage() {\n        if (mPageMode == PageAnimation.Mode.SCROLL) {\n            resetPageOffset();\n            mPageView.invalidate();\n        } else {\n            upPage();\n        }\n    }\n\n    /**\n     * 更新页面\n     */\n    private void upPage() {\n        if (mPageMode != PageAnimation.Mode.SCROLL) {\n            mPageView.drawPage(0);\n            if (mCurPagePos > 0 || curChapter().txtChapter.getPosition() > 0) {\n                mPageView.drawPage(-1);\n            }\n            if (mCurPagePos < curChapter().txtChapter.getPageSize() - 1 || curChapter().txtChapter.getPosition() < callback.getChapterList().size() - 1) {\n                mPageView.drawPage(1);\n            }\n        }\n    }\n\n    /**\n     * 翻页完成\n     */\n    void pagingEnd(PageAnimation.Direction direction) {\n        if (!isChapterListPrepare) {\n            return;\n        }\n        switch (direction) {\n            case NEXT:\n                if (mCurPagePos < curChapter().txtChapter.getPageSize() - 1) {\n                    mCurPagePos = mCurPagePos + 1;\n                } else if (mCurChapterPos < book.getChapterListSize() - 1) {\n                    mCurChapterPos = mCurChapterPos + 1;\n                    mCurPagePos = 0;\n                    Collections.swap(chapterContainers, 0, 1);\n                    Collections.swap(chapterContainers, 1, 2);\n                    nextChapter().txtChapter = null;\n                    parseNextChapter();\n                    chapterChangeCallback();\n                }\n                if (mPageMode != PageAnimation.Mode.SCROLL) {\n                    mPageView.drawPage(1);\n                }\n                break;\n            case PREV:\n                if (mCurPagePos > 0) {\n                    mCurPagePos = mCurPagePos - 1;\n                } else if (mCurChapterPos > 0) {\n                    mCurChapterPos = mCurChapterPos - 1;\n                    mCurPagePos = prevChapter().txtChapter.getPageSize() - 1;\n                    Collections.swap(chapterContainers, 2, 1);\n                    Collections.swap(chapterContainers, 1, 0);\n                    prevChapter().txtChapter = null;\n                    parsePrevChapter();\n                    chapterChangeCallback();\n                }\n                if (mPageMode != PageAnimation.Mode.SCROLL) {\n                    mPageView.drawPage(-1);\n                }\n                break;\n        }\n        mPageView.setContentDescription(getContent());\n        book.setDurChapter(mCurChapterPos);\n        book.setDurChapterPage(mCurPagePos);\n        callback.onPageChange(mCurChapterPos, getCurPagePos(), resetReadAloud);\n        resetReadAloud = true;\n    }\n\n    /**\n     * 绘制页面\n     * pageOnCur: 位于当前页的位置, 小于0上一页, 0 当前页, 大于0下一页\n     */\n    synchronized void drawPage(Bitmap bitmap, int pageOnCur) {\n        TxtChapter txtChapter;\n        TxtPage txtPage = null;\n        if (curChapter().txtChapter == null) {\n            curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n        }\n        if (pageOnCur == 0) { //当前页\n            txtChapter = curChapter().txtChapter;\n            txtPage = txtChapter.getPage(mCurPagePos);\n        } else if (pageOnCur < 0) { //上一页\n            if (mCurPagePos > 0) {\n                txtChapter = curChapter().txtChapter;\n                txtPage = txtChapter.getPage(mCurPagePos - 1);\n            } else {\n                if (prevChapter().txtChapter == null) {\n                    txtChapter = new TxtChapter(mCurChapterPos + 1);\n                    txtChapter.setStatus(TxtChapter.Status.ERROR);\n                    txtChapter.setMsg(\"未加载完成\");\n                } else {\n                    txtChapter = prevChapter().txtChapter;\n                    txtPage = txtChapter.getPage(txtChapter.getPageSize() - 1);\n                }\n            }\n        } else { //下一页\n            if (mCurPagePos + 1 < curChapter().txtChapter.getPageSize()) {\n                txtChapter = curChapter().txtChapter;\n                txtPage = txtChapter.getPage(mCurPagePos + 1);\n            } else {\n                if (mCurChapterPos + 1 >= callback.getChapterList().size()) {\n                    txtChapter = new TxtChapter(mCurChapterPos + 1);\n                    txtChapter.setStatus(TxtChapter.Status.ERROR);\n                    txtChapter.setMsg(\"没有下一页\");\n                } else if (nextChapter().txtChapter == null) {\n                    txtChapter = new TxtChapter(mCurChapterPos + 1);\n                    txtChapter.setStatus(TxtChapter.Status.ERROR);\n                    txtChapter.setMsg(\"未加载完成\");\n                } else {\n                    txtChapter = nextChapter().txtChapter;\n                    txtPage = txtChapter.getPage(0);\n                }\n            }\n        }\n        if (bitmap != null)\n            drawBackground(bitmap, txtChapter, txtPage);\n        drawContent(bitmap, txtChapter, txtPage);\n    }\n\n    /**\n     * 滚动模式绘制背景\n     */\n    void drawBackground(Canvas canvas) {\n        if (curChapter().txtChapter == null) {\n            curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n        }\n        drawBackground(canvas, curChapter().txtChapter, curChapter().txtChapter.getPage(mCurPagePos));\n    }\n\n    /**\n     * 横翻模式绘制背景\n     */\n    private synchronized void drawBackground(Bitmap bitmap, TxtChapter txtChapter, TxtPage txtPage) {\n        if (bitmap == null) return;\n        Canvas canvas = new Canvas(bitmap);\n        if (!readBookControl.bgIsColor() && !readBookControl.bgBitmapIsNull()) {\n            Rect mDestRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());\n            canvas.drawBitmap(readBookControl.getBgBitmap(), null, mDestRect, null);\n        } else {\n            canvas.drawColor(readBookControl.getBgColor());\n        }\n        drawBackground(canvas, txtChapter, txtPage);\n    }\n\n    /**\n     * 绘制背景\n     */\n    @SuppressLint(\"DefaultLocale\")\n    private synchronized void drawBackground(final Canvas canvas, TxtChapter txtChapter, TxtPage txtPage) {\n        if (canvas == null) return;\n        if (!callback.getChapterList().isEmpty()) {\n            String title = callback.getChapterList().size() > txtChapter.getPosition() ? callback.getChapterList().get(txtChapter.getPosition()).getDurChapterName() : \"\";\n            title = ChapterContentHelp.getInstance().replaceContent(book.getBookInfoBean().getName(), book.getTag(), title, book.getReplaceEnable());\n            String page = (txtChapter.getStatus() != TxtChapter.Status.FINISH || txtPage == null) ? \"\"\n                    : String.format(\"%d/%d\", txtPage.getPosition() + 1, txtChapter.getPageSize());\n            String progress = (txtChapter.getStatus() != TxtChapter.Status.FINISH) ? \"\"\n                    : BookshelfHelp.getReadProgress(mCurChapterPos, book.getChapterListSize(), mCurPagePos, curChapter().txtChapter.getPageSize());\n\n            float tipBottom;\n            float tipLeft;\n            //初始化标题的参数\n            //需要注意的是:绘制text的y的起始点是text的基准线的位置，而不是从text的头部的位置\n            if (!hideStatusBar) { //显示状态栏\n                if (txtChapter.getStatus() != TxtChapter.Status.FINISH) {\n                    if (isChapterListPrepare) {\n                        //绘制标题\n                        title = TextUtils.ellipsize(title, mTipPaint, tipVisibleWidth, TextUtils.TruncateAt.END).toString();\n                        canvas.drawText(title, tipMarginLeft, tipBottomBot, mTipPaint);\n                    }\n                } else {\n                    //绘制总进度\n                    tipLeft = displayRightEnd - mTipPaint.measureText(progress);\n                    canvas.drawText(progress, tipLeft, tipBottomBot, mTipPaint);\n                    //绘制页码\n                    tipLeft = tipLeft - tipDistance - mTipPaint.measureText(page);\n                    canvas.drawText(page, tipLeft, tipBottomBot, mTipPaint);\n                    //绘制标题\n                    title = TextUtils.ellipsize(title, mTipPaint, tipLeft - tipDistance, TextUtils.TruncateAt.END).toString();\n                    canvas.drawText(title, tipMarginLeft, tipBottomBot, mTipPaint);\n                }\n                if (readBookControl.getShowLine()) {\n                    //绘制分隔线\n                    tipBottom = mDisplayHeight - tipMarginBottom;\n                    canvas.drawRect(tipMarginLeft, tipBottom, displayRightEnd, tipBottom + 2, mTipPaint);\n                }\n            } else { //隐藏状态栏\n                if (getPageStatus() != TxtChapter.Status.FINISH) {\n                    if (isChapterListPrepare) {\n                        //绘制标题\n                        title = TextUtils.ellipsize(title, mTipPaint, tipVisibleWidth, TextUtils.TruncateAt.END).toString();\n                        canvas.drawText(title, tipMarginLeft, tipBottomTop, mTipPaint);\n                    }\n                } else {\n                    //绘制标题\n                    float titleTipLength = showTimeBattery ? tipVisibleWidth - mTipPaint.measureText(progress) - tipDistance : tipVisibleWidth;\n                    title = TextUtils.ellipsize(title, mTipPaint, titleTipLength, TextUtils.TruncateAt.END).toString();\n                    canvas.drawText(title, tipMarginLeft, tipBottomTop, mTipPaint);\n                    // 绘制页码\n                    canvas.drawText(page, tipMarginLeft, tipBottomBot, mTipPaint);\n                    //绘制总进度\n                    float progressTipLeft = displayRightEnd - mTipPaint.measureText(progress);\n                    float progressTipBottom = showTimeBattery ? tipBottomTop : tipBottomBot;\n                    canvas.drawText(progress, progressTipLeft, progressTipBottom, mTipPaint);\n                }\n                if (readBookControl.getShowLine()) {\n                    //绘制分隔线\n                    tipBottom = tipMarginTop - 2;\n                    canvas.drawRect(tipMarginLeft, tipBottom, displayRightEnd, tipBottom + 2, mTipPaint);\n                }\n            }\n        }\n\n        int visibleRight = (int) displayRightEnd;\n        if (hideStatusBar && showTimeBattery) {\n            //绘制当前时间\n            String time = StringUtils.dateConvert(System.currentTimeMillis(), AppConstant.FORMAT_TIME);\n            float timeTipLeft = (mDisplayWidth - mTipPaint.measureText(time)) / 2;\n            canvas.drawText(time, timeTipLeft, tipBottomBot, mTipPaint);\n\n            //绘制电池\n            int polarHeight = ScreenUtils.dpToPx(4);\n            int polarWidth = ScreenUtils.dpToPx(2);\n            int border = 2;\n            int outFrameWidth = (int) mBatteryPaint.measureText(\"0000\") + polarWidth;\n            int outFrameHeight = (int) mBatteryPaint.getTextSize() + oneSpPx;\n            int visibleBottom = mDisplayHeight - (tipMarginBottom - outFrameHeight) / 2;\n\n            //电极的制作\n            int polarLeft = visibleRight - polarWidth;\n            int polarTop = visibleBottom - (outFrameHeight + polarHeight) / 2;\n            Rect polar = new Rect(polarLeft, polarTop, visibleRight, polarTop + polarHeight);\n\n            mBatteryPaint.setStyle(Paint.Style.FILL);\n            canvas.drawRect(polar, mBatteryPaint);\n\n            //外框的制作\n            int outFrameLeft = polarLeft - outFrameWidth;\n            int outFrameTop = visibleBottom - outFrameHeight;\n            Rect outFrame = new Rect(outFrameLeft, outFrameTop, polarLeft, visibleBottom);\n\n            mBatteryPaint.setStyle(Paint.Style.STROKE);\n            mBatteryPaint.setStrokeWidth(border);\n            canvas.drawRect(outFrame, mBatteryPaint);\n\n            //绘制电量\n            mBatteryPaint.setStyle(Paint.Style.FILL);\n            Paint.FontMetrics fontMetrics = mBatteryPaint.getFontMetrics();\n            String batteryLevel = String.valueOf(mBatteryLevel);\n            float batTextLeft = outFrameLeft + (outFrameWidth - mBatteryPaint.measureText(batteryLevel)) / 2;\n            float batTextBaseLine = visibleBottom - outFrameHeight / 2f - fontMetrics.top / 2 - fontMetrics.bottom / 2;\n            canvas.drawText(batteryLevel, batTextLeft, batTextBaseLine, mBatteryPaint);\n        }\n    }\n\n\n    /**\n     * 绘制内容\n     */\n    private synchronized void drawContent(Bitmap bitmap, TxtChapter txtChapter, TxtPage txtPage) {\n        if (bitmap == null) return;\n        Canvas canvas = new Canvas(bitmap);\n        if (mPageMode == PageAnimation.Mode.SCROLL) {\n            bitmap.eraseColor(Color.TRANSPARENT);\n        }\n\n        Paint.FontMetrics fontMetricsForTitle = mTitlePaint.getFontMetrics();\n        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();\n\n        if (txtChapter.getStatus() != TxtChapter.Status.FINISH) {\n            //绘制字体\n            String tip = getStatusText(txtChapter);\n            drawErrorMsg(canvas, tip, 0);\n        } else {\n            float top = contentMarginHeight - fontMetrics.ascent;\n            if (mPageMode != PageAnimation.Mode.SCROLL) {\n                top += readBookControl.getHideStatusBar() ? mMarginTop : mPageView.getStatusBarHeight() + mMarginTop;\n            }\n            int ppp = 0;//pzl,文字位置\n            //对标题进行绘制\n            String str;\n            int strLength = 0;\n            boolean isLight;\n            for (int i = 0; i < txtPage.getTitleLines(); ++i) {\n                str = txtPage.getLine(i);\n                strLength = strLength + str.length();\n                isLight = ReadAloudService.running && readAloudParagraph == 0;\n                mTitlePaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor());\n\n                //进行绘制\n                canvas.drawText(str, mDisplayWidth / 2f, top, mTitlePaint);\n\n                //pzl\n                float leftposition = mDisplayWidth / 2;\n                float rightposition = 0;\n                float bottomposition = top + mTitlePaint.getFontMetrics().descent;\n                float TextHeight = Math.abs(fontMetricsForTitle.ascent) + Math.abs(fontMetricsForTitle.descent);\n\n                if (txtPage.getTxtLists() != null) {\n                    for (TxtChar c : txtPage.getTxtLists().get(i).getCharsData()) {\n                        rightposition = leftposition + c.getCharWidth();\n                        Point tlp = new Point();\n                        c.setTopLeftPosition(tlp);\n                        tlp.x = (int) leftposition;\n                        tlp.y = (int) (bottomposition - TextHeight);\n\n                        Point blp = new Point();\n                        c.setBottomLeftPosition(blp);\n                        blp.x = (int) leftposition;\n                        blp.y = (int) bottomposition;\n\n                        Point trp = new Point();\n                        c.setTopRightPosition(trp);\n                        trp.x = (int) rightposition;\n                        trp.y = (int) (bottomposition - TextHeight);\n\n                        Point brp = new Point();\n                        c.setBottomRightPosition(brp);\n                        brp.x = (int) rightposition;\n                        brp.y = (int) bottomposition;\n                        ppp++;\n                        c.setIndex(ppp);\n\n                        leftposition = rightposition;\n                    }\n                }\n\n                //设置尾部间距\n                if (i == txtPage.getTitleLines() - 1) {\n                    top += titlePara;\n                } else {\n                    //行间距\n                    top += titleInterval;\n                }\n            }\n\n            if (txtPage.getLines().isEmpty()) {\n                return;\n            }\n            //对内容进行绘制\n            for (int i = txtPage.getTitleLines(); i < txtPage.size(); ++i) {\n                str = txtPage.getLine(i);\n                strLength = strLength + str.length();\n                int paragraphLength = txtPage.getPosition() == 0 ? strLength : txtChapter.getPageLength(txtPage.getPosition() - 1) + strLength;\n                isLight = ReadAloudService.running && readAloudParagraph == txtChapter.getParagraphIndex(paragraphLength);\n                mTextPaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor());\n                Layout tempLayout = new StaticLayout(str, mTextPaint, mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false);\n                float width = StaticLayout.getDesiredWidth(str, tempLayout.getLineStart(0), tempLayout.getLineEnd(0), mTextPaint);\n                if (needScale(str)) {\n                    drawScaledText(canvas, str, width, mTextPaint, top, i, txtPage.getTxtLists());\n                } else {\n                    canvas.drawText(str, mMarginLeft, top, mTextPaint);\n                }\n\n                //记录文字位置 --开始 pzl\n                float leftposition = mMarginLeft;\n                if (isFirstLineOfParagraph(str)) {\n                    String blanks = StringUtils.halfToFull(\"  \");\n                    //canvas.drawText(blanks, x, top, mTextPaint);\n                    float bw = StaticLayout.getDesiredWidth(blanks, mTextPaint);\n                    leftposition += bw;\n                }\n                float rightposition = 0;\n                float bottomposition = top + mTextPaint.getFontMetrics().descent;\n                float textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);\n\n                if (txtPage.getTxtLists() != null) {\n                    for (TxtChar c : txtPage.getTxtLists().get(i).getCharsData()) {\n                        rightposition = leftposition + c.getCharWidth();\n                        Point tlp = new Point();\n                        c.setTopLeftPosition(tlp);\n                        tlp.x = (int) leftposition;\n                        tlp.y = (int) (bottomposition - textHeight);\n\n                        Point blp = new Point();\n                        c.setBottomLeftPosition(blp);\n                        blp.x = (int) leftposition;\n                        blp.y = (int) bottomposition;\n\n                        Point trp = new Point();\n                        c.setTopRightPosition(trp);\n                        trp.x = (int) rightposition;\n                        trp.y = (int) (bottomposition - textHeight);\n\n                        Point brp = new Point();\n                        c.setBottomRightPosition(brp);\n                        brp.x = (int) rightposition;\n                        brp.y = (int) bottomposition;\n\n                        leftposition = rightposition;\n\n                        ppp++;\n                        c.setIndex(ppp);\n                    }\n                }\n                //记录文字位置 --结束 pzl\n\n                //设置尾部间距\n                if (str.endsWith(\"\\n\")) {\n                    top += textPara;\n                } else {\n                    top += textInterval;\n                }\n            }\n        }\n    }\n\n    public void drawCover(Canvas canvas, float top) {\n    }\n\n    private int getCoverHeight() {\n        return cover == null ? 0 : cover.getHeight() + 20;\n    }\n\n    /**\n     * 绘制内容-滚动\n     */\n    void drawContent(final Canvas canvas, float offset) {\n\n        if (offset > MAX_SCROLL_OFFSET) {\n            offset = MAX_SCROLL_OFFSET;\n        } else if (offset < 0 - MAX_SCROLL_OFFSET) {\n            offset = -MAX_SCROLL_OFFSET;\n        }\n\n        boolean pageChanged = false;\n        Paint.FontMetrics fontMetricsForTitle = mTitlePaint.getFontMetrics();\n        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();\n\n        final float totalHeight = mVisibleHeight + titleInterval;\n        if (curChapter().txtChapter == null) {\n            curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n        }\n\n        if (!isLastPage || offset < 0) {\n            pageOffset += offset;\n            isLastPage = false;\n        }\n        // 首页\n        if (pageOffset < 0 && mCurChapterPos == 0 && mCurPagePos == 0) {\n            pageOffset = 0;\n        }\n\n        float cHeight = getFixedPageHeight(curChapter().txtChapter, mCurPagePos);\n        cHeight = cHeight > 0 ? cHeight : mVisibleHeight;\n        if (offset > 0 && pageOffset > cHeight) {\n            while (pageOffset > cHeight) {\n                switchToPageOffset(1);\n                pageOffset -= cHeight;\n                cHeight = getFixedPageHeight(curChapter().txtChapter, mCurPagePos);\n                cHeight = cHeight > 0 ? cHeight : mVisibleHeight;\n                pageChanged = true;\n            }\n        } else if (offset < 0 && pageOffset < 0) {\n            while (pageOffset < 0) {\n                switchToPageOffset(-1);\n                cHeight = getFixedPageHeight(curChapter().txtChapter, mCurPagePos);\n                cHeight = cHeight > 0 ? cHeight : mVisibleHeight;\n                pageOffset += cHeight;\n                pageChanged = true;\n            }\n        }\n\n        if (pageChanged) {\n            chapterChangeCallback();\n            pagingEnd(PageAnimation.Direction.NONE);\n        }\n\n        float top = contentMarginHeight - mTextPaint.ascent() - pageOffset;\n\n        int chapterPos = mCurChapterPos;\n        int pagePos = mCurPagePos;\n        boolean isLight;\n        int ppp = 0;//pzl,文字位置\n\n        if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) {\n            String tip = getStatusText(curChapter().txtChapter);\n            drawErrorMsg(canvas, tip, pageOffset);\n            top += mVisibleHeight;\n            chapterPos += 1;\n            pagePos = 0;\n        }\n        String str;\n        linePos = 0;\n        boolean linePosSet = false;\n        boolean bookEnd = false;\n        float startHeight = -2 * titleInterval;\n        if (pageOffset < mTextPaint.getTextSize()) {\n            linePos = 0;\n            linePosSet = true;\n        }\n        while (top < totalHeight) {\n            TxtChapter chapter = chapterPos == mCurChapterPos ? curChapter().txtChapter : nextChapter().txtChapter;\n            if (chapter == null || chapterPos - mCurChapterPos > 1) break;\n            if (chapter.getStatus() != TxtChapter.Status.FINISH) {\n                String tip = getStatusText(chapter);\n                drawErrorMsg(canvas, tip, 0 - top);\n                top += mVisibleHeight;\n                chapterPos += 1;\n                pagePos = 0;\n                continue;\n            }\n            if (chapter.getPageSize() == 0) break;\n            TxtPage page = chapter.getPage(pagePos);\n            if (page.getLines().isEmpty()) break;\n            if (top > totalHeight) break;\n            float topi = top;\n            int strLength = 0;\n            isLight = ReadAloudService.running && readAloudParagraph == 0;\n            mTitlePaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor());\n            for (int i = 0; i < page.getTitleLines(); i++) {\n                if (top > totalHeight) {\n                    break;\n                } else if (top > startHeight) {\n                    str = page.getLine(i);\n                    strLength = strLength + str.length();\n                    //进行绘制\n                    canvas.drawText(str, mDisplayWidth / 2f, top, mTitlePaint);\n                    //pzl\n                    float leftposition = mDisplayWidth / 2f;\n                    float rightPosition = 0;\n                    float bottomPosition = top + mTitlePaint.getFontMetrics().descent;\n                    float TextHeight = Math.abs(fontMetricsForTitle.ascent) + Math.abs(fontMetricsForTitle.descent);\n\n                    if (page.getTxtLists() != null) {\n                        for (TxtChar c : page.getTxtLists().get(i).getCharsData()) {\n                            rightPosition = leftposition + c.getCharWidth();\n                            Point tlp = new Point();\n                            c.setTopLeftPosition(tlp);\n                            tlp.x = (int) leftposition;\n                            tlp.y = (int) (bottomPosition - TextHeight);\n\n                            Point blp = new Point();\n                            c.setBottomLeftPosition(blp);\n                            blp.x = (int) leftposition;\n                            blp.y = (int) bottomPosition;\n\n                            Point trp = new Point();\n                            c.setTopRightPosition(trp);\n                            trp.x = (int) rightPosition;\n                            trp.y = (int) (bottomPosition - TextHeight);\n\n                            Point brp = new Point();\n                            c.setBottomRightPosition(brp);\n                            brp.x = (int) rightPosition;\n                            brp.y = (int) bottomPosition;\n                            ppp++;\n                            c.setIndex(ppp);\n\n                            leftposition = rightPosition;\n                        }\n                    }\n                    //pzl\n                }\n                top += (i == page.getTitleLines() - 1) ? titlePara : titleInterval;\n                if (!linePosSet && chapterPos == mCurChapterPos && top > titlePara) {\n                    linePos = i;\n                    linePosSet = true;\n                }\n            }\n            if (top > totalHeight) break;\n            // 首页画封面\n            if (pagePos == 0 && chapterPos == 0) {\n                drawCover(canvas, top);\n                top += getCoverHeight();\n            }\n            if (top > totalHeight) break;\n            for (int i = page.getTitleLines(); i < page.size(); i++) {\n                str = page.getLine(i);\n                strLength = strLength + str.length();\n                int paragraphLength = page.getPosition() == 0 ? strLength : chapter.getPageLength(page.getPosition() - 1) + strLength;\n                isLight = ReadAloudService.running && readAloudParagraph == chapter.getParagraphIndex(paragraphLength);\n                mTextPaint.setColor(isLight ? ThemeStore.accentColor(mContext) : readBookControl.getTextColor());\n                if (top > totalHeight) {\n                    break;\n                } else if (top > startHeight) {\n                    Layout tempLayout = new StaticLayout(str, mTextPaint, mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false);\n                    float width = StaticLayout.getDesiredWidth(str, tempLayout.getLineStart(0), tempLayout.getLineEnd(0), mTextPaint);\n                    if (needScale(str)) {\n                        drawScaledText(canvas, str, width, mTextPaint, top, i, page.getTxtLists());\n                    } else {\n                        canvas.drawText(str, mMarginLeft, top, mTextPaint);\n                    }\n                    //记录文字位置 --开始 pzl\n\n                    float leftposition = mMarginLeft;\n\n                    if (isFirstLineOfParagraph(str)) {\n                        String blanks = StringUtils.halfToFull(\"  \");\n                        //canvas.drawText(blanks, x, top, mTextPaint);\n                        float bw = StaticLayout.getDesiredWidth(blanks, mTextPaint);\n                        leftposition += bw;\n                    }\n\n                    float rightposition = 0;\n                    float bottomposition = top + mTextPaint.getFontMetrics().descent;\n                    float textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);\n\n                    if (page.getTxtLists() != null) {\n                        for (TxtChar c : page.getTxtLists().get(i).getCharsData()) {\n\n                            rightposition = leftposition + c.getCharWidth();\n                            Point tlp = new Point();\n                            c.setTopLeftPosition(tlp);\n                            tlp.x = (int) leftposition;\n                            tlp.y = (int) (bottomposition - textHeight);\n\n                            Point blp = new Point();\n                            c.setBottomLeftPosition(blp);\n                            blp.x = (int) leftposition;\n                            blp.y = (int) bottomposition;\n\n                            Point trp = new Point();\n                            c.setTopRightPosition(trp);\n                            trp.x = (int) rightposition;\n                            trp.y = (int) (bottomposition - textHeight);\n\n                            Point brp = new Point();\n                            c.setBottomRightPosition(brp);\n                            brp.x = (int) rightposition;\n                            brp.y = (int) bottomposition;\n\n                            leftposition = rightposition;\n\n                            ppp++;\n                            c.setIndex(ppp);\n                        }\n                    }\n                    //记录文字位置 --结束 pzl\n                }\n                top += str.endsWith(\"\\n\") ? textPara : textInterval;\n                if (!linePosSet && chapterPos == mCurChapterPos && top >= textPara) {\n                    linePos = i;\n                    linePosSet = true;\n                }\n            }\n            if (top > totalHeight) break;\n            if (pagePos == chapter.getPageSize() - 1) {\n                String sign = \"\\u23af \\u23af\";\n                if (chapterPos == book.getChapterListSize() - 1) {\n                    bookEnd = pagePos == mCurPagePos;\n                    str = sign + \" 所有章节已读完 \" + sign;\n                } else {\n                    str = sign + \" 本章完 \" + sign;\n                }\n                top += textPara;\n                canvas.drawText(str, mDisplayWidth / 2f, top, mTextEndPaint);\n                top += textPara * 2;\n            }\n            if (top > totalHeight) break;\n            if (chapter.getPageSize() == 1) {\n                float pHeight = getFixedPageHeight(chapter, pagePos);\n                if (top - topi < pHeight) {\n                    top = topi + pHeight;\n                }\n                if (top > totalHeight) break;\n            }\n            if (pagePos >= chapter.getPageSize() - 1) {\n                chapterPos += 1;\n                pagePos = 0;\n                top += 60;\n            } else {\n                pagePos += 1;\n            }\n            if (bookEnd && top < mVisibleHeight) {\n                isLastPage = true;\n                break;\n            }\n        }\n    }\n\n    void resetPageOffset() {\n        pageOffset = 0;\n        linePos = 0;\n        isLastPage = false;\n    }\n\n    private void switchToPageOffset(int offset) {\n        switch (offset) {\n            case 1:\n                if (mCurPagePos < curChapter().txtChapter.getPageSize() - 1) {\n                    mCurPagePos = mCurPagePos + 1;\n                } else if (mCurChapterPos < book.getChapterListSize() - 1) {\n                    mCurChapterPos = mCurChapterPos + 1;\n                    Collections.swap(chapterContainers, 0, 1);\n                    Collections.swap(chapterContainers, 1, 2);\n                    nextChapter().txtChapter = null;\n                    mCurPagePos = 0;\n                    if (curChapter().txtChapter == null) {\n                        curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n                        parseCurChapter();\n                    } else {\n                        parseNextChapter();\n                    }\n                }\n                break;\n            case -1:\n                if (mCurPagePos > 0) {\n                    mCurPagePos = mCurPagePos - 1;\n                } else if (mCurChapterPos > 0) {\n                    mCurChapterPos = mCurChapterPos - 1;\n                    Collections.swap(chapterContainers, 2, 1);\n                    Collections.swap(chapterContainers, 1, 0);\n                    prevChapter().txtChapter = null;\n                    if (curChapter().txtChapter == null) {\n                        curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n                        mCurPagePos = 0;\n                        parseCurChapter();\n                    } else {\n                        mCurPagePos = curChapter().txtChapter.getPageSize() - 1;\n                        parsePrevChapter();\n                    }\n                }\n                break;\n            default:\n                break;\n        }\n    }\n\n    private float getFixedPageHeight(TxtChapter chapter, int pagePos) {\n        float height = getPageHeight(chapter, pagePos);\n        if (height == 0) {\n            return height;\n        }\n        int lastPageIndex = chapter.getPageSize() - 1;\n        if (pagePos == lastPageIndex) {\n            height += 60 + textPara * 3;\n        }\n        if (lastPageIndex <= 0 && height < mVisibleHeight / 2.0f) {\n            height = mVisibleHeight / 2.0f;\n        }\n        return height;\n    }\n\n    private float getPageHeight(TxtChapter chapter, int pagePos) {\n        float height = 0;\n        if (chapter == null || chapter.getStatus() != TxtChapter.Status.FINISH) {\n            return height;\n        }\n        if (pagePos >= 0 && pagePos < chapter.getPageSize()) {\n            height = getPageHeight(chapter.getPage(pagePos));\n        }\n        if (chapter.getPosition() == 0 && pagePos == 0) {\n            height += getCoverHeight();\n        }\n        return height;\n    }\n\n    private float getPageHeight(TxtPage page) {\n        if (page.getLines().isEmpty())\n            return 0;\n        float height = 0;\n        if (page.getTitleLines() > 0)\n            height += titleInterval * (page.getTitleLines() - 1) + titlePara;\n        for (int i = page.getTitleLines(); i < page.size(); i++) {\n            height += page.getLine(i).endsWith(\"\\n\") ? textPara : textInterval;\n        }\n        return height;\n    }\n\n    private void drawErrorMsg(Canvas canvas, String msg, float offset) {\n        Layout tempLayout = new StaticLayout(msg, mTextPaint, mVisibleWidth, Layout.Alignment.ALIGN_NORMAL, 0, 0, false);\n        List<String> linesData = new ArrayList<>();\n        for (int i = 0; i < tempLayout.getLineCount(); i++) {\n            linesData.add(msg.substring(tempLayout.getLineStart(i), tempLayout.getLineEnd(i)));\n        }\n        float pivotY = (mDisplayHeight - textInterval * linesData.size()) / 3f - offset;\n        for (String str : linesData) {\n            float textWidth = mTextPaint.measureText(str);\n            float pivotX = (mDisplayWidth - textWidth) / 2;\n            canvas.drawText(str, pivotX, pivotY, mTextPaint);\n            pivotY += textInterval;\n        }\n    }\n\n    /**\n     * 获取状态文本\n     */\n    private String getStatusText(TxtChapter chapter) {\n        String tip = \"\";\n        switch (chapter.getStatus()) {\n            case LOADING:\n                tip = mContext.getString(R.string.loading);\n                break;\n            case ERROR:\n                tip = mContext.getString(R.string.load_error_msg, curChapter().txtChapter.getMsg());\n                break;\n            case EMPTY:\n                tip = mContext.getString(R.string.content_empty);\n                break;\n            case CATEGORY_EMPTY:\n                tip = mContext.getString(R.string.chapter_list_empty);\n                break;\n            case CHANGE_SOURCE:\n                tip = mContext.getString(R.string.on_change_source);\n        }\n        return tip;\n    }\n\n    /**\n     * 判断是否存在上一页\n     */\n    boolean hasPrev() {\n        // 以下情况禁止翻页\n        if (canNotTurnPage()) {\n            return false;\n        }\n        if (getPageStatus() == TxtChapter.Status.FINISH) {\n            // 先查看是否存在上一页\n            if (mCurPagePos > 0) {\n                return true;\n            }\n        }\n        return mCurChapterPos > 0;\n    }\n\n    /**\n     * 判断是否下一页存在\n     */\n    boolean hasNext(int pageOnCur) {\n        // 以下情况禁止翻页\n        if (canNotTurnPage()) {\n            return false;\n        }\n        if (getPageStatus() == TxtChapter.Status.FINISH) {\n            // 先查看是否存在下一页\n            if (mCurPagePos + pageOnCur < curChapter().txtChapter.getPageSize() - 1) {\n                return true;\n            }\n        }\n        return mCurChapterPos + 1 < book.getChapterListSize();\n    }\n\n    /**\n     * 解析当前页数据\n     */\n    void parseCurChapter() {\n        if (curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) {\n            Single.create((SingleOnSubscribe<TxtChapter>) e -> {\n                ChapterProvider chapterProvider = new ChapterProvider(this);\n                TxtChapter txtChapter = chapterProvider.dealLoadPageList(callback.getChapterList().get(mCurChapterPos), mPageView.isPrepare());\n                e.onSuccess(txtChapter);\n            })\n                    .compose(RxUtils::toSimpleSingle)\n                    .subscribe(new SingleObserver<TxtChapter>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            compositeDisposable.add(d);\n                        }\n\n                        @Override\n                        public void onSuccess(TxtChapter txtChapter) {\n                            upTextChapter(txtChapter);\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            if (curChapter().txtChapter == null || curChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) {\n                                curChapter().txtChapter = new TxtChapter(mCurChapterPos);\n                                curChapter().txtChapter.setStatus(TxtChapter.Status.ERROR);\n                                curChapter().txtChapter.setMsg(e.getMessage());\n                            }\n                        }\n                    });\n        }\n        parsePrevChapter();\n        parseNextChapter();\n    }\n\n    /**\n     * 解析上一章数据\n     */\n    void parsePrevChapter() {\n        final int prevChapterPos = mCurChapterPos - 1;\n        if (prevChapterPos < 0) {\n            prevChapter().txtChapter = null;\n            return;\n        }\n        if (prevChapter().txtChapter == null)\n            prevChapter().txtChapter = new TxtChapter(prevChapterPos);\n        if (prevChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) {\n            return;\n        }\n        Single.create((SingleOnSubscribe<TxtChapter>) e -> {\n            ChapterProvider chapterProvider = new ChapterProvider(this);\n            TxtChapter txtChapter = chapterProvider.dealLoadPageList(callback.getChapterList().get(prevChapterPos), mPageView.isPrepare());\n            e.onSuccess(txtChapter);\n        })\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new SingleObserver<TxtChapter>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onSuccess(TxtChapter txtChapter) {\n                        upTextChapter(txtChapter);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        if (prevChapter().txtChapter == null || prevChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) {\n                            prevChapter().txtChapter = new TxtChapter(prevChapterPos);\n                            prevChapter().txtChapter.setStatus(TxtChapter.Status.ERROR);\n                            prevChapter().txtChapter.setMsg(e.getMessage());\n                        }\n                    }\n                });\n    }\n\n    /**\n     * 解析下一章数据\n     */\n    void parseNextChapter() {\n        final int nextChapterPos = mCurChapterPos + 1;\n        if (nextChapterPos >= callback.getChapterList().size()) {\n            nextChapter().txtChapter = null;\n            return;\n        }\n        if (nextChapter().txtChapter == null)\n            nextChapter().txtChapter = new TxtChapter(nextChapterPos);\n        if (nextChapter().txtChapter.getStatus() == TxtChapter.Status.FINISH) {\n            return;\n        }\n        Single.create((SingleOnSubscribe<TxtChapter>) e -> {\n            ChapterProvider chapterProvider = new ChapterProvider(this);\n            TxtChapter txtChapter = chapterProvider.dealLoadPageList(callback.getChapterList().get(nextChapterPos), mPageView.isPrepare());\n            e.onSuccess(txtChapter);\n        })\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new SingleObserver<TxtChapter>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onSuccess(TxtChapter txtChapter) {\n                        upTextChapter(txtChapter);\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        if (nextChapter().txtChapter == null || nextChapter().txtChapter.getStatus() != TxtChapter.Status.FINISH) {\n                            nextChapter().txtChapter = new TxtChapter(nextChapterPos);\n                            nextChapter().txtChapter.setStatus(TxtChapter.Status.ERROR);\n                            nextChapter().txtChapter.setMsg(e.getMessage());\n                        }\n                    }\n                });\n    }\n\n    private void upTextChapter(TxtChapter txtChapter) {\n        if (txtChapter.getPosition() == mCurChapterPos - 1) {\n            prevChapter().txtChapter = txtChapter;\n            if (mPageMode == PageAnimation.Mode.SCROLL) {\n                mPageView.drawContent(-1);\n            } else {\n                mPageView.drawPage(-1);\n            }\n        } else if (txtChapter.getPosition() == mCurChapterPos) {\n            curChapter().txtChapter = txtChapter;\n            reSetPage();\n            chapterChangeCallback();\n            pagingEnd(PageAnimation.Direction.NONE);\n        } else if (txtChapter.getPosition() == mCurChapterPos + 1) {\n            nextChapter().txtChapter = txtChapter;\n            if (mPageMode == PageAnimation.Mode.SCROLL) {\n                mPageView.drawContent(1);\n            } else {\n                mPageView.drawPage(1);\n            }\n        }\n    }\n\n    private void drawScaledText(Canvas canvas, String line, float lineWidth, TextPaint paint, float top, int y, List<TxtLine> txtLists) {\n        float x = mMarginLeft;\n\n        if (isFirstLineOfParagraph(line)) {\n            canvas.drawText(indent, x, top, paint);\n            float bw = StaticLayout.getDesiredWidth(indent, paint);\n            x += bw;\n            line = line.substring(readBookControl.getIndent());\n        }\n        int gapCount = line.length() - 1;\n        int i = 0;\n\n        TxtLine txtList = new TxtLine();//每一行pzl\n        txtList.setCharsData(new ArrayList<>());//pzl\n\n        float d = ((mDisplayWidth - (mMarginLeft + mMarginRight)) - lineWidth) / gapCount;\n        for (; i < line.length(); i++) {\n            String c = String.valueOf(line.charAt(i));\n            float cw = StaticLayout.getDesiredWidth(c, paint);\n            canvas.drawText(c, x, top, paint);\n            //pzl\n            TxtChar txtChar = new TxtChar();\n            txtChar.setChardata(line.charAt(i));\n            if (i == 0) txtChar.setCharWidth(cw + d / 2);\n            if (i == gapCount) txtChar.setCharWidth(cw + d / 2);\n            txtChar.setCharWidth(cw + d);\n            ;//字宽\n            //txtChar.Index = y;//每页每个字的位置\n            txtList.getCharsData().add(txtChar);\n            //pzl\n            x += cw + d;\n        }\n        if (txtLists != null) {\n            txtLists.set(y, txtList);//pzl\n        }\n    }\n\n    //判断是不是d'hou\n    private boolean isFirstLineOfParagraph(String line) {\n        return line.length() > 3 && line.charAt(0) == (char) 12288 && line.charAt(1) == (char) 12288;\n    }\n\n    private boolean needScale(String line) {//判断不是空行\n        return line != null && line.length() != 0 && line.charAt(line.length() - 1) != '\\n';\n    }\n\n    private void chapterChangeCallback() {\n        if (callback != null) {\n            readAloudParagraph = -1;\n            callback.onChapterChange(mCurChapterPos);\n            callback.onPageCountChange(curChapter().txtChapter != null ? curChapter().txtChapter.getPageSize() : 0);\n        }\n    }\n\n    public abstract void updateChapter();\n\n    /**\n     * 根据当前状态，决定是否能够翻页\n     */\n    private boolean canNotTurnPage() {\n        return !isChapterListPrepare\n                || getPageStatus() == TxtChapter.Status.CHANGE_SOURCE;\n    }\n\n    /**\n     * 关闭书本\n     */\n    public void closeBook() {\n        compositeDisposable.dispose();\n        compositeDisposable = null;\n\n        isChapterListPrepare = false;\n        isClose = true;\n\n        prevChapter().txtChapter = null;\n        curChapter().txtChapter = null;\n        nextChapter().txtChapter = null;\n    }\n\n    public boolean isClose() {\n        return isClose;\n    }\n\n    private ChapterContainer prevChapter() {\n        return chapterContainers.get(0);\n    }\n\n    ChapterContainer curChapter() {\n        return chapterContainers.get(1);\n    }\n\n    private ChapterContainer nextChapter() {\n        return chapterContainers.get(2);\n    }\n\n    /*****************************************interface*****************************************/\n\n    static class ChapterContainer {\n        TxtChapter txtChapter;\n    }\n\n    /**\n     * --------------------\n     * 检测获取按压坐标所在位置的字符，没有的话返回null\n     * --------------------\n     * author: huangwei\n     * 2017年7月4日上午10:23:19\n     */\n    TxtChar detectPressTxtChar(float down_X2, float down_Y2) {\n        TxtPage txtPage = curChapter().txtChapter.getPage(mCurPagePos);\n        if (txtPage == null) return null;\n        List<TxtLine> txtLines = txtPage.getTxtLists();\n        if (txtLines == null) return null;\n        for (TxtLine l : txtLines) {\n            List<TxtChar> txtChars = l.getCharsData();\n            if (txtChars != null) {\n                for (TxtChar c : txtChars) {\n                    Point leftPoint = c.getBottomLeftPosition();\n                    Point rightPoint = c.getBottomRightPosition();\n                    if (leftPoint != null && down_Y2 > leftPoint.y) {\n                        break;// 说明是在下一行\n                    }\n                    if (leftPoint != null && rightPoint != null && down_X2 >= leftPoint.x && down_X2 <= rightPoint.x) {\n                        return c;\n                    }\n\n                }\n            }\n        }\n        return null;\n    }\n\n    public interface Callback {\n        List<BookChapterBean> getChapterList();\n\n        /**\n         * 作用：章节切换的时候进行回调\n         *\n         * @param pos:切换章节的序号\n         */\n        void onChapterChange(int pos);\n\n        /**\n         * 作用：章节目录加载完成时候回调\n         *\n         * @param chapters：返回章节目录\n         */\n        void onCategoryFinish(List<BookChapterBean> chapters);\n\n        /**\n         * 作用：章节页码数量改变之后的回调。==> 字体大小的调整，或者是否关闭虚拟按钮功能都会改变页面的数量。\n         *\n         * @param count:页面的数量\n         */\n        void onPageCountChange(int count);\n\n        /**\n         * 作用：当页面改变的时候回调\n         *\n         * @param chapterIndex   章节序号\n         * @param pageIndex      页数\n         * @param resetReadAloud 是否重置朗读\n         */\n        void onPageChange(int chapterIndex, int pageIndex, boolean resetReadAloud);\n\n        void vipPop();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoaderEpub.java",
    "content": "package com.kunfei.bookshelf.widget.page;\n\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.Canvas;\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.utils.RxUtils;\nimport com.kunfei.bookshelf.utils.StringUtils;\n\nimport net.sf.jazzlib.ZipFile;\n\nimport org.jsoup.Jsoup;\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.nodes.TextNode;\nimport org.jsoup.select.Elements;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.Observer;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\nimport nl.siegmann.epublib.domain.Book;\nimport nl.siegmann.epublib.domain.MediaType;\nimport nl.siegmann.epublib.domain.Metadata;\nimport nl.siegmann.epublib.domain.Resource;\nimport nl.siegmann.epublib.domain.SpineReference;\nimport nl.siegmann.epublib.domain.TOCReference;\nimport nl.siegmann.epublib.epub.EpubReader;\nimport nl.siegmann.epublib.service.MediatypeService;\n\npublic class PageLoaderEpub extends PageLoader {\n\n    //编码类型\n    private Charset mCharset;\n\n    private Book epubBook;\n\n    private List<BookChapterBean> chapterList;\n\n    PageLoaderEpub(PageView pageView, BookShelfBean bookShelfBean, Callback callback) {\n        super(pageView, bookShelfBean, callback);\n    }\n\n    @Override\n    public void refreshChapterList() {\n        if (book == null) return;\n\n        Observable.create((ObservableOnSubscribe<BookShelfBean>) e -> {\n            File bookFile = new File(book.getNoteUrl());\n            epubBook = readBook(bookFile);\n\n            if (epubBook == null) {\n                e.onError(new Exception(\"文件解析失败\"));\n                return;\n            }\n            if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) {\n                book.getBookInfoBean().setCharset(\"UTF-8\");\n            }\n            mCharset = Charset.forName(book.getBookInfoBean().getCharset());\n\n            e.onNext(book);\n            e.onComplete();\n        }).subscribeOn(Schedulers.single())\n                .flatMap(this::checkChapterList)\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new MyObserver<BookShelfBean>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(BookShelfBean bookShelfBean) {\n                        isChapterListPrepare = true;\n                        // 加载并显示当前章节\n                        skipToChapter(bookShelfBean.getDurChapter(), bookShelfBean.getDurChapterPage());\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        durDhapterError(e.getMessage());\n                    }\n                });\n    }\n\n    public static Book readBook(File file) {\n        try {\n            EpubReader epubReader = new EpubReader();\n            MediaType[] lazyTypes = {\n                    MediatypeService.CSS,\n                    MediatypeService.GIF,\n                    MediatypeService.JPG,\n                    MediatypeService.PNG,\n                    MediatypeService.MP3,\n                    MediatypeService.MP4};\n            ZipFile zipFile = new ZipFile(file);\n            return epubReader.readEpubLazy(zipFile, \"utf-8\", Arrays.asList(lazyTypes));\n        } catch (Exception e) {\n            return null;\n        }\n    }\n\n    private void extractScaledCoverImage() {\n        try {\n            byte[] data = epubBook.getCoverImage().getData();\n            Bitmap rawCover = BitmapFactory.decodeByteArray(data, 0, data.length);\n            int width = rawCover.getWidth();\n            int height = rawCover.getHeight();\n            if (width <= mVisibleWidth && width >= 0.8 * mVisibleWidth) {\n                cover = rawCover;\n                return;\n            }\n            height = Math.round(mVisibleWidth * 1.0f * height / width);\n            cover = Bitmap.createScaledBitmap(rawCover, mVisibleWidth, height, true);\n        } catch (Exception ignored) {\n        }\n    }\n\n    @Override\n    public void drawCover(Canvas canvas, float top) {\n        if (cover == null) {\n            extractScaledCoverImage();\n        }\n        if (cover == null)\n            return;\n        int left = (mDisplayWidth - cover.getWidth()) / 2;\n        canvas.drawBitmap(cover, left, top, mTextPaint);\n    }\n\n    private List<BookChapterBean> loadChapters() {\n        Metadata metadata = epubBook.getMetadata();\n        book.getBookInfoBean().setName(metadata.getFirstTitle());\n        if (metadata.getAuthors().size() > 0) {\n            String author = metadata.getAuthors().get(0).toString().replaceAll(\"^, |, $\", \"\");\n            book.getBookInfoBean().setAuthor(author);\n        }\n        if (metadata.getDescriptions().size() > 0) {\n            book.getBookInfoBean().setIntroduce(Jsoup.parse(metadata.getDescriptions().get(0)).text());\n        }\n        chapterList = new ArrayList<>();\n        List<TOCReference> refs = epubBook.getTableOfContents().getTocReferences();\n        if (refs == null || refs.isEmpty()) {\n            List<SpineReference> spineReferences = epubBook.getSpine().getSpineReferences();\n            for (int i = 0, size = spineReferences.size(); i < size; i++) {\n                Resource resource = spineReferences.get(i).getResource();\n                String title = resource.getTitle();\n                if (TextUtils.isEmpty(title)) {\n                    try {\n                        Document doc = Jsoup.parse(new String(resource.getData(), mCharset));\n                        Elements elements = doc.getElementsByTag(\"title\");\n                        if (elements.size() > 0) {\n                            title = elements.get(0).text();\n                        }\n                    } catch (IOException e) {\n                        e.printStackTrace();\n                    }\n                }\n                BookChapterBean bean = new BookChapterBean();\n                bean.setDurChapterIndex(i);\n                bean.setNoteUrl(bean.getNoteUrl());\n                bean.setDurChapterUrl(resource.getHref());\n                if (i == 0 && title.isEmpty()) {\n                    bean.setDurChapterName(\"封面\");\n                } else {\n                    bean.setDurChapterName(title);\n                }\n                chapterList.add(bean);\n            }\n        } else {\n            parseMenu(refs, 0);\n            for (int i = 0; i < chapterList.size(); i++) {\n                chapterList.get(i).setDurChapterIndex(i);\n            }\n        }\n\n        return chapterList;\n    }\n\n    private void parseMenu(List<TOCReference> refs, int level) {\n        if (refs == null) return;\n        for (TOCReference ref : refs) {\n            if (ref.getResource() != null) {\n                BookChapterBean bookChapterBean = new BookChapterBean();\n                bookChapterBean.setNoteUrl(book.getNoteUrl());\n                bookChapterBean.setDurChapterName(ref.getTitle());\n                bookChapterBean.setDurChapterUrl(ref.getCompleteHref());\n                chapterList.add(bookChapterBean);\n            }\n            if (ref.getChildren() != null && !ref.getChildren().isEmpty()) {\n                parseMenu(ref.getChildren(), level + 1);\n            }\n        }\n    }\n\n    @Override\n    protected String getChapterContent(BookChapterBean chapter) throws Exception {\n        Resource resource = epubBook.getResources().getByHref(chapter.getDurChapterUrl());\n        StringBuilder content = new StringBuilder();\n        Document doc = Jsoup.parse(new String(resource.getData(), mCharset));\n        Elements elements = doc.getAllElements();\n        for (Element element : elements) {\n            List<TextNode> contentEs = element.textNodes();\n            for (int i = 0; i < contentEs.size(); i++) {\n                String text = contentEs.get(i).text().trim();\n                text = StringUtils.formatHtml(text);\n                if (elements.size() > 1) {\n                    if (text.length() > 0) {\n                        if (content.length() > 0) {\n                            content.append(\"\\r\\n\");\n                        }\n                        content.append(\"\\u3000\\u3000\").append(text);\n                    }\n                } else {\n                    content.append(text);\n                }\n            }\n        }\n        return content.toString();\n    }\n\n    private Observable<BookShelfBean> checkChapterList(BookShelfBean collBook) {\n        if (!collBook.getHasUpdate() && !callback.getChapterList().isEmpty()) {\n            return Observable.just(collBook);\n        } else {\n            return Observable.create((ObservableOnSubscribe<List<BookChapterBean>>) e -> {\n                List<BookChapterBean> chapterList = loadChapters();\n                if (!chapterList.isEmpty()) {\n                    e.onNext(chapterList);\n                } else {\n                    e.onError(new IllegalAccessException(\"epubBook sub-chapter failed!\"));\n                }\n                e.onComplete();\n            })\n                    .flatMap(chapterList -> {\n                        collBook.setChapterListSize(chapterList.size());\n                        callback.onCategoryFinish(chapterList);\n                        return Observable.just(collBook);\n                    })\n                    .doOnNext(bookShelfBean -> {\n                        // 存储章节到数据库\n                        bookShelfBean.setHasUpdate(false);\n                        bookShelfBean.setFinalRefreshData(System.currentTimeMillis());\n                    });\n        }\n    }\n\n    @Override\n    protected boolean noChapterData(BookChapterBean chapter) {\n        return false;\n    }\n\n    @Override\n    public void updateChapter() {\n        mPageView.getActivity().toast(\"目录更新中\");\n        Observable.create((ObservableOnSubscribe<BookShelfBean>) e -> {\n            if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) {\n                book.getBookInfoBean().setCharset(\"UTF-8\");\n            }\n            mCharset = Charset.forName(book.getBookInfoBean().getCharset());\n            //清除原目录\n            BookshelfHelp.delChapterList(book.getNoteUrl());\n            callback.getChapterList().clear();\n            e.onNext(book);\n            e.onComplete();\n        }).flatMap(this::checkChapterList)\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new Observer<BookShelfBean>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(BookShelfBean bookShelfBean) {\n                        mPageView.getActivity().toast(\"更新完成\");\n                        isChapterListPrepare = true;\n                        // 加载并显示当前章节\n                        skipToChapter(bookShelfBean.getDurChapter(), bookShelfBean.getDurChapterPage());\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        durDhapterError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoaderNet.java",
    "content": "package com.kunfei.bookshelf.widget.page;\n\nimport android.annotation.SuppressLint;\n\nimport com.kunfei.bookshelf.base.observer.MyObserver;\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookContentBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.WebBookModel;\nimport com.kunfei.bookshelf.model.content.VipThrowable;\nimport com.kunfei.bookshelf.model.content.WebBook;\nimport com.kunfei.bookshelf.utils.NetworkUtils;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\nimport io.reactivex.Observable;\nimport io.reactivex.ObservableOnSubscribe;\nimport io.reactivex.Observer;\nimport io.reactivex.Scheduler;\nimport io.reactivex.android.schedulers.AndroidSchedulers;\nimport io.reactivex.disposables.Disposable;\nimport io.reactivex.schedulers.Schedulers;\n\n/**\n * 网络页面加载器\n */\n\npublic class PageLoaderNet extends PageLoader {\n    private static final String TAG = \"PageLoaderNet\";\n    private List<String> downloadingChapterList = new ArrayList<>();\n    private ExecutorService executorService;\n    private Scheduler scheduler;\n\n    PageLoaderNet(PageView pageView, BookShelfBean bookShelfBean, Callback callback) {\n        super(pageView, bookShelfBean, callback);\n        executorService = Executors.newFixedThreadPool(20);\n        scheduler = Schedulers.from(executorService);\n    }\n\n    @Override\n    public void refreshChapterList() {\n        if (!callback.getChapterList().isEmpty()) {\n            isChapterListPrepare = true;\n            // 打开章节\n            skipToChapter(book.getDurChapter(), book.getDurChapterPage());\n        } else {\n            WebBookModel.getInstance().getChapterList(book)\n                    .compose(RxUtils::toSimpleSingle)\n                    .subscribe(new MyObserver<List<BookChapterBean>>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            compositeDisposable.add(d);\n                        }\n\n                        @Override\n                        public void onNext(List<BookChapterBean> chapterBeanList) {\n                            isChapterListPrepare = true;\n                            // 目录加载完成\n                            if (!chapterBeanList.isEmpty()) {\n                                BookshelfHelp.delChapterList(book.getNoteUrl());\n                                callback.onCategoryFinish(chapterBeanList);\n                            }\n                            // 加载并显示当前章节\n                            skipToChapter(book.getDurChapter(), book.getDurChapterPage());\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            if (e instanceof WebBook.NoSourceThrowable) {\n                                mPageView.autoChangeSource();\n                            } else {\n                                durDhapterError(e.getMessage());\n                            }\n                        }\n                    });\n        }\n    }\n\n\n    public void changeSourceFinish(BookShelfBean bookShelfBean) {\n        if (bookShelfBean == null) {\n            openChapter(book.getDurChapter());\n        } else {\n            this.book = bookShelfBean;\n            refreshChapterList();\n        }\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    private synchronized void loadContent(final int chapterIndex) {\n        if (downloadingChapterList.size() >= 20) return;\n        if (chapterIndex >= callback.getChapterList().size()\n                || DownloadingList(listHandle.CHECK, callback.getChapterList().get(chapterIndex).getDurChapterUrl()))\n            return;\n        if (null != book && callback.getChapterList().size() > 0) {\n            Observable.create((ObservableOnSubscribe<Integer>) e -> {\n                if (shouldRequestChapter(chapterIndex)) {\n                    DownloadingList(listHandle.ADD, callback.getChapterList().get(chapterIndex).getDurChapterUrl());\n                    e.onNext(chapterIndex);\n                }\n                e.onComplete();\n            })\n                    .flatMap(index -> WebBookModel.getInstance().getBookContent(book, callback.getChapterList().get(chapterIndex), null))\n                    .subscribeOn(scheduler)\n                    .observeOn(AndroidSchedulers.mainThread())\n                    .subscribe(new MyObserver<BookContentBean>() {\n                        @Override\n                        public void onSubscribe(Disposable d) {\n                            compositeDisposable.add(d);\n                        }\n\n                        @SuppressLint(\"DefaultLocale\")\n                        @Override\n                        public void onNext(BookContentBean bookContentBean) {\n                            DownloadingList(listHandle.REMOVE, bookContentBean.getDurChapterUrl());\n                            finishContent(bookContentBean.getDurChapterIndex());\n                        }\n\n                        @Override\n                        public void onError(Throwable e) {\n                            DownloadingList(listHandle.REMOVE, callback.getChapterList().get(chapterIndex).getDurChapterUrl());\n                            if (chapterIndex == book.getDurChapter()) {\n                                if (e instanceof WebBook.NoSourceThrowable) {\n                                    mPageView.autoChangeSource();\n                                } else if (e instanceof VipThrowable) {\n                                    callback.vipPop();\n                                } else {\n                                    durDhapterError(e.getMessage());\n                                }\n                            }\n                        }\n                    });\n        }\n    }\n\n    /**\n     * 编辑下载列表\n     */\n    private synchronized boolean DownloadingList(listHandle editType, String value) {\n        if (editType == listHandle.ADD) {\n            downloadingChapterList.add(value);\n            return true;\n        } else if (editType == listHandle.REMOVE) {\n            downloadingChapterList.remove(value);\n            return true;\n        } else {\n            return downloadingChapterList.indexOf(value) != -1;\n        }\n    }\n\n    /**\n     * 章节下载完成\n     */\n    private void finishContent(int chapterIndex) {\n        if (chapterIndex == mCurChapterPos) {\n            super.parseCurChapter();\n        }\n        if (chapterIndex == mCurChapterPos - 1) {\n            super.parsePrevChapter();\n        }\n        if (chapterIndex == mCurChapterPos + 1) {\n            super.parseNextChapter();\n        }\n    }\n\n    /**\n     * 刷新当前章节\n     */\n    @SuppressLint(\"DefaultLocale\")\n    public void refreshDurChapter() {\n        if (callback.getChapterList().isEmpty()) {\n            updateChapter();\n            return;\n        }\n        if (callback.getChapterList().size() - 1 < mCurChapterPos) {\n            mCurChapterPos = callback.getChapterList().size() - 1;\n        }\n        BookshelfHelp.delChapter(BookshelfHelp.getCachePathName(book.getBookInfoBean().getName(), book.getTag()),\n                mCurChapterPos, callback.getChapterList().get(mCurChapterPos).getDurChapterName());\n        skipToChapter(mCurChapterPos, 0);\n    }\n\n    @Override\n    protected String getChapterContent(BookChapterBean chapter) {\n        return BookshelfHelp.getChapterCache(book, chapter);\n    }\n\n    @SuppressLint(\"DefaultLocale\")\n    @Override\n    protected boolean noChapterData(BookChapterBean chapter) {\n        return !BookshelfHelp.isChapterCached(book.getBookInfoBean().getName(), book.getTag(), chapter, book.isAudio());\n    }\n\n    private boolean shouldRequestChapter(Integer chapterIndex) {\n        return NetworkUtils.isNetWorkAvailable() && noChapterData(callback.getChapterList().get(chapterIndex));\n    }\n\n    // 装载上一章节的内容\n    @Override\n    void parsePrevChapter() {\n        if (mCurChapterPos >= 1) {\n            loadContent(mCurChapterPos - 1);\n        }\n        super.parsePrevChapter();\n    }\n\n    // 装载当前章内容。\n    @Override\n    void parseCurChapter() {\n        for (int i = mCurChapterPos; i < Math.min(mCurChapterPos + 5, book.getChapterListSize()); i++) {\n            loadContent(i);\n        }\n        super.parseCurChapter();\n    }\n\n    // 装载下一章节的内容\n    @Override\n    void parseNextChapter() {\n        for (int i = mCurChapterPos; i < Math.min(mCurChapterPos + 5, book.getChapterListSize()); i++) {\n            loadContent(i);\n        }\n        super.parseNextChapter();\n    }\n\n    @Override\n    public void updateChapter() {\n        mPageView.getActivity().toast(\"目录更新中\");\n        WebBookModel.getInstance().getChapterList(book)\n                .subscribeOn(Schedulers.io())\n                .observeOn(AndroidSchedulers.mainThread())\n                .subscribe(new Observer<List<BookChapterBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onNext(List<BookChapterBean> chapterBeanList) {\n                        isChapterListPrepare = true;\n\n                        if (chapterBeanList.size() > callback.getChapterList().size()) {\n                            mPageView.getActivity().toast(\"更新完成,有新章节\");\n                            callback.onCategoryFinish(chapterBeanList);\n                        } else {\n                            mPageView.getActivity().toast(\"更新完成,无新章节\");\n                        }\n\n                        // 加载并显示当前章节\n                        skipToChapter(book.getDurChapter(), book.getDurChapterPage());\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        durDhapterError(e.getMessage());\n                    }\n\n                    @Override\n                    public void onComplete() {\n\n                    }\n                });\n    }\n\n    @Override\n    public void closeBook() {\n        super.closeBook();\n        executorService.shutdown();\n    }\n\n    public enum listHandle {\n        ADD, REMOVE, CHECK\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/PageLoaderText.java",
    "content": "package com.kunfei.bookshelf.widget.page;\n\nimport android.text.TextUtils;\n\nimport com.kunfei.bookshelf.bean.BookChapterBean;\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.BookshelfHelp;\nimport com.kunfei.bookshelf.model.TxtChapterRuleManager;\nimport com.kunfei.bookshelf.utils.EncodingDetect;\nimport com.kunfei.bookshelf.utils.IOUtils;\nimport com.kunfei.bookshelf.utils.MD5Utils;\nimport com.kunfei.bookshelf.utils.RxUtils;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.nio.charset.Charset;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport io.reactivex.Single;\nimport io.reactivex.SingleObserver;\nimport io.reactivex.SingleOnSubscribe;\nimport io.reactivex.disposables.Disposable;\n\nimport static com.kunfei.bookshelf.help.FileHelp.BLANK;\n\n/**\n * 加载本地书籍\n */\npublic class PageLoaderText extends PageLoader {\n    private static final String TAG = \"PageLoaderText\";\n    //默认从文件中获取数据的长度\n    private final static int BUFFER_SIZE = 512 * 1024;\n    //没有标题的时候，每个章节的最大长度\n    private final static int MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024;\n\n    private List<String> chapterPatterns = new ArrayList<>();\n    //章节解析模式\n    private Pattern mChapterPattern = null;\n    //获取书本的文件\n    private File mBookFile;\n    //编码类型\n    private Charset mCharset;\n\n    PageLoaderText(PageView pageView, BookShelfBean bookShelfBean, Callback callback) {\n        super(pageView, bookShelfBean, callback);\n    }\n\n    @Override\n    public void refreshChapterList() {\n        Single.create((SingleOnSubscribe<List<BookChapterBean>>) e -> {\n            // 对于文件是否存在，或者为空的判断，不作处理。 ==> 在文件打开前处理过了。\n            mBookFile = new File(book.getNoteUrl());\n            //获取文件编码\n            if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) {\n                book.getBookInfoBean().setCharset(EncodingDetect.getJavaEncode(mBookFile));\n            }\n            mCharset = Charset.forName(book.getBookInfoBean().getCharset());\n\n            Long lastModified = mBookFile.lastModified();\n            if (book.getFinalRefreshData() < lastModified) {\n                book.setFinalRefreshData(lastModified);\n                book.setHasUpdate(true);\n            }\n            if (book.getHasUpdate() || callback.getChapterList().size() == 0) {\n                List<BookChapterBean> chapterBeanList = loadChapters();\n                book.setHasUpdate(false);\n                e.onSuccess(chapterBeanList);\n            } else {\n                e.onSuccess(new ArrayList<>());\n            }\n        }).compose(RxUtils::toSimpleSingle)\n                .subscribe(new SingleObserver<List<BookChapterBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onSuccess(List<BookChapterBean> bookChapterBeans) {\n                        isChapterListPrepare = true;\n                        // 目录加载完成，执行回调操作。\n                        if (!bookChapterBeans.isEmpty()) {\n                            callback.onCategoryFinish(bookChapterBeans);\n                        }\n                        // 打开章节\n                        skipToChapter(book.getDurChapter(), book.getDurChapterPage());\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        durDhapterError(e.getMessage());\n                    }\n                });\n    }\n\n    @Override\n    protected String getChapterContent(BookChapterBean chapter) {\n        //从文件中获取数据\n        byte[] content = getChapterContentByte(chapter);\n        return new String(content, mCharset);\n    }\n\n    @Override\n    protected boolean noChapterData(BookChapterBean chapter) {\n        return false;\n    }\n\n    @Override\n    public void updateChapter() {\n        mPageView.getActivity().toast(\"目录更新中\");\n        Single.create((SingleOnSubscribe<List<BookChapterBean>>) e -> {\n            BookshelfHelp.delChapterList(book.getNoteUrl());\n            //获取文件编码\n            if (TextUtils.isEmpty(book.getBookInfoBean().getCharset())) {\n                book.getBookInfoBean().setCharset(EncodingDetect.getJavaEncode(mBookFile));\n            }\n            mCharset = Charset.forName(book.getBookInfoBean().getCharset());\n            e.onSuccess(loadChapters());\n        })\n                .compose(RxUtils::toSimpleSingle)\n                .subscribe(new SingleObserver<List<BookChapterBean>>() {\n                    @Override\n                    public void onSubscribe(Disposable d) {\n                        compositeDisposable.add(d);\n                    }\n\n                    @Override\n                    public void onSuccess(List<BookChapterBean> value) {\n                        isChapterListPrepare = true;\n                        mPageView.getActivity().toast(\"更新完成\");\n                        book.setHasUpdate(false);\n\n                        // 提示目录加载完成\n                        if (callback != null) {\n                            callback.onCategoryFinish(value);\n                        }\n\n                        // 加载并显示当前章节\n                        openChapter(book.getDurChapterPage());\n                    }\n\n                    @Override\n                    public void onError(Throwable e) {\n                        durDhapterError(e.getMessage());\n                    }\n                });\n    }\n\n    /**\n     * 从文件中提取一章的内容\n     */\n    private byte[] getChapterContentByte(BookChapterBean chapter) {\n        RandomAccessFile bookStream = null;\n        try {\n            bookStream = new RandomAccessFile(mBookFile, \"r\");\n            bookStream.seek(chapter.getStart());\n            int extent = (int) (chapter.getEnd() - chapter.getStart());\n            byte[] content = new byte[extent];\n            bookStream.read(content, 0, extent);\n            return content;\n        } catch (Exception e) {\n            e.printStackTrace();\n        } finally {\n            IOUtils.close(bookStream);\n        }\n        return new byte[0];\n    }\n\n    /**\n     * 1. 检查文件中是否存在章节名\n     * 2. 判断文件中使用的章节名类型的正则表达式\n     *\n     * @return 是否存在章节名\n     */\n    private boolean checkChapterType(RandomAccessFile bookStream) throws IOException {\n        chapterPatterns.clear();\n        if (TextUtils.isEmpty(book.getBookInfoBean().getChapterUrl())) {\n            chapterPatterns.addAll(TxtChapterRuleManager.enabledRuleList());\n        } else {\n            chapterPatterns.add(book.getBookInfoBean().getChapterUrl());\n        }\n        //首先获取128k的数据\n        byte[] buffer = new byte[BUFFER_SIZE / 4];\n        int length = bookStream.read(buffer, 0, buffer.length);\n        //进行章节匹配\n        for (String str : chapterPatterns) {\n            Pattern pattern = Pattern.compile(str, Pattern.MULTILINE);\n            Matcher matcher = pattern.matcher(new String(buffer, 0, length, mCharset));\n            //如果匹配存在，那么就表示当前章节使用这种匹配方式\n            if (matcher.find()) {\n                mChapterPattern = pattern;\n                //重置指针位置\n                bookStream.seek(0);\n                return true;\n            }\n        }\n\n        //重置指针位置\n        bookStream.seek(0);\n        return false;\n    }\n\n    /**\n     * 未完成的部分:\n     * 1. 序章的添加\n     * 2. 章节存在的书本的虚拟分章效果\n     */\n    private List<BookChapterBean> loadChapters() throws IOException {\n        List<BookChapterBean> mChapterList = new ArrayList<>();\n        //获取文件流\n        RandomAccessFile bookStream = new RandomAccessFile(mBookFile, \"r\");\n        //寻找匹配文章标题的正则表达式，判断是否存在章节名\n        boolean hasChapter = checkChapterType(bookStream);\n        //加载章节\n        byte[] buffer = new byte[BUFFER_SIZE];\n        //获取到的块起始点，在文件中的位置\n        long curOffset = 0;\n        //block的个数\n        int blockPos = 0;\n        //读取的长度\n        int length;\n        int allLength = 0;\n\n        //获取文件中的数据到buffer，直到没有数据为止\n        while ((length = bookStream.read(buffer, 0, buffer.length)) > 0) {\n            ++blockPos;\n            //如果存在Chapter\n            if (hasChapter) {\n                //将数据转换成String\n                String blockContent = new String(buffer, 0, length, mCharset);\n                int lastN = blockContent.lastIndexOf(\"\\n\");\n                if (lastN != 0) {\n                    blockContent = blockContent.substring(0, lastN);\n                    length = blockContent.getBytes(mCharset).length;\n                    allLength = allLength + length;\n                    bookStream.seek(allLength);\n                }\n                //当前Block下使过的String的指针\n                int seekPos = 0;\n                //进行正则匹配\n                Matcher matcher = mChapterPattern.matcher(blockContent);\n                //如果存在相应章节\n                while (matcher.find()) {\n                    //获取匹配到的字符在字符串中的起始位置\n                    int chapterStart = matcher.start();\n\n                    //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容\n                    //第一种情况一定是序章 第二种情况可能是上一个章节的内容\n                    if (seekPos == 0 && chapterStart != 0) {\n                        //获取当前章节的内容\n                        String chapterContent = blockContent.substring(seekPos, chapterStart);\n                        //设置指针偏移\n                        seekPos += chapterContent.length();\n\n                        if (mChapterList.size() == 0) { //如果当前没有章节，那么就是序章\n                            //加入简介\n                            book.getBookInfoBean().setIntroduce(chapterContent);\n\n                            //创建当前章节\n                            BookChapterBean curChapter = new BookChapterBean();\n                            curChapter.setDurChapterName(matcher.group());\n                            curChapter.setStart((long) chapterContent.getBytes(mCharset).length);\n                            mChapterList.add(curChapter);\n                        } else {  //否则就block分割之后，上一个章节的剩余内容\n                            //获取上一章节\n                            BookChapterBean lastChapter = mChapterList.get(mChapterList.size() - 1);\n                            //将当前段落添加上一章去\n                            lastChapter.setEnd(lastChapter.getEnd() + chapterContent.getBytes(mCharset).length);\n\n                            //创建当前章节\n                            BookChapterBean curChapter = new BookChapterBean();\n                            curChapter.setDurChapterName(matcher.group());\n                            curChapter.setStart(lastChapter.getEnd());\n                            mChapterList.add(curChapter);\n                        }\n                    } else {\n                        //是否存在章节\n                        if (mChapterList.size() != 0) {\n                            //获取章节内容\n                            String chapterContent = blockContent.substring(seekPos, matcher.start());\n                            seekPos += chapterContent.length();\n\n                            //获取上一章节\n                            BookChapterBean lastChapter = mChapterList.get(mChapterList.size() - 1);\n                            lastChapter.setEnd(lastChapter.getStart() + chapterContent.getBytes(mCharset).length);\n\n                            //创建当前章节\n                            BookChapterBean curChapter = new BookChapterBean();\n                            curChapter.setDurChapterName(matcher.group());\n                            curChapter.setStart(lastChapter.getEnd());\n                            mChapterList.add(curChapter);\n                        } else { //如果章节不存在则创建章节\n                            BookChapterBean curChapter = new BookChapterBean();\n                            curChapter.setDurChapterName(matcher.group());\n                            curChapter.setStart(0L);\n                            curChapter.setEnd(0L);\n                            mChapterList.add(curChapter);\n                        }\n                    }\n                }\n            } else { //进行本地虚拟分章\n                //章节在buffer的偏移量\n                int chapterOffset = 0;\n                //当前剩余可分配的长度\n                int strLength = length;\n                //分章的位置\n                int chapterPos = 0;\n\n                while (strLength > 0) {\n                    ++chapterPos;\n                    //是否长度超过一章\n                    if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) {\n                        //在buffer中一章的终止点\n                        int end = length;\n                        //寻找换行符作为终止点\n                        for (int i = chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER; i < length; ++i) {\n                            if (buffer[i] == BLANK) {\n                                end = i;\n                                break;\n                            }\n                        }\n                        BookChapterBean chapter = new BookChapterBean();\n                        chapter.setDurChapterName(\"第\" + blockPos + \"章\" + \"(\" + chapterPos + \")\");\n                        chapter.setStart(curOffset + chapterOffset + 1);\n                        chapter.setEnd(curOffset + end);\n                        mChapterList.add(chapter);\n                        //减去已经被分配的长度\n                        strLength = strLength - (end - chapterOffset);\n                        //设置偏移的位置\n                        chapterOffset = end;\n                    } else {\n                        BookChapterBean chapter = new BookChapterBean();\n                        chapter.setDurChapterName(\"第\" + blockPos + \"章\" + \"(\" + chapterPos + \")\");\n                        chapter.setStart(curOffset + chapterOffset + 1);\n                        chapter.setEnd(curOffset + length);\n                        mChapterList.add(chapter);\n                        strLength = 0;\n                    }\n                }\n            }\n\n            //block的偏移点\n            curOffset += length;\n\n            if (hasChapter) {\n                //设置上一章的结尾\n                BookChapterBean lastChapter = mChapterList.get(mChapterList.size() - 1);\n                lastChapter.setEnd(curOffset);\n            }\n\n            //当添加的block太多的时候，执行GC\n            if (blockPos % 15 == 0) {\n                System.gc();\n                System.runFinalization();\n            }\n        }\n\n        for (int i = 0; i < mChapterList.size(); i++) {\n            BookChapterBean bean = mChapterList.get(i);\n            bean.setDurChapterIndex(i);\n            bean.setNoteUrl(book.getNoteUrl());\n            bean.setDurChapterUrl(MD5Utils.strToMd5By16(mBookFile.getAbsolutePath() + i + bean.getDurChapterName()));\n        }\n        IOUtils.close(bookStream);\n\n        System.gc();\n        System.runFinalization();\n\n        return mChapterList;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/PageView.java",
    "content": "package com.kunfei.bookshelf.widget.page;\n\nimport static com.kunfei.bookshelf.utils.ScreenUtils.getDisplayMetrics;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.RectF;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\n\nimport com.kunfei.bookshelf.bean.BookShelfBean;\nimport com.kunfei.bookshelf.help.FileHelp;\nimport com.kunfei.bookshelf.help.ReadBookControl;\nimport com.kunfei.bookshelf.utils.ContextExtensionsKt;\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.view.activity.ReadBookActivity;\nimport com.kunfei.bookshelf.widget.page.animation.CoverPageAnim;\nimport com.kunfei.bookshelf.widget.page.animation.HorizonPageAnim;\nimport com.kunfei.bookshelf.widget.page.animation.NonePageAnim;\nimport com.kunfei.bookshelf.widget.page.animation.PageAnimation;\nimport com.kunfei.bookshelf.widget.page.animation.ScrollPageAnim;\nimport com.kunfei.bookshelf.widget.page.animation.SimulationPageAnim;\nimport com.kunfei.bookshelf.widget.page.animation.SlidePageAnim;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\n\n/**\n * 绘制页面显示内容的类\n */\npublic class PageView extends View implements PageAnimation.OnPageChangeListener {\n\n    private ReadBookActivity activity;\n\n    private int mViewWidth = 0; // 当前View的宽\n    private int mViewHeight = 0; // 当前View的高\n    private int statusBarHeight = 0; //状态栏高度\n\n    private boolean actionFromEdge = false;\n    // 初始化参数\n    private ReadBookControl readBookControl = ReadBookControl.getInstance();\n    private boolean isPrepare;\n    // 动画类\n    private PageAnimation mPageAnim;\n    //点击监听\n    private TouchListener mTouchListener;\n    //内容加载器\n    private PageLoader mPageLoader;\n\n    //文字选择画笔\n    private Paint mTextSelectPaint = null;\n    //文字选择画笔颜色\n    private int TextSelectColor = Color.parseColor(\"#77fadb08\");\n\n    private Path mSelectTextPath = new Path();\n\n    //触摸到起始位置\n    private int mStartX = 0;\n    private int mStartY = 0;\n    // 是否发触了长按事件\n    private boolean isLongPress = false;\n    //第一个选择的文字\n    private TxtChar firstSelectTxtChar = null;\n    //最后选择的一个文字\n    private TxtChar lastSelectTxtChar = null;\n    //选择模式\n    private SelectMode selectMode = SelectMode.Normal;\n    //文本高度\n    private float textHeight = 0;\n    // 唤醒菜单的区域\n    private RectF mCenterRect = null;\n    //是否在移动\n    private boolean isMove = false;\n    //长按的runnable\n    private Runnable mLongPressRunnable;\n    //长按时间\n    private static final int LONG_PRESS_TIMEOUT = 1000;\n    //选择的列\n    private List<TxtLine> mSelectLines = new ArrayList<TxtLine>();\n\n\n    public PageView(Context context) {\n        this(context, null);\n    }\n\n    public PageView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public PageView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init();\n    }\n\n    private void init() {\n        //初始化画笔\n        mTextSelectPaint = new Paint();\n        mTextSelectPaint.setAntiAlias(true);\n        mTextSelectPaint.setTextSize(19);\n        mTextSelectPaint.setColor(TextSelectColor);\n\n        mLongPressRunnable = () -> {\n            if (mPageLoader == null) return;\n            performLongClick();\n            if (mStartX > 0 && mStartY > 0) {// 说明还没释放，是长按事件\n                isLongPress = true;//长按\n                TxtChar p = mPageLoader.detectPressTxtChar(mStartX, mStartY);//找到长按的点\n                firstSelectTxtChar = p;//设置开始位置字符\n                lastSelectTxtChar = p;//设置结束位置字符\n                selectMode = SelectMode.PressSelectText;//设置模式为长按选择\n                mTouchListener.onLongPress();//响应长按事件，供上层调用\n            }\n        };\n    }\n\n    @Override\n    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {\n        super.onSizeChanged(width, height, oldWidth, oldHeight);\n        mViewWidth = width;\n        mViewHeight = height;\n\n        isPrepare = true;\n\n        if (mPageLoader != null) {\n            mPageLoader.prepareDisplay(width, height);\n        }\n        //设置中间区域范围\n        mCenterRect = new RectF(mViewWidth / 3f, mViewHeight / 3f,\n                mViewWidth * 2f / 3, mViewHeight * 2f / 3);\n    }\n\n    //设置翻页的模式\n    void setPageMode(PageAnimation.Mode pageMode, int marginTop, int marginBottom) {\n        //视图未初始化的时候，禁止调用\n        if (mViewWidth == 0 || mViewHeight == 0 || mPageLoader == null) return;\n        if (!readBookControl.getHideStatusBar()) {\n            marginTop = marginTop + statusBarHeight;\n        }\n        switch (pageMode) {\n            case COVER:\n                mPageAnim = new CoverPageAnim(mViewWidth, mViewHeight, this, this);\n                break;\n            case SLIDE:\n                mPageAnim = new SlidePageAnim(mViewWidth, mViewHeight, this, this);\n                break;\n            case NONE:\n                mPageAnim = new NonePageAnim(mViewWidth, mViewHeight, this, this);\n                break;\n            case SCROLL:\n                mPageAnim = new ScrollPageAnim(mViewWidth, mViewHeight, 0,\n                        marginTop, marginBottom, this, this);\n                break;\n            default:\n                mPageAnim = new SimulationPageAnim(mViewWidth, mViewHeight, this, this);\n        }\n    }\n\n    public ReadBookActivity getActivity() {\n        return activity;\n    }\n\n    public int getStatusBarHeight() {\n        return statusBarHeight;\n    }\n\n    public Bitmap getBgBitmap(int pageOnCur) {\n        if (mPageAnim == null) return null;\n        return mPageAnim.getBgBitmap(pageOnCur);\n    }\n\n    public void autoPrevPage() {\n        if (mPageAnim instanceof ScrollPageAnim) {\n            ((ScrollPageAnim) mPageAnim).startAnim(PageAnimation.Direction.PREV);\n        } else {\n            startHorizonPageAnim(PageAnimation.Direction.PREV);\n        }\n    }\n\n    public void autoNextPage() {\n        if (mPageAnim instanceof ScrollPageAnim) {\n            ((ScrollPageAnim) mPageAnim).startAnim(PageAnimation.Direction.NEXT);\n        } else {\n            startHorizonPageAnim(PageAnimation.Direction.NEXT);\n        }\n    }\n\n    private synchronized void startHorizonPageAnim(PageAnimation.Direction direction) {\n        if (mTouchListener == null) return;\n        //结束动画\n        mPageAnim.abortAnim();\n        if (direction == PageAnimation.Direction.NEXT) {\n            int x = mViewWidth;\n            int y = mViewHeight;\n            //初始化动画\n            mPageAnim.setStartPoint(x, y);\n            //设置点击点\n            mPageAnim.setTouchPoint(x, y);\n            //设置方向\n            boolean hasNext = hasNextPage(0);\n\n            mPageAnim.setDirection(direction);\n            if (!hasNext) {\n                ((HorizonPageAnim) mPageAnim).setNoNext(true);\n                return;\n            }\n        } else if (direction == PageAnimation.Direction.PREV) {\n            int x = 0;\n            int y = mViewHeight;\n            //初始化动画\n            mPageAnim.setStartPoint(x, y);\n            //设置点击点\n            mPageAnim.setTouchPoint(x, y);\n            mPageAnim.setDirection(direction);\n            //设置方向方向\n            boolean hashPrev = hasPrevPage();\n            if (!hashPrev) {\n                ((HorizonPageAnim) mPageAnim).setNoNext(true);\n                return;\n            }\n        } else {\n            return;\n        }\n        ((HorizonPageAnim) mPageAnim).setNoNext(false);\n        ((HorizonPageAnim) mPageAnim).setCancel(false);\n        mPageAnim.startAnim();\n    }\n\n    public void drawPage(int pageOnCur) {\n        if (!isPrepare) return;\n        if (mPageLoader != null) {\n            mPageLoader.drawPage(getBgBitmap(pageOnCur), pageOnCur);\n        }\n        invalidate();\n    }\n\n    /**\n     * 绘制滚动背景\n     */\n    @Override\n    public void drawBackground(Canvas canvas) {\n        if (!isPrepare) return;\n        if (mPageLoader != null) {\n            mPageLoader.drawBackground(canvas);\n        }\n    }\n\n    /**\n     * 绘制滚动内容\n     */\n    @Override\n    public void drawContent(Canvas canvas, float offset) {\n        if (!isPrepare) return;\n        if (mPageLoader != null) {\n            mPageLoader.drawContent(canvas, offset);\n        }\n    }\n\n    /**\n     * 绘制横翻背景\n     */\n    public void drawBackground(int pageOnCur) {\n        if (!isPrepare) return;\n        if (mPageLoader != null) {\n            mPageLoader.drawPage(getBgBitmap(pageOnCur), pageOnCur);\n        }\n        invalidate();\n    }\n\n    /**\n     * 绘制横翻内容\n     *\n     * @param pageOnCur 相对当前页的位置\n     */\n    public void drawContent(int pageOnCur) {\n        if (!isPrepare) return;\n        if (mPageLoader != null) {\n            mPageLoader.drawPage(getBgBitmap(pageOnCur), pageOnCur);\n        }\n        invalidate();\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        if (mPageAnim instanceof ScrollPageAnim)\n            super.onDraw(canvas);\n        //绘制动画\n        if (mPageAnim != null) {\n            mPageAnim.draw(canvas);\n        }\n        if (selectMode != SelectMode.Normal && !isRunning() && !isMove) {\n            DrawSelectText(canvas);\n        }\n    }\n\n    private void DrawSelectText(Canvas canvas) {\n        if (selectMode == SelectMode.PressSelectText) {\n            drawPressSelectText(canvas);\n        } else if (selectMode == SelectMode.SelectMoveForward) {\n            drawMoveSelectText(canvas);\n        } else if (selectMode == SelectMode.SelectMoveBack) {\n            drawMoveSelectText(canvas);\n        }\n    }\n\n\n    private void drawPressSelectText(Canvas canvas) {\n        if (lastSelectTxtChar != null) {// 找到了选择的字符\n            mSelectTextPath.reset();\n            mSelectTextPath.moveTo(firstSelectTxtChar.getTopLeftPosition().x, firstSelectTxtChar.getTopLeftPosition().y);\n            mSelectTextPath.lineTo(firstSelectTxtChar.getTopRightPosition().x, firstSelectTxtChar.getTopRightPosition().y);\n            mSelectTextPath.lineTo(firstSelectTxtChar.getBottomRightPosition().x, firstSelectTxtChar.getBottomRightPosition().y);\n            mSelectTextPath.lineTo(firstSelectTxtChar.getBottomLeftPosition().x, firstSelectTxtChar.getBottomLeftPosition().y);\n            canvas.drawPath(mSelectTextPath, mTextSelectPaint);\n        }\n    }\n\n\n    public String getSelectStr() {\n\n        if (mSelectLines.size() == 0) {\n            return String.valueOf(firstSelectTxtChar.getChardata());\n        }\n        StringBuilder sb = new StringBuilder();\n        for (TxtLine l : mSelectLines) {\n            //Log.e(\"selectline\", l.getLineData() + \"\");\n            sb.append(l.getLineData());\n        }\n\n        return sb.toString();\n    }\n\n\n    private void drawMoveSelectText(Canvas canvas) {\n        if (firstSelectTxtChar == null || lastSelectTxtChar == null)\n            return;\n        getSelectData();\n        drawSelectLines(canvas);\n    }\n\n    List<TxtLine> mLinseData = null;\n\n    private void getSelectData() {\n        TxtPage txtPage = mPageLoader.curChapter().txtChapter.getPage(mPageLoader.getCurPagePos());\n        if (txtPage != null) {\n            mLinseData = txtPage.getTxtLists();\n\n            Boolean Started = false;\n            Boolean Ended = false;\n\n            mSelectLines.clear();\n\n            // 找到选择的字符数据，转化为选择的行，然后将行选择背景画出来\n            for (TxtLine l : mLinseData) {\n\n                TxtLine selectline = new TxtLine();\n                selectline.setCharsData(new ArrayList<>());\n\n                for (TxtChar c : l.getCharsData()) {\n                    if (!Started) {\n                        if (c.getIndex() == firstSelectTxtChar.getIndex()) {\n                            Started = true;\n                            selectline.getCharsData().add(c);\n                            if (c.getIndex() == lastSelectTxtChar.getIndex()) {\n                                Ended = true;\n                                break;\n                            }\n                        }\n                    } else {\n                        if (c.getIndex() == lastSelectTxtChar.getIndex()) {\n                            Ended = true;\n                            if (!selectline.getCharsData().contains(c)) {\n                                selectline.getCharsData().add(c);\n                            }\n                            break;\n                        } else {\n                            selectline.getCharsData().add(c);\n                        }\n                    }\n                }\n\n                mSelectLines.add(selectline);\n\n                if (Started && Ended) {\n                    break;\n                }\n            }\n        }\n    }\n\n    public SelectMode getSelectMode() {\n        return selectMode;\n    }\n\n    public void setSelectMode(SelectMode mCurrentMode) {\n        this.selectMode = mCurrentMode;\n    }\n\n    private void drawSelectLines(Canvas canvas) {\n        drawOaleSeletLinesBg(canvas);\n    }\n\n    public void clearSelect() {\n        firstSelectTxtChar = null;\n        lastSelectTxtChar = null;\n        selectMode = SelectMode.Normal;\n        mSelectTextPath.reset();\n        invalidate();\n\n    }\n\n\n    //根据当前坐标返回文字\n    public TxtChar getCurrentTxtChar(float x, float y) {\n        return mPageLoader.detectPressTxtChar(x, y);\n    }\n\n    private void drawOaleSeletLinesBg(Canvas canvas) {// 绘制椭圆型的选中背景\n        for (TxtLine l : mSelectLines) {\n            if (l.getCharsData() != null && l.getCharsData().size() > 0) {\n\n                TxtChar fistchar = l.getCharsData().get(0);\n                TxtChar lastchar = l.getCharsData().get(l.getCharsData().size() - 1);\n\n                float fw = fistchar.getCharWidth();\n                float lw = lastchar.getCharWidth();\n\n                RectF rect = new RectF(fistchar.getTopLeftPosition().x, fistchar.getTopLeftPosition().y,\n                        lastchar.getTopRightPosition().x, lastchar.getBottomRightPosition().y);\n\n                canvas.drawRoundRect(rect, fw / 4,\n                        textHeight /4, mTextSelectPaint);\n            }\n        }\n    }\n\n    public TxtChar getFirstSelectTxtChar() {\n        return firstSelectTxtChar;\n    }\n\n    public void setFirstSelectTxtChar(TxtChar firstSelectTxtChar) {\n        this.firstSelectTxtChar = firstSelectTxtChar;\n    }\n\n    public TxtChar getLastSelectTxtChar() {\n        return lastSelectTxtChar;\n    }\n\n    public void setLastSelectTxtChar(TxtChar lastSelectTxtChar) {\n        this.lastSelectTxtChar = lastSelectTxtChar;\n    }\n\n    @Override\n    public void computeScroll() {\n        //进行滑动\n        if (mPageAnim != null) {\n            mPageAnim.scrollAnim();\n        }\n        super.computeScroll();\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        super.onTouchEvent(event);\n        if (mPageAnim == null) return true;\n        if (mPageLoader == null) return true;\n\n        Paint.FontMetrics fontMetrics = mPageLoader.mTextPaint.getFontMetrics();\n        textHeight = Math.abs(fontMetrics.ascent) + Math.abs(fontMetrics.descent);\n\n        if (actionFromEdge) {\n            if (event.getAction() == MotionEvent.ACTION_UP)\n                actionFromEdge = false;\n            return true;\n        }\n\n        int x = (int) event.getX();\n        int y = (int) event.getY();\n\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                mPageAnim.initTouch(x, y);\n                if (event.getEdgeFlags() != 0 || event.getRawY() < ScreenUtils.dpToPx(5) || event.getRawY() > getDisplayMetrics().heightPixels - ScreenUtils.dpToPx(5)) {\n                    actionFromEdge = true;\n                    return true;\n                }\n                mStartX = x;\n                mStartY = y;\n                isMove = false;\n\n                //\n                if (readBookControl.isCanSelectText() && mPageLoader.getPageStatus() == TxtChapter.Status.FINISH) {\n                    postDelayed(mLongPressRunnable, LONG_PRESS_TIMEOUT);\n                }\n\n                //\n                isLongPress = false;\n                mTouchListener.onTouch();\n                mPageAnim.onTouchEvent(event);\n\n                selectMode = SelectMode.Normal;\n\n                mTouchListener.onTouchClearCursor();\n\n                break;\n            case MotionEvent.ACTION_MOVE:\n                mPageAnim.initTouch(x, y);\n                // 判断是否大于最小滑动值。\n                int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();\n                if (!isMove) {\n                    isMove = Math.abs(mStartX - event.getX()) > slop || Math.abs(mStartY - event.getY()) > slop;\n                }\n\n                // 如果滑动了,且不是长按，则进行翻页。\n                if (isMove) {\n                    if (readBookControl.isCanSelectText()) {\n                        removeCallbacks(mLongPressRunnable);\n                    }\n                    mPageAnim.onTouchEvent(event);\n                }\n                break;\n            case MotionEvent.ACTION_CANCEL:\n            case MotionEvent.ACTION_UP:\n                mPageAnim.initTouch(x, y);\n                mPageAnim.setTouchInitFalse();\n                if (!isMove) {\n                    if (readBookControl.isCanSelectText()) {\n                        removeCallbacks(mLongPressRunnable);\n                    }\n\n                    //是否点击了中间\n                    if (mCenterRect.contains(x, y)) {\n                        if (firstSelectTxtChar == null) {\n                            if (mTouchListener != null) {\n                                mTouchListener.center();\n                            }\n                        } else {\n                            if (mSelectTextPath != null) {//长安选择删除选中状态\n                                if (!isLongPress) {\n                                    firstSelectTxtChar = null;\n                                    mSelectTextPath.reset();\n                                    invalidate();\n                                }\n                            }\n                            //清除移动选择状态\n                        }\n                        return true;\n                    }\n\n                    if (!readBookControl.getCanClickTurn()) {\n                        return true;\n                    }\n\n                    if (mPageAnim instanceof ScrollPageAnim && readBookControl.disableScrollClickTurn()) {\n                        return true;\n                    }\n                }\n\n                if (firstSelectTxtChar == null || isMove) {//长安选择删除选中状态\n                    mPageAnim.onTouchEvent(event);\n                } else {\n                    if (!isLongPress) {\n                        //释放了\n                        if (LONG_PRESS_TIMEOUT != 0) {\n                            removeCallbacks(mLongPressRunnable);\n                        }\n                        firstSelectTxtChar = null;\n                        mSelectTextPath.reset();\n                        invalidate();\n                    }\n                }\n                break;\n        }\n        return true;\n    }\n\n    /**\n     * 判断是否存在上一页\n     */\n    private boolean hasPrevPage() {\n        if (mPageLoader.hasPrev()) {\n            return true;\n        } else {\n            showSnackBar(\"没有上一页\");\n            return false;\n        }\n    }\n\n    /**\n     * 判断是否下一页存在\n     */\n    private boolean hasNextPage(int pageOnCur) {\n        if (mPageLoader.hasNext(pageOnCur)) {\n            return true;\n        } else {\n            showSnackBar(\"没有下一页\");\n            return false;\n        }\n    }\n\n    public boolean isRunning() {\n        return mPageAnim != null && mPageAnim.isRunning();\n    }\n\n    public boolean isPrepare() {\n        return isPrepare;\n    }\n\n    public void setTouchListener(TouchListener mTouchListener) {\n        this.mTouchListener = mTouchListener;\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n        if (mPageAnim != null) {\n            mPageAnim.abortAnim();\n            mPageAnim.clear();\n        }\n\n        mPageLoader = null;\n        mPageAnim = null;\n    }\n\n    @Override\n    public void resetScroll() {\n        mPageLoader.resetPageOffset();\n    }\n\n    @Override\n    public boolean hasPrev() {\n        return PageView.this.hasPrevPage();\n    }\n\n    @Override\n    public boolean hasNext(int pageOnCur) {\n        return PageView.this.hasNextPage(pageOnCur);\n    }\n\n    @Override\n    public void changePage(PageAnimation.Direction direction) {\n        mPageLoader.pagingEnd(direction);\n    }\n\n    /**\n     * 获取 PageLoader\n     */\n    public PageLoader getPageLoader(ReadBookActivity activity, BookShelfBean bookShelfBean, PageLoader.Callback callback) {\n        this.activity = activity;\n        this.statusBarHeight = ContextExtensionsKt.getStatusBarHeight(activity);\n        // 判是否已经存在\n        if (mPageLoader != null) {\n            return mPageLoader;\n        }\n        // 根据书籍类型，获取具体的加载器\n        if (!Objects.equals(bookShelfBean.getTag(), BookShelfBean.LOCAL_TAG)) {\n            mPageLoader = new PageLoaderNet(this, bookShelfBean, callback);\n        } else {\n            String fileSuffix = FileHelp.getFileSuffix(bookShelfBean.getNoteUrl());\n            if (fileSuffix.equalsIgnoreCase(FileHelp.SUFFIX_EPUB)) {\n                mPageLoader = new PageLoaderEpub(this, bookShelfBean, callback);\n            } else {\n                mPageLoader = new PageLoaderText(this, bookShelfBean, callback);\n            }\n        }\n        // 判断是否 PageView 已经初始化完成\n        if (mViewWidth != 0 || mViewHeight != 0) {\n            // 初始化 PageLoader 的屏幕大小\n            mPageLoader.prepareDisplay(mViewWidth, mViewHeight);\n        }\n\n        return mPageLoader;\n    }\n\n    public void autoChangeSource() {\n        mPageLoader.setStatus(TxtChapter.Status.CHANGE_SOURCE);\n        activity.autoChangeSource();\n    }\n\n    public void showSnackBar(String msg) {\n        activity.showSnackBar(this, msg);\n    }\n\n    public enum SelectMode {\n        Normal, PressSelectText, SelectMoveForward, SelectMoveBack\n    }\n\n    public interface TouchListener {\n        void onTouch();\n\n        void onTouchClearCursor();\n\n        void onLongPress();\n\n        void center();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/TxtChapter.kt",
    "content": "package com.kunfei.bookshelf.widget.page\n\nimport java.util.*\nimport kotlin.math.max\nimport kotlin.math.min\n\n/**\n * 章节\n */\n\nclass TxtChapter internal constructor(val position: Int) {\n    val txtPageList = ArrayList<TxtPage>()\n    val txtPageLengthList = ArrayList<Int>()\n    val paragraphLengthList = ArrayList<Int>()\n    var status = Status.LOADING\n    var msg: String? = null\n\n    val pageSize: Int\n        get() = txtPageList.size\n\n    fun addPage(txtPage: TxtPage) {\n        txtPageList.add(txtPage)\n    }\n\n    fun getPage(page: Int): TxtPage? {\n        return if (txtPageList.isNotEmpty()) {\n            txtPageList[max(0, min(page, txtPageList.size - 1))]\n        } else null\n    }\n\n    fun getPageLength(position: Int): Int {\n        return if (position >= 0 && position < txtPageLengthList.size) {\n            txtPageLengthList[position]\n        } else -1\n    }\n\n    fun addTxtPageLength(length: Int) {\n        txtPageLengthList.add(length)\n    }\n\n    fun addParagraphLength(length: Int) {\n        paragraphLengthList.add(length)\n    }\n\n    fun getParagraphIndex(length: Int): Int {\n        for (i in paragraphLengthList.indices) {\n            if ((i == 0 || paragraphLengthList[i - 1] < length) && length <= paragraphLengthList[i]) {\n                return i\n            }\n        }\n        return -1\n    }\n\n    enum class Status {\n        LOADING, FINISH, ERROR, EMPTY, CATEGORY_EMPTY, CHANGE_SOURCE\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/TxtChar.kt",
    "content": "package com.kunfei.bookshelf.widget.page\n\nimport android.graphics.Point\n\n\nclass TxtChar {\n    var chardata: Char = ' '//字符数据\n\n    var selected: Boolean? = false//当前字符是否被选中\n\n    //记录文字的左上右上左下右下四个点坐标\n    var topLeftPosition: Point? = null//左上\n    var topRightPosition: Point? = null//右上\n    var bottomLeftPosition: Point? = null//左下\n    var bottomRightPosition: Point? = null//右下\n\n    var charWidth = 0f//字符宽度\n    var Index = 0//当前字符位置\n\n    override fun toString(): String {\n        return (\"ShowChar [chardata=\" + chardata + \", Selected=\" + selected + \", TopLeftPosition=\" + topLeftPosition\n                + \", TopRightPosition=\" + topRightPosition + \", BottomLeftPosition=\" + bottomLeftPosition\n                + \", BottomRightPosition=\" + bottomRightPosition + \", charWidth=\" + charWidth + \", Index=\" + Index\n                + \"]\");\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/TxtLine.kt",
    "content": "package com.kunfei.bookshelf.widget.page\n\nclass TxtLine {\n\n    var charsData: List<TxtChar>? = null\n\n    fun getLineData(): String {\n        var linedata = \"\"\n        if (charsData == null) return linedata\n        charsData?.let {\n            if (it.isEmpty()) return linedata\n            for (c in it) {\n                linedata += c.chardata\n            }\n        }\n        return linedata\n    }\n\n    override fun toString(): String {\n        return \"ShowLine [Linedata=\" + getLineData() + \"]\"\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/TxtPage.kt",
    "content": "package com.kunfei.bookshelf.widget.page\n\nimport java.util.*\n\n/**\n * 页面\n */\n\nclass TxtPage(val position: Int) {\n    var title: String? = null\n    var titleLines: Int = 0 //当前 lines 中为 title 的行数。\n    private val lines = ArrayList<String>()\n    //存放每个字的位置\n    var txtLists: List<TxtLine>? = null\n\n    val content: String\n        get() {\n            val s = StringBuilder()\n            for (i in lines.indices) {\n                s.append(lines[i])\n            }\n            return s.toString()\n        }\n\n    fun addLine(line: String) {\n        lines.add(line)\n    }\n\n    fun addLines(lines: List<String>) {\n        this.lines.addAll(lines)\n    }\n\n    fun getLine(i: Int): String {\n        return lines[i]\n    }\n\n    fun getLines(): List<String> {\n        return lines\n    }\n\n    fun size(): Int {\n        return lines.size\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/animation/CoverPageAnim.java",
    "content": "package com.kunfei.bookshelf.widget.page.animation;\n\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.graphics.drawable.GradientDrawable;\nimport android.view.View;\n\n/**\n * 覆盖翻页\n */\n\npublic class CoverPageAnim extends HorizonPageAnim {\n\n    private Rect mSrcRect, mDestRect;\n    private GradientDrawable mBackShadowDrawableLR;\n\n    public CoverPageAnim(int w, int h, View view, OnPageChangeListener listener) {\n        super(w, h, view, listener);\n        mSrcRect = new Rect(0, 0, mViewWidth, mViewHeight);\n        mDestRect = new Rect(0, 0, mViewWidth, mViewHeight);\n        int[] mBackShadowColors = new int[]{0x66000000, 0x00000000};\n        mBackShadowDrawableLR = new GradientDrawable(\n                GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors);\n        mBackShadowDrawableLR.setGradientType(GradientDrawable.LINEAR_GRADIENT);\n    }\n\n    @Override\n    public void drawMove(Canvas canvas) {\n        int dis;\n        if (mDirection == Direction.NEXT) {\n            dis = (int) (mViewWidth - mStartX + mTouchX);\n            if (dis > mViewWidth) {\n                dis = mViewWidth;\n            }\n            //计算bitmap截取的区域\n            mSrcRect.left = mViewWidth - dis;\n            //计算bitmap在canvas显示的区域\n            mDestRect.right = dis;\n            canvas.drawBitmap(bitmapList.get(2), 0, 0, null);\n            canvas.drawBitmap(bitmapList.get(1), mSrcRect, mDestRect, null);\n            addShadow(dis, canvas);\n        } else {\n            dis = (int) (mTouchX - mStartX);\n            if (dis > mViewWidth) {\n                dis = mViewWidth;\n            }\n            mSrcRect.left = mViewWidth - dis;\n            mDestRect.right = dis;\n            canvas.drawBitmap(bitmapList.get(1), 0, 0, null);\n            canvas.drawBitmap(bitmapList.get(0), mSrcRect, mDestRect, null);\n            addShadow(dis, canvas);\n        }\n    }\n\n    //添加阴影\n    private void addShadow(int left, Canvas canvas) {\n        mBackShadowDrawableLR.setBounds(left, 0, left + 30, mScreenHeight);\n        mBackShadowDrawableLR.draw(canvas);\n    }\n\n    @Override\n    public void startAnim() {\n        int dx;\n        if (mDirection == Direction.NEXT) {\n            if (isCancel) {\n                int dis = (int) ((mViewWidth - mStartX) + mTouchX);\n                if (dis > mViewWidth) {\n                    dis = mViewWidth;\n                }\n                dx = mViewWidth - dis;\n            } else {\n                dx = (int) -(mTouchX + (mViewWidth - mStartX));\n            }\n        } else {\n            if (isCancel) {\n                dx = (int) -(mTouchX - mStartX);\n            } else {\n                dx = (int) (mViewWidth - (mTouchX - mStartX));\n            }\n        }\n\n        //滑动速度保持一致\n        int duration = (animationSpeed * Math.abs(dx)) / mViewWidth;\n        mScroller.startScroll((int) mTouchX, 0, dx, 0, duration);\n        super.startAnim();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/animation/HorizonPageAnim.java",
    "content": "package com.kunfei.bookshelf.widget.page.animation;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\n\n/**\n * 横向动画的模板\n */\n\npublic abstract class HorizonPageAnim extends PageAnimation {\n    private static final String TAG = \"HorizonPageAnim\";\n    List<Bitmap> bitmapList = new ArrayList<>();\n\n    HorizonPageAnim(int w, int h, View view, OnPageChangeListener listener) {\n        super(w, h, view, listener);\n        //创建图片\n        for (int i = 0; i < 3; i++) {\n            bitmapList.add(Bitmap.createBitmap(mViewWidth, mViewHeight, Bitmap.Config.ARGB_8888));\n        }\n    }\n\n    /**\n     * 转换页面，在显示下一章的时候，必须首先调用此方法\n     */\n    @Override\n    public boolean changePage() {\n        if (isCancel) return false;\n        switch (mDirection) {\n            case NEXT:\n                Collections.swap(bitmapList, 0, 1);\n                Collections.swap(bitmapList, 1, 2);\n                break;\n            case PREV:\n                Collections.swap(bitmapList, 1, 2);\n                Collections.swap(bitmapList, 0, 1);\n                break;\n            default:\n                return false;\n        }\n        return true;\n    }\n\n    public abstract void drawMove(Canvas canvas);\n\n    @Override\n    public void onTouchEvent(MotionEvent event) {\n        abortAnim();\n        final int slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();\n        //获取点击位置\n        int x = (int) event.getX();\n        int y = (int) event.getY();\n        //设置触摸点\n        setTouchPoint(x, y);\n\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                break;\n            case MotionEvent.ACTION_MOVE:\n                //判断是否移动了\n                if (!isMove) {\n                    isMove = Math.abs(mStartX - x) > slop || Math.abs(mStartY - y) > slop;\n                }\n\n                if (isMove) {\n                    //判断是否是准备移动的状态(将要移动但是还没有移动)\n                    if (mMoveX == 0 && mMoveY == 0) {\n                        //判断翻得是上一页还是下一页\n                        if (x - mStartX > 0) {\n                            //上一页的参数配置\n                            isNext = false;\n                            boolean hasPrev = mListener.hasPrev();\n                            setDirection(Direction.PREV);\n                            //如果上一页不存在\n                            if (!hasPrev) {\n                                noNext = true;\n                                return;\n                            }\n                        } else {\n                            //进行下一页的配置\n                            isNext = true;\n                            //判断是否下一页存在\n                            boolean hasNext = mListener.hasNext(0);\n                            //如果存在设置动画方向\n                            setDirection(Direction.NEXT);\n\n                            //如果不存在表示没有下一页了\n                            if (!hasNext) {\n                                noNext = true;\n                                return;\n                            }\n                        }\n                    } else {\n                        //判断是否取消翻页\n                        isCancel = isNext ? x - mMoveX > 0 : x - mMoveX < 0;\n                    }\n\n                    mMoveX = x;\n                    mMoveY = y;\n                    isRunning = true;\n                    mView.invalidate();\n                }\n                break;\n            case MotionEvent.ACTION_CANCEL:\n            case MotionEvent.ACTION_UP:\n                isRunning = false;\n                if (!isMove) {\n\n                    if (!readBookControl.getCanClickTurn()) {\n                        return;\n                    }\n\n                    isNext = x > mScreenWidth / 2 || readBookControl.getClickAllNext();\n\n                    if (isNext) {\n                        //判断是否下一页存在\n                        boolean hasNext = mListener.hasNext(0);\n                        //设置动画方向\n                        if (!hasNext) {\n                            return;\n                        }\n                        setDirection(Direction.NEXT);\n                    } else {\n                        boolean hasPrev = mListener.hasPrev();\n                        if (!hasPrev) {\n                            return;\n                        }\n                        setDirection(Direction.PREV);\n                    }\n                } else {\n                    isCancel = Math.abs(mLastX - mStartX) < slop * 3 || isCancel;\n                }\n\n                // 开启翻页效果\n                if (!noNext) {\n                    startAnim();\n                }\n                mView.invalidate();\n                break;\n        }\n    }\n\n    @Override\n    public void draw(Canvas canvas) {\n        if (isRunning && !noNext) {\n            drawMove(canvas);\n        } else {\n            canvas.drawBitmap(getBgBitmap(0), 0, 0, null);\n            isCancel = true;\n        }\n    }\n\n    @Override\n    public void abortAnim() {\n        if (!mScroller.isFinished()) {\n            mScroller.abortAnimation();\n            if (changePage()) {\n                mListener.changePage(mDirection);\n                setDirection(PageAnimation.Direction.NONE);\n            }\n            movingFinish();\n            setTouchPoint(mScroller.getFinalX(), mScroller.getFinalY());\n            mView.invalidate();\n        }\n    }\n\n    @Override\n    public Bitmap getBgBitmap(int pageOnCur) {\n        if (pageOnCur < 0) {\n            return bitmapList.get(0);\n        } else if (pageOnCur > 0) {\n            return bitmapList.get(2);\n        }\n        return bitmapList.get(1);\n    }\n\n    public void setCancel(boolean cancel) {\n        isCancel = cancel;\n    }\n\n    public void setNoNext(boolean noNext) {\n        this.noNext = noNext;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/animation/NonePageAnim.java",
    "content": "package com.kunfei.bookshelf.widget.page.animation;\n\nimport android.graphics.Canvas;\nimport android.view.View;\n\n/**\n * 无动画翻页\n */\n\npublic class NonePageAnim extends HorizonPageAnim {\n\n    public NonePageAnim(int w, int h, View view, OnPageChangeListener listener) {\n        super(w, h, view, listener);\n    }\n\n    @Override\n    public void drawMove(Canvas canvas) {\n        canvas.drawBitmap(bitmapList.get(1), 0, 0, null);\n    }\n\n    @Override\n    public void startAnim() {\n        super.startAnim();\n        isRunning = false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/animation/PageAnimation.java",
    "content": "package com.kunfei.bookshelf.widget.page.animation;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.animation.LinearInterpolator;\nimport android.widget.Scroller;\n\nimport androidx.annotation.NonNull;\n\nimport com.kunfei.bookshelf.MApplication;\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.help.ReadBookControl;\n\n/**\n * 翻页动画抽象类\n */\n\npublic abstract class PageAnimation {\n    //动画速度\n    static final int animationSpeed = 300;\n    //正在使用的View\n    protected View mView;\n    protected ReadBookControl readBookControl = ReadBookControl.getInstance();\n    //滑动装置\n    Scroller mScroller;\n    //监听器\n    protected OnPageChangeListener mListener;\n    //移动方向\n    Direction mDirection = Direction.NONE;\n\n    //屏幕的尺寸\n    int mScreenWidth;\n    int mScreenHeight;\n    int mMarginTop;\n    //视图的尺寸\n    int mViewWidth;\n    int mViewHeight;\n    //起始点\n    float mStartX;\n    float mStartY;\n    //触碰点\n    float mTouchX;\n    float mTouchY;\n    //上一个触碰点\n    float mLastX;\n    float mLastY;\n    private boolean isMoving = false;\n    boolean isRunning = false;\n    private boolean touchInit = false;\n    //是否取消翻页\n    boolean isCancel = false;\n    //可以使用 mLast代替\n    int mMoveX = 0;\n    int mMoveY = 0;\n    //是否移动了\n    boolean isMove = false;\n    //是否翻阅下一页。true表示翻到下一页，false表示上一页。\n    boolean isNext = false;\n    //是否没下一页或者上一页\n    boolean noNext = false;\n\n    PageAnimation(int w, int h, View view, OnPageChangeListener listener) {\n        this(w, h, 0, 0, 0, view, listener);\n    }\n\n    PageAnimation(int w, int h, int marginWidth, int marginTop, int marginBottom, View view, OnPageChangeListener listener) {\n        mScreenWidth = w;\n        mScreenHeight = h;\n\n        //屏幕的间距\n        mMarginTop = marginTop;\n\n        mViewWidth = mScreenWidth - marginWidth * 2;\n        mViewHeight = mScreenHeight - mMarginTop - marginBottom;\n\n        mView = view;\n        mListener = listener;\n\n        mScroller = new Scroller(mView.getContext(), new LinearInterpolator());\n    }\n\n    public Scroller getScroller() {\n        return mScroller;\n    }\n\n    public void setStartPoint(float x, float y) {\n        mStartX = x;\n        mStartY = y;\n\n        mLastX = mStartX;\n        mLastY = mStartY;\n    }\n\n    public void setTouchPoint(float x, float y) {\n        mLastX = mTouchX;\n        mLastY = mTouchY;\n\n        mTouchX = x;\n        mTouchY = y;\n    }\n\n    public void setTouchInitFalse() {\n        touchInit = false;\n    }\n\n    public void initTouch(int x, int y) {\n        if (!touchInit) {\n            //移动的点击位置\n            mMoveX = 0;\n            mMoveY = 0;\n            //是否移动\n            isMove = false;\n            //是否存在下一章\n            noNext = false;\n            //是下一章还是前一章\n            isNext = false;\n            //是否正在执行动画\n            isRunning = false;\n            //取消\n            isCancel = false;\n            //设置起始位置的触摸点\n            setStartPoint(x, y);\n            touchInit = true;\n        }\n    }\n\n    public boolean isRunning() {\n        return isRunning;\n    }\n\n    void movingFinish() {\n        isMoving = false;\n        isRunning = false;\n    }\n\n    /**\n     * 开启翻页动画\n     */\n    public void startAnim() {\n        isRunning = true;\n        isMoving = true;\n        mView.invalidate();\n    }\n\n    public void setDirection(Direction direction) {\n        mDirection = direction;\n    }\n\n    public void clear() {\n        mView = null;\n    }\n\n    /**\n     * 点击事件的处理\n     */\n    public abstract void onTouchEvent(MotionEvent event);\n\n    /**\n     * 绘制图形\n     */\n    public abstract void draw(Canvas canvas);\n\n    /**\n     * 滚动动画\n     * 必须放在computeScroll()方法中执行\n     */\n    public void scrollAnim() {\n        if (mScroller.computeScrollOffset()) {\n            int x = mScroller.getCurrX();\n            int y = mScroller.getCurrY();\n\n            setTouchPoint(x, y);\n\n            mView.postInvalidate();\n        } else if (isMoving) {\n            if (changePage()) {\n                mListener.changePage(mDirection);\n                setDirection(PageAnimation.Direction.NONE);\n            }\n            movingFinish();\n        }\n    }\n\n    /**\n     * 取消动画\n     */\n    public abstract void abortAnim();\n\n    public abstract boolean changePage();\n\n    /**\n     * 获取背景板\n     * pageOnCur: 位于当前页的位置, 小于0上一页, 0 当前页, 大于0下一页\n     */\n    public abstract Bitmap getBgBitmap(int pageOnCur);\n\n    /**\n     * 翻页模式\n     */\n    public enum Mode {\n        COVER(MApplication.getAppResources().getString(R.string.page_mode_COVER)),\n        SIMULATION(MApplication.getAppResources().getString(R.string.page_mode_SIMULATION)),\n        SLIDE(MApplication.getAppResources().getString(R.string.page_mode_SLIDE)),\n        SCROLL(MApplication.getAppResources().getString(R.string.page_mode_SCROLL)),\n        NONE(MApplication.getAppResources().getString(R.string.page_mode_NONE));\n\n        private String name;\n\n        Mode(String name) {\n            this.name = name;\n        }\n\n        public static Mode getPageMode(int pageMode) {\n            switch (pageMode) {\n                case 0:\n                    return COVER;\n                case 1:\n                    return SIMULATION;\n                case 2:\n                    return SLIDE;\n                case 3:\n                    return SCROLL;\n                case 4:\n                    return NONE;\n                default:\n                    return COVER;\n            }\n        }\n\n        public static String[] getAllPageMode() {\n            return new String[]{COVER.name, SIMULATION.name, SLIDE.name, SCROLL.name, NONE.name};\n        }\n\n        @NonNull\n        @Override\n        public String toString() {\n            return this.name;\n        }\n    }\n\n    /**\n     * 翻页方向\n     */\n    public enum Direction {\n        NONE(true), NEXT(true), PREV(true);\n\n        public final boolean isHorizontal;\n\n        Direction(boolean isHorizontal) {\n            this.isHorizontal = isHorizontal;\n        }\n    }\n\n    public interface OnPageChangeListener {\n\n        void resetScroll();\n\n        boolean hasPrev();\n\n        boolean hasNext(int pageOnCur);\n\n        void drawContent(Canvas canvas, float offset);\n\n        void drawBackground(Canvas canvas);\n\n        void changePage(Direction direction);\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/animation/ScrollPageAnim.java",
    "content": "package com.kunfei.bookshelf.widget.page.animation;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.view.MotionEvent;\nimport android.view.VelocityTracker;\nimport android.view.View;\nimport android.view.ViewConfiguration;\n\n\npublic class ScrollPageAnim extends PageAnimation {\n    private static final String TAG = \"ScrollAnimation\";\n    // 滑动追踪的时间\n    private static final int VELOCITY_DURATION = 1000;\n    private VelocityTracker mVelocity;\n    // 整个Bitmap的背景显示\n    private Bitmap mBgBitmap;\n\n    public ScrollPageAnim(int w, int h, int marginWidth, int marginTop, int marginBottom, View view, OnPageChangeListener listener) {\n        super(w, h, marginWidth, marginTop, marginBottom, view, listener);\n        mListener.resetScroll();\n        mBgBitmap = Bitmap.createBitmap(mScreenWidth, mScreenHeight, Bitmap.Config.ARGB_8888);\n    }\n\n    @Override\n    public void onTouchEvent(MotionEvent event) {\n        final int slop = ViewConfiguration.get(mView.getContext()).getScaledTouchSlop();\n        int x = (int) event.getX();\n        int y = (int) event.getY();\n\n        // 初始化速度追踪器\n        if (mVelocity == null) {\n            mVelocity = VelocityTracker.obtain();\n        }\n\n        mVelocity.addMovement(event);\n        // 设置触碰点\n        setTouchPoint(x, y);\n\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                isMove = false;\n                isRunning = false;\n                // 设置起始点\n                setStartPoint(x, y);\n                // 停止动画\n                abortAnim();\n                break;\n            case MotionEvent.ACTION_MOVE:\n                if (!isMove) {\n                    isMove = Math.abs(mStartX - x) > slop || Math.abs(mStartY - y) > slop;\n                }\n                mVelocity.computeCurrentVelocity(VELOCITY_DURATION);\n                isRunning = true;\n                // 进行刷新\n                mView.postInvalidate();\n                break;\n            case MotionEvent.ACTION_CANCEL:\n            case MotionEvent.ACTION_UP:\n                isRunning = false;\n                if (!isMove) {\n\n                    if (!readBookControl.getCanClickTurn() || readBookControl.disableScrollClickTurn()) {\n                        return;\n                    }\n\n                    //是否翻阅下一页。true表示翻到下一页，false表示上一页。\n                    boolean isNext = x > mScreenWidth / 2 || readBookControl.getClickAllNext();\n                    if (isNext) {\n                        startAnim(Direction.NEXT);\n                    } else {\n                        startAnim(Direction.PREV);\n                    }\n                } else {\n                    // 开启动画\n                    startAnim();\n                }\n                // 删除检测器\n                if (mVelocity != null) {\n                    mVelocity.recycle();\n                    mVelocity = null;\n                }\n                break;\n        }\n    }\n\n    @Override\n    public void draw(Canvas canvas) {\n        //进行布局\n        float offset = mLastY - mTouchY;\n\n        //绘制背景\n        mListener.drawBackground(canvas);\n        //绘制内容\n        canvas.save();\n        //移动位置\n        canvas.translate(0, mMarginTop);\n        //裁剪显示区域\n        canvas.clipRect(0, 0, mViewWidth, mViewHeight);\n        mListener.drawContent(canvas, offset);\n        canvas.restore();\n    }\n\n    @Override\n    public synchronized void startAnim() {\n        super.startAnim();\n        //惯性滚动\n        mScroller.fling(0, (int) mTouchY, 0, (int) mVelocity.getYVelocity(),\n                0, 0, -10 * mViewHeight, 10 * mViewHeight);\n    }\n\n    /**\n     * 翻页动画\n     */\n    public void startAnim(Direction direction) {\n        setStartPoint(0, 0);\n        setTouchPoint(0, 0);\n        switch (direction) {\n            case NEXT:\n                super.startAnim();\n                mScroller.startScroll(0, 0, 0, -mViewHeight + 300, animationSpeed);\n                break;\n            case PREV:\n                super.startAnim();\n                mScroller.startScroll(0, 0, 0, mViewHeight - 300, animationSpeed);\n                break;\n        }\n    }\n\n    @Override\n    public void abortAnim() {\n        if (!mScroller.isFinished()) {\n            mScroller.abortAnimation();\n            isRunning = false;\n        }\n    }\n\n    @Override\n    public boolean changePage() {\n        return false;\n    }\n\n    @Override\n    public Bitmap getBgBitmap(int pageOnCur) {\n        return mBgBitmap;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/animation/SimulationPageAnim.java",
    "content": "package com.kunfei.bookshelf.widget.page.animation;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.ColorMatrix;\nimport android.graphics.ColorMatrixColorFilter;\nimport android.graphics.Matrix;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.PointF;\nimport android.graphics.Region;\nimport android.graphics.drawable.GradientDrawable;\nimport android.os.Build;\nimport android.view.View;\n\n/**\n * 仿真翻页\n */\npublic class SimulationPageAnim extends HorizonPageAnim {\n    private static final String TAG = \"SimulationPageAnim\";\n\n    private int mCornerX = 1; // 拖拽点对应的页脚\n    private int mCornerY = 1;\n    private Path mPath0;\n    private Path mPath1;\n\n    private PointF mBezierStart1 = new PointF(); // 贝塞尔曲线起始点\n    private PointF mBezierControl1 = new PointF(); // 贝塞尔曲线控制点\n    private PointF mBezierVertex1 = new PointF(); // 贝塞尔曲线顶点\n    private PointF mBezierEnd1 = new PointF(); // 贝塞尔曲线结束点\n\n    private PointF mBezierStart2 = new PointF(); // 另一条贝塞尔曲线\n    private PointF mBezierControl2 = new PointF();\n    private PointF mBezierVertex2 = new PointF();\n    private PointF mBezierEnd2 = new PointF();\n\n    private float mMiddleX;\n    private float mMiddleY;\n    private float mDegrees;\n    private float mTouchToCornerDis;\n    private ColorMatrixColorFilter mColorMatrixFilter;\n    private Matrix mMatrix;\n    private float[] mMatrixArray = {0, 0, 0, 0, 0, 0, 0, 0, 1.0f};\n\n    private boolean mIsRT_LB; // 是否属于右上左下\n    private float mMaxLength;\n    private int[] mBackShadowColors;// 背面颜色组\n    private int[] mFrontShadowColors;// 前面颜色组\n    private GradientDrawable mBackShadowDrawableLR; // 有阴影的GradientDrawable\n    private GradientDrawable mBackShadowDrawableRL;\n    private GradientDrawable mFolderShadowDrawableLR;\n    private GradientDrawable mFolderShadowDrawableRL;\n\n    private GradientDrawable mFrontShadowDrawableHBT;\n    private GradientDrawable mFrontShadowDrawableHTB;\n    private GradientDrawable mFrontShadowDrawableVLR;\n    private GradientDrawable mFrontShadowDrawableVRL;\n\n    private Paint mPaint;\n\n\n    public SimulationPageAnim(int w, int h, View view, OnPageChangeListener listener) {\n        super(w, h, view, listener);\n        mPath0 = new Path();\n        mPath1 = new Path();\n        mMaxLength = (float) Math.hypot(mScreenWidth, mScreenHeight);\n        mPaint = new Paint();\n\n        mPaint.setStyle(Paint.Style.FILL);\n\n        createDrawable();\n\n        ColorMatrix cm = new ColorMatrix();//设置颜色数组\n        float[] array = {1, 0, 0, 0, 0,\n                0, 1, 0, 0, 0,\n                0, 0, 1, 0, 0,\n                0, 0, 0, 1, 0};\n        cm.set(array);\n        mColorMatrixFilter = new ColorMatrixColorFilter(cm);\n        mMatrix = new Matrix();\n\n        mTouchX = 0.01f; // 不让x,y为0,否则在点计算时会有问题\n        mTouchY = 0.01f;\n    }\n\n    @Override\n    public void setStartPoint(float x, float y) {\n        super.setStartPoint(x, y);\n        calcCornerXY(x, y);\n    }\n\n    @Override\n    public void setTouchPoint(float x, float y) {\n        super.setTouchPoint(x, y);\n        //触摸y中间位置吧y变成屏幕高度\n        if ((mStartY > mScreenHeight / 3.0 && mStartY < mScreenHeight * 2 / 3.0) || mDirection.equals(Direction.PREV)) {\n            mTouchY = mScreenHeight;\n        }\n\n        if (mStartY > mScreenHeight / 3.0 && mStartY < mScreenHeight / 2.0 && mDirection.equals(Direction.NEXT)) {\n            mTouchY = 1;\n        }\n    }\n\n    @Override\n    public void setDirection(Direction direction) {\n        super.setDirection(direction);\n\n        switch (direction) {\n            case PREV:\n                //上一页滑动不出现对角\n                if (mStartX > mScreenWidth / 2.0) {\n                    calcCornerXY(mStartX, mScreenHeight);\n                } else {\n                    calcCornerXY(mScreenWidth - mStartX, mScreenHeight);\n                }\n                break;\n            case NEXT:\n                if (mScreenWidth / 2.0 > mStartX) {\n                    calcCornerXY(mScreenWidth - mStartX, mStartY);\n                }\n                break;\n        }\n    }\n\n    @Override\n    public void startAnim() {\n        int dx, dy;\n        // dx 水平方向滑动的距离，负值会使滚动向左滚动\n        // dy 垂直方向滑动的距离，负值会使滚动向上滚动\n        if (isCancel) {\n\n            if (mCornerX > 0 && mDirection.equals(Direction.NEXT)) {\n                dx = (int) (mScreenWidth - mTouchX);\n            } else {\n                dx = -(int) mTouchX;\n            }\n\n            if (!mDirection.equals(Direction.NEXT)) {\n                dx = (int) -(mScreenWidth + mTouchX);\n            }\n\n            if (mCornerY > 0) {\n                dy = (int) (mScreenHeight - mTouchY);\n            } else {\n                dy = -(int) mTouchY; // 防止mTouchY最终变为0\n            }\n        } else {\n            if (mCornerX > 0 && mDirection.equals(Direction.NEXT)) {\n                dx = -(int) (mScreenWidth + mTouchX);\n            } else {\n                dx = (int) (mScreenWidth - mTouchX + mScreenWidth);\n            }\n            if (mCornerY > 0) {\n                dy = (int) (mScreenHeight - mTouchY);\n            } else {\n                dy = (int) (1 - mTouchY); // 防止mTouchY最终变为0\n            }\n        }\n        mScroller.startScroll((int) mTouchX, (int) mTouchY, dx, dy, animationSpeed);\n        super.startAnim();\n    }\n\n    @Override\n    public void drawMove(Canvas canvas) {\n        if (mDirection == Direction.NEXT) {\n            calcPoints();\n            drawCurrentPageArea(canvas, bitmapList.get(1));//绘制翻页时的正面页\n            drawNextPageAreaAndShadow(canvas, bitmapList.get(2));\n            drawCurrentPageShadow(canvas);\n            drawCurrentBackArea(canvas, bitmapList.get(1));\n        } else {\n            calcPoints();\n            drawCurrentPageArea(canvas, bitmapList.get(0));\n            drawNextPageAreaAndShadow(canvas, bitmapList.get(1));\n            drawCurrentPageShadow(canvas);\n            drawCurrentBackArea(canvas, bitmapList.get(0));\n        }\n    }\n\n    /**\n     * 创建阴影的GradientDrawable\n     */\n    private void createDrawable() {\n        int[] color = {0x333333, 0xb0333333};\n        mFolderShadowDrawableRL = new GradientDrawable(\n                GradientDrawable.Orientation.RIGHT_LEFT, color);\n        mFolderShadowDrawableRL\n                .setGradientType(GradientDrawable.LINEAR_GRADIENT);\n\n        mFolderShadowDrawableLR = new GradientDrawable(\n                GradientDrawable.Orientation.LEFT_RIGHT, color);\n        mFolderShadowDrawableLR\n                .setGradientType(GradientDrawable.LINEAR_GRADIENT);\n\n        mBackShadowColors = new int[]{0xff111111, 0x111111};\n        mBackShadowDrawableRL = new GradientDrawable(\n                GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors);\n        mBackShadowDrawableRL.setGradientType(GradientDrawable.LINEAR_GRADIENT);\n\n        mBackShadowDrawableLR = new GradientDrawable(\n                GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors);\n        mBackShadowDrawableLR.setGradientType(GradientDrawable.LINEAR_GRADIENT);\n\n        mFrontShadowColors = new int[]{0x80111111, 0x111111};\n        mFrontShadowDrawableVLR = new GradientDrawable(\n                GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors);\n        mFrontShadowDrawableVLR\n                .setGradientType(GradientDrawable.LINEAR_GRADIENT);\n        mFrontShadowDrawableVRL = new GradientDrawable(\n                GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors);\n        mFrontShadowDrawableVRL\n                .setGradientType(GradientDrawable.LINEAR_GRADIENT);\n\n        mFrontShadowDrawableHTB = new GradientDrawable(\n                GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors);\n        mFrontShadowDrawableHTB\n                .setGradientType(GradientDrawable.LINEAR_GRADIENT);\n\n        mFrontShadowDrawableHBT = new GradientDrawable(\n                GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors);\n        mFrontShadowDrawableHBT\n                .setGradientType(GradientDrawable.LINEAR_GRADIENT);\n    }\n\n    /**\n     * 绘制翻起页背面\n     */\n    private void drawCurrentBackArea(Canvas canvas, Bitmap bitmap) {\n        int i = (int) (mBezierStart1.x + mBezierControl1.x) / 2;\n        float f1 = Math.abs(i - mBezierControl1.x);\n        int i1 = (int) (mBezierStart2.y + mBezierControl2.y) / 2;\n        float f2 = Math.abs(i1 - mBezierControl2.y);\n        float f3 = Math.min(f1, f2);\n        mPath1.reset();\n        mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y);\n        mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y);\n        mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y);\n        mPath1.lineTo(mTouchX, mTouchY);\n        mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y);\n        mPath1.close();\n        GradientDrawable mFolderShadowDrawable;\n        int left;\n        int right;\n        if (mIsRT_LB) {\n            left = (int) (mBezierStart1.x - 1);\n            right = (int) (mBezierStart1.x + f3 + 1);\n            mFolderShadowDrawable = mFolderShadowDrawableLR;\n        } else {\n            left = (int) (mBezierStart1.x - f3 - 1);\n            right = (int) (mBezierStart1.x + 1);\n            mFolderShadowDrawable = mFolderShadowDrawableRL;\n        }\n        canvas.save();\n        try {\n            canvas.clipPath(mPath0);\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                canvas.clipPath(mPath1);\n            } else {\n                canvas.clipPath(mPath1, Region.Op.INTERSECT);\n            }\n        } catch (Exception ignored) {\n        }\n\n        mPaint.setColorFilter(mColorMatrixFilter);\n\n        float dis = (float) Math.hypot(mCornerX - mBezierControl1.x,\n                mBezierControl2.y - mCornerY);\n        float f8 = (mCornerX - mBezierControl1.x) / dis;\n        float f9 = (mBezierControl2.y - mCornerY) / dis;\n        mMatrixArray[0] = 1 - 2 * f9 * f9;\n        mMatrixArray[1] = 2 * f8 * f9;\n        mMatrixArray[3] = mMatrixArray[1];\n        mMatrixArray[4] = 1 - 2 * f8 * f8;\n        mMatrix.reset();\n        mMatrix.setValues(mMatrixArray);\n        mMatrix.preTranslate(-mBezierControl1.x, -mBezierControl1.y);\n        mMatrix.postTranslate(mBezierControl1.x, mBezierControl1.y);\n        canvas.drawBitmap(bitmap, mMatrix, mPaint);\n\n        mPaint.setColorFilter(null);\n\n        canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y);\n        mFolderShadowDrawable.setBounds(\n                left, (int) mBezierStart1.y,\n                right, (int) (mBezierStart1.y + mMaxLength)\n        );\n        mFolderShadowDrawable.draw(canvas);\n        canvas.restore();\n    }\n\n    /**\n     * 绘制翻起页的阴影\n     */\n    private void drawCurrentPageShadow(Canvas canvas) {\n        double degree;\n        if (mIsRT_LB) {\n            degree = Math.PI / 4 - Math.atan2(mBezierControl1.y - mTouchY, mTouchX - mBezierControl1.x);\n        } else {\n            degree = Math.PI / 4 - Math.atan2(mTouchY - mBezierControl1.y, mTouchX - mBezierControl1.x);\n        }\n        // 翻起页阴影顶点与touch点的距离\n        double d1 = (float) 25 * 1.414 * Math.cos(degree);\n        double d2 = (float) 25 * 1.414 * Math.sin(degree);\n        float x = (float) (mTouchX + d1);\n        float y;\n        if (mIsRT_LB) {\n            y = (float) (mTouchY + d2);\n        } else {\n            y = (float) (mTouchY - d2);\n        }\n        mPath1.reset();\n        mPath1.moveTo(x, y);\n        mPath1.lineTo(mTouchX, mTouchY);\n        mPath1.lineTo(mBezierControl1.x, mBezierControl1.y);\n        mPath1.lineTo(mBezierStart1.x, mBezierStart1.y);\n        mPath1.close();\n        canvas.save();\n        try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                canvas.clipOutPath(mPath0);\n            } else {\n                canvas.clipPath(mPath0, Region.Op.XOR);\n            }\n\n            canvas.clipPath(mPath1, Region.Op.INTERSECT);\n        } catch (Exception ignored) {\n        }\n\n        int leftx;\n        int rightx;\n        GradientDrawable mCurrentPageShadow;\n        if (mIsRT_LB) {\n            leftx = (int) (mBezierControl1.x);\n            rightx = (int) mBezierControl1.x + 25;\n            mCurrentPageShadow = mFrontShadowDrawableVLR;\n        } else {\n            leftx = (int) (mBezierControl1.x - 25);\n            rightx = (int) mBezierControl1.x + 1;\n            mCurrentPageShadow = mFrontShadowDrawableVRL;\n        }\n\n        float rotateDegrees;\n        rotateDegrees = (float) Math.toDegrees(Math.atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY));\n        canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y);\n        mCurrentPageShadow.setBounds(\n                leftx, (int) (mBezierControl1.y - mMaxLength),\n                rightx, (int) (mBezierControl1.y));\n        mCurrentPageShadow.draw(canvas);\n        canvas.restore();\n\n        mPath1.reset();\n        mPath1.moveTo(x, y);\n        mPath1.lineTo(mTouchX, mTouchY);\n        mPath1.lineTo(mBezierControl2.x, mBezierControl2.y);\n        mPath1.lineTo(mBezierStart2.x, mBezierStart2.y);\n        mPath1.close();\n        canvas.save();\n        try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                canvas.clipOutPath(mPath0);\n            } else {\n                canvas.clipPath(mPath0, Region.Op.XOR);\n            }\n\n            canvas.clipPath(mPath1);\n        } catch (Exception ignored) {\n        }\n\n        if (mIsRT_LB) {\n            leftx = (int) (mBezierControl2.y);\n            rightx = (int) (mBezierControl2.y + 25);\n            mCurrentPageShadow = mFrontShadowDrawableHTB;\n        } else {\n            leftx = (int) (mBezierControl2.y - 25);\n            rightx = (int) (mBezierControl2.y + 1);\n            mCurrentPageShadow = mFrontShadowDrawableHBT;\n        }\n        rotateDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX));\n        canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y);\n        float temp;\n        if (mBezierControl2.y < 0) temp = mBezierControl2.y - mScreenHeight;\n        else temp = mBezierControl2.y;\n\n        int hmg = (int) Math.hypot(mBezierControl2.x, temp);\n        if (hmg > mMaxLength)\n            mCurrentPageShadow.setBounds(\n                    (int) (mBezierControl2.x - 25) - hmg, leftx,\n                    (int) (mBezierControl2.x + mMaxLength) - hmg, rightx\n            );\n        else\n            mCurrentPageShadow.setBounds(\n                    (int) (mBezierControl2.x - mMaxLength), leftx,\n                    (int) (mBezierControl2.x), rightx);\n\n        mCurrentPageShadow.draw(canvas);\n        canvas.restore();\n    }\n\n    private void drawNextPageAreaAndShadow(Canvas canvas, Bitmap bitmap) {\n        mPath1.reset();\n        mPath1.moveTo(mBezierStart1.x, mBezierStart1.y);\n        mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y);\n        mPath1.lineTo(mBezierVertex2.x, mBezierVertex2.y);\n        mPath1.lineTo(mBezierStart2.x, mBezierStart2.y);\n        mPath1.lineTo(mCornerX, mCornerY);\n        mPath1.close();\n\n        mDegrees = (float) Math.toDegrees(Math.atan2(mBezierControl1.x - mCornerX,\n                mBezierControl2.y - mCornerY));\n        int leftx;\n        int rightx;\n        GradientDrawable mBackShadowDrawable;\n        if (mIsRT_LB) {  //左下及右上\n            leftx = (int) (mBezierStart1.x);\n            rightx = (int) (mBezierStart1.x + mTouchToCornerDis / 4);\n            mBackShadowDrawable = mBackShadowDrawableLR;\n        } else {\n            leftx = (int) (mBezierStart1.x - mTouchToCornerDis / 4);\n            rightx = (int) mBezierStart1.x;\n            mBackShadowDrawable = mBackShadowDrawableRL;\n        }\n        canvas.save();\n        try {\n            canvas.clipPath(mPath0);\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                canvas.clipPath(mPath1);\n            } else {\n                canvas.clipPath(mPath1, Region.Op.INTERSECT);\n            }\n            //canvas.clipPath(mPath1, Region.Op.INTERSECT);\n        } catch (Exception ignored) {\n        }\n\n        canvas.drawBitmap(bitmap, 0, 0, null);\n        canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y);\n        mBackShadowDrawable.setBounds(\n                leftx, (int) mBezierStart1.y,\n                rightx, (int) (mMaxLength + mBezierStart1.y));//左上及右下角的xy坐标值,构成一个矩形\n        mBackShadowDrawable.draw(canvas);\n        canvas.restore();\n    }\n\n    private void drawCurrentPageArea(Canvas canvas, Bitmap bitmap) {\n        mPath0.reset();\n        mPath0.moveTo(mBezierStart1.x, mBezierStart1.y);\n        mPath0.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, mBezierEnd1.y);\n        mPath0.lineTo(mTouchX, mTouchY);\n        mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y);\n        mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y);\n        mPath0.lineTo(mCornerX, mCornerY);\n        mPath0.close();\n\n        canvas.save();\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            canvas.clipOutPath(mPath0);\n        } else {\n            canvas.clipPath(mPath0, Region.Op.XOR);\n        }\n        canvas.drawBitmap(bitmap, 0, 0, null);\n        try {\n            canvas.restore();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n    }\n\n    /**\n     * 计算拖拽点对应的拖拽脚\n     */\n    private void calcCornerXY(float x, float y) {\n        if (x <= mScreenWidth / 2.0) {\n            mCornerX = 0;\n        } else {\n            mCornerX = mScreenWidth;\n        }\n        if (y <= mScreenHeight / 2.0) {\n            mCornerY = 0;\n        } else {\n            mCornerY = mScreenHeight;\n        }\n\n        mIsRT_LB = (mCornerX == 0 && mCornerY == mScreenHeight)\n                || (mCornerX == mScreenWidth && mCornerY == 0);\n\n    }\n\n    private void calcPoints() {\n        mMiddleX = (mTouchX + mCornerX) / 2;\n        mMiddleY = (mTouchY + mCornerY) / 2;\n        mBezierControl1.x =\n                mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX);\n        mBezierControl1.y = mCornerY;\n\n        mBezierControl2.x = mCornerX;\n        if (mCornerY - mMiddleY == 0) {\n            mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f;\n        } else {\n            mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX)\n                    * (mCornerX - mMiddleX) / (mCornerY - mMiddleY);\n        }\n        mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2;\n        mBezierStart1.y = mCornerY;\n\n        // 固定左边上下两个点\n        if (mTouchX > 0 && mTouchX < mScreenWidth) {\n            if (mBezierStart1.x < 0 || mBezierStart1.x > mScreenWidth) {\n                if (mBezierStart1.x < 0)\n                    mBezierStart1.x = mScreenWidth - mBezierStart1.x;\n\n                float f1 = Math.abs(mCornerX - mTouchX);\n                float f2 = mScreenWidth * f1 / mBezierStart1.x;\n                mTouchX = Math.abs(mCornerX - f2);\n\n                float f3 = Math.abs(mCornerX - mTouchX) * Math.abs(mCornerY - mTouchY) / f1;\n                mTouchY = Math.abs(mCornerY - f3);\n\n                mMiddleX = (mTouchX + mCornerX) / 2;\n                mMiddleY = (mTouchY + mCornerY) / 2;\n\n                mBezierControl1.x =\n                        mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX);\n                mBezierControl1.y = mCornerY;\n\n                mBezierControl2.x = mCornerX;\n                float f5 = mCornerY - mMiddleY;\n                if (f5 == 0) {\n                    mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX)\n                            * (mCornerX - mMiddleX) / 0.1f;\n                } else {\n                    mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX)\n                            * (mCornerX - mMiddleX) / (mCornerY - mMiddleY);\n                }\n\n                mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2;\n            }\n        }\n        mBezierStart2.x = mCornerX;\n        mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2;\n\n        mTouchToCornerDis = (float) Math.hypot((mTouchX - mCornerX), (mTouchY - mCornerY));\n\n        mBezierEnd1 =\n                getCross(new PointF(mTouchX, mTouchY), mBezierControl1, mBezierStart1, mBezierStart2);\n        mBezierEnd2 =\n                getCross(new PointF(mTouchX, mTouchY), mBezierControl2, mBezierStart1, mBezierStart2);\n\n        mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4;\n        mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4;\n        mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4;\n        mBezierVertex2.y = (2 * mBezierControl2.y + mBezierStart2.y + mBezierEnd2.y) / 4;\n    }\n\n    /**\n     * 求解直线P1P2和直线P3P4的交点坐标\n     */\n    private PointF getCross(PointF P1, PointF P2, PointF P3, PointF P4) {\n        PointF CrossP = new PointF();\n        // 二元函数通式： y=ax+b\n        float a1 = (P2.y - P1.y) / (P2.x - P1.x);\n        float b1 = ((P1.x * P2.y) - (P2.x * P1.y)) / (P1.x - P2.x);\n\n        float a2 = (P4.y - P3.y) / (P4.x - P3.x);\n        float b2 = ((P3.x * P4.y) - (P4.x * P3.y)) / (P3.x - P4.x);\n        CrossP.x = (b2 - b1) / (a1 - a2);\n        CrossP.y = a1 * CrossP.x + b1;\n        return CrossP;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/page/animation/SlidePageAnim.java",
    "content": "package com.kunfei.bookshelf.widget.page.animation;\n\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.view.View;\n\n/**\n * 滑动翻页\n */\n\npublic class SlidePageAnim extends HorizonPageAnim {\n    private Rect mSrcRect, mDestRect, mNextSrcRect, mNextDestRect;\n\n    public SlidePageAnim(int w, int h, View view, OnPageChangeListener listener) {\n        super(w, h, view, listener);\n        mSrcRect = new Rect(0, 0, mViewWidth, mViewHeight);\n        mDestRect = new Rect(0, 0, mViewWidth, mViewHeight);\n        mNextSrcRect = new Rect(0, 0, mViewWidth, mViewHeight);\n        mNextDestRect = new Rect(0, 0, mViewWidth, mViewHeight);\n    }\n\n    @Override\n    public void drawMove(Canvas canvas) {\n        int dis;\n        if (mDirection == Direction.NEXT) {//左半边的剩余区域\n            dis = (int) (mScreenWidth - mStartX + mTouchX);\n            if (dis > mScreenWidth) {\n                dis = mScreenWidth;\n            }\n            //计算bitmap截取的区域\n            mSrcRect.left = mScreenWidth - dis;\n            //计算bitmap在canvas显示的区域\n            mDestRect.right = dis;\n            //计算下一页截取的区域\n            mNextSrcRect.right = mScreenWidth - dis;\n            //计算下一页在canvas显示的区域\n            mNextDestRect.left = dis;\n\n            canvas.drawBitmap(bitmapList.get(2), mNextSrcRect, mNextDestRect, null);\n            canvas.drawBitmap(bitmapList.get(1), mSrcRect, mDestRect, null);\n        } else {\n            dis = (int) (mTouchX - mStartX);\n            if (dis < 0) {\n                dis = 0;\n                mStartX = mTouchX;\n            }\n            mSrcRect.left = mScreenWidth - dis;\n            mDestRect.right = dis;\n\n            //计算下一页截取的区域\n            mNextSrcRect.right = mScreenWidth - dis;\n            //计算下一页在canvas显示的区域\n            mNextDestRect.left = dis;\n\n            canvas.drawBitmap(bitmapList.get(1), mNextSrcRect, mNextDestRect, null);\n            canvas.drawBitmap(bitmapList.get(0), mSrcRect, mDestRect, null);\n        }\n    }\n\n    @Override\n    public void startAnim() {\n        int dx;\n        if (mDirection == Direction.NEXT) {\n            if (isCancel) {\n                int dis = (int) ((mScreenWidth - mStartX) + mTouchX);\n                if (dis > mScreenWidth) {\n                    dis = mScreenWidth;\n                }\n                dx = mScreenWidth - dis;\n            } else {\n                dx = (int) -(mTouchX + (mScreenWidth - mStartX));\n            }\n        } else {\n            if (isCancel) {\n                dx = (int) -Math.abs(mTouchX - mStartX);\n            } else {\n                dx = (int) (mScreenWidth - (mTouchX - mStartX));\n            }\n        }\n        //滑动速度保持一致\n        int duration = (animationSpeed * Math.abs(dx)) / mScreenWidth;\n        mScroller.startScroll((int) mTouchX, 0, dx, 0, duration);\n        super.startAnim();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/prefs/ATEPreferenceCategory.java",
    "content": "package com.kunfei.bookshelf.widget.prefs;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.preference.PreferenceCategory;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.widget.TextView;\n\nimport androidx.annotation.RequiresApi;\n\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n@SuppressWarnings(\"unused\")\npublic class ATEPreferenceCategory extends PreferenceCategory {\n\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    public ATEPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n    }\n\n    public ATEPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    public ATEPreferenceCategory(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public ATEPreferenceCategory(Context context) {\n        super(context);\n    }\n\n    @Override\n    protected void onBindView(View view) {\n        super.onBindView(view);\n        if (view.isInEditMode()) {\n            return;\n        }\n        if (view instanceof TextView) {\n            TextView tv = (TextView) view;\n            tv.setTextColor(ThemeStore.accentColor(view.getContext()));//设置title文本的颜色\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/prefs/ATESwitchPreference.java",
    "content": "package com.kunfei.bookshelf.widget.prefs;\n\nimport android.content.Context;\nimport android.os.Build;\nimport android.preference.SwitchPreference;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Switch;\n\nimport androidx.annotation.RequiresApi;\n\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\nimport java.util.LinkedList;\n\n@SuppressWarnings(\"unused\")\npublic class ATESwitchPreference extends SwitchPreference {\n\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    public ATESwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n    }\n\n    public ATESwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    public ATESwitchPreference(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public ATESwitchPreference(Context context) {\n        super(context);\n    }\n\n    @Override\n    protected void onBindView(View view) {\n        super.onBindView(view);\n        if (view.isInEditMode()) {\n            return;\n        }\n        if (view instanceof ViewGroup) {\n            ViewGroup viewGroup = (ViewGroup) view;\n            LinkedList<ViewGroup> queue = new LinkedList<>();\n            queue.add(viewGroup);\n            while (!queue.isEmpty()) {\n                ViewGroup current = queue.removeFirst();\n                for (int i = 0; i < current.getChildCount(); i++) {\n                    if (current.getChildAt(i) instanceof Switch) {\n                        ATH.setTint(current.getChildAt(i), ThemeStore.accentColor(view.getContext()));\n                        return;\n                    } else if (current.getChildAt(i) instanceof ViewGroup) {\n                        queue.addLast((ViewGroup) current.getChildAt(i));\n                    }\n                }\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/prefs/IconListPreference.java",
    "content": "package com.kunfei.bookshelf.widget.prefs;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.app.AlertDialog.Builder;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.drawable.Drawable;\nimport android.preference.ListPreference;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.CheckedTextView;\nimport android.widget.ImageView;\nimport android.widget.ListAdapter;\n\nimport com.kunfei.bookshelf.R;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n\npublic class IconListPreference extends ListPreference {\n\n    private List<Drawable> mEntryDrawables = new ArrayList<>();\n\n    public IconListPreference(Context context, AttributeSet attrs) {\n        super(context, attrs);\n\n        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0);\n\n        CharSequence[] drawables;\n\n        try {\n            drawables = a.getTextArray(R.styleable.IconListPreference_icons);\n        } finally {\n            a.recycle();\n        }\n\n        for (CharSequence drawable : drawables) {\n            int resId = context.getResources().getIdentifier(drawable.toString(), \"mipmap\", context.getPackageName());\n\n            Drawable d = context.getResources().getDrawable(resId);\n\n            mEntryDrawables.add(d);\n        }\n\n        setWidgetLayoutResource(R.layout.view_icon);\n    }\n\n    private ListAdapter createListAdapter() {\n        final String selectedValue = getValue();\n        int selectedIndex = findIndexOfValue(selectedValue);\n        return new AppArrayAdapter(getContext(), R.layout.item_icon_preference, getEntries(), mEntryDrawables, selectedIndex);\n    }\n\n    @Override\n    protected void onBindView(View view) {\n        super.onBindView(view);\n\n        String selectedValue = getValue();\n        int selectedIndex = findIndexOfValue(selectedValue);\n\n        Drawable drawable = mEntryDrawables.get(selectedIndex);\n\n        ((ImageView) view.findViewById(R.id.preview)).setImageDrawable(drawable);\n    }\n\n    @Override\n    protected void onPrepareDialogBuilder(Builder builder) {\n        builder.setAdapter(createListAdapter(), this);\n        super.onPrepareDialogBuilder(builder);\n    }\n\n    public static class AppArrayAdapter extends ArrayAdapter<CharSequence> {\n        private List<Drawable> mImageDrawables = null;\n        private int mSelectedIndex = 0;\n\n        AppArrayAdapter(Context context, int textViewResourceId,\n                        CharSequence[] objects, List<Drawable> imageDrawables,\n                        int selectedIndex) {\n            super(context, textViewResourceId, objects);\n            mSelectedIndex = selectedIndex;\n            mImageDrawables = imageDrawables;\n        }\n\n        @NotNull\n        @Override\n        @SuppressLint(\"ViewHolder\")\n        public View getView(int position, View convertView, ViewGroup parent) {\n            LayoutInflater inflater = ((Activity) getContext()).getLayoutInflater();\n            View view = inflater.inflate(R.layout.item_icon_preference, parent, false);\n            CheckedTextView textView = view.findViewById(R.id.label);\n            textView.setText(getItem(position));\n            textView.setChecked(position == mSelectedIndex);\n\n            ImageView imageView = view.findViewById(R.id.icon);\n            imageView.setImageDrawable(mImageDrawables.get(position));\n            return view;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/BaseExpandAbleViewHolder.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.expandable;\n\nimport android.content.Context;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * author：Drawthink\n * describe：BaseViewHolder\n * date: 2017/5/22\n */\n\npublic abstract class BaseExpandAbleViewHolder extends RecyclerView.ViewHolder {\n\n    public static final int VIEW_TYPE_PARENT = 1;\n    public static final int VIEW_TYPE_CHILD = 2;\n\n    public ViewGroup childView;\n\n    public ViewGroup groupView;\n\n    public BaseExpandAbleViewHolder(Context ctx, View itemView, int viewType) {\n        super(itemView);\n        switch (viewType) {\n            case VIEW_TYPE_PARENT:\n                groupView = (ViewGroup) itemView.findViewById(getGroupViewResId());\n                break;\n            case VIEW_TYPE_CHILD:\n                childView = (ViewGroup) itemView.findViewById(getChildViewResId());\n                break;\n        }\n    }\n\n    /**\n     * return ChildView root layout id\n     */\n    public abstract int getChildViewResId();\n\n    /**\n     * return GroupView root layout id\n     */\n    public abstract int getGroupViewResId();\n\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/BaseExpandableRecyclerAdapter.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.expandable;\n\nimport android.content.Context;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.BaseItem;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.GroupItem;\nimport com.kunfei.bookshelf.widget.recycler.expandable.bean.RecyclerViewData;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandAbleViewHolder.VIEW_TYPE_CHILD;\nimport static com.kunfei.bookshelf.widget.recycler.expandable.BaseExpandAbleViewHolder.VIEW_TYPE_PARENT;\n\n\n/**\n * author：Drawthink\n * describe:\n * date: 2017/5/22\n * T :group  data\n * S :child  data\n * VH :ViewHolder\n */\n\n@SuppressWarnings(\"unchecked\")\npublic abstract class BaseExpandableRecyclerAdapter<T, S, VH extends BaseExpandAbleViewHolder> extends RecyclerView.Adapter<VH> {\n\n    public static final String TAG = BaseExpandableRecyclerAdapter.class.getSimpleName();\n\n    private Context ctx;\n    /**\n     * all data\n     */\n    private List<RecyclerViewData> allDatas;\n    /**\n     * showing datas\n     */\n    private List showingDatas = new ArrayList<>();\n\n    /**\n     * child datas\n     */\n    private List<List<S>> childDatas;\n\n    private boolean canExpandAll;\n\n    private OnRecyclerViewListener.OnItemClickListener itemClickListener;\n    private OnRecyclerViewListener.OnItemLongClickListener itemLongClickListener;\n    private OnRecyclerViewListener.OnGroupCollapseListener groupCollapseListener;\n    private OnRecyclerViewListener.OnGroupExpandedListener groupExpandedListener;\n\n    public BaseExpandableRecyclerAdapter(Context ctx, List<RecyclerViewData> datas) {\n        this.ctx = ctx;\n        this.allDatas = datas;\n        setShowingDatas();\n        this.notifyDataSetChanged();\n    }\n\n    public void setOnItemClickListener(OnRecyclerViewListener.OnItemClickListener listener) {\n        this.itemClickListener = listener;\n    }\n\n    public void setOnItemLongClickListener(OnRecyclerViewListener.OnItemLongClickListener longClickListener) {\n        this.itemLongClickListener = longClickListener;\n    }\n\n    public void setGroupCollapseListener(OnRecyclerViewListener.OnGroupCollapseListener groupCollapseListener) {\n        this.groupCollapseListener = groupCollapseListener;\n    }\n\n    public void setGroupExpandedListener(OnRecyclerViewListener.OnGroupExpandedListener groupExpandedListener) {\n        this.groupExpandedListener = groupExpandedListener;\n    }\n\n    public List<RecyclerViewData> getAllDatas() {\n        return allDatas;\n    }\n\n    public void setAllDatas(List<RecyclerViewData> allDatas) {\n        this.allDatas = allDatas;\n        setShowingDatas();\n        this.notifyDataSetChanged();\n    }\n\n    public void clearAll() {\n        this.allDatas.clear();\n        setShowingDatas();\n        this.notifyDataSetChanged();\n    }\n\n    @Override\n    public int getItemCount() {\n        return null == showingDatas ? 0 : showingDatas.size();\n    }\n\n    @Override\n    public int getItemViewType(int position) {\n        if (showingDatas.get(position) instanceof GroupItem) {\n            return VIEW_TYPE_PARENT;\n        } else {\n            return VIEW_TYPE_CHILD;\n        }\n    }\n\n    @NonNull\n    @Override\n    public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        View view = null;\n        switch (viewType) {\n            case VIEW_TYPE_PARENT:\n                view = getGroupView(parent);\n                break;\n            case VIEW_TYPE_CHILD:\n                view = getChildView(parent);\n                break;\n        }\n        return createRealViewHolder(ctx, view, viewType);\n    }\n\n\n    @Override\n    public void onBindViewHolder(@NonNull final VH holder, final int position) {\n        final Object item = showingDatas.get(position);\n        final int gp = getGroupPosition(position);\n        final int cp = getChildPosition(gp, position);\n        if (item != null && item instanceof GroupItem) {\n            onBindGroupHolder(holder, gp, position, (T) ((GroupItem) item).getGroupData());\n            holder.groupView.setOnClickListener(v -> {\n                if (null != itemClickListener) {\n                    itemClickListener.onGroupItemClick(position, gp, holder.groupView);\n                }\n                if (((GroupItem) item).isExpand()) {\n                    collapseGroup(position);\n                } else {\n                    expandGroup(position);\n                }\n            });\n            holder.groupView.setOnLongClickListener(v -> {\n                if (null != itemLongClickListener) {\n                    itemLongClickListener.onGroupItemLongClick(position, gp, holder.groupView);\n                }\n                return true;\n            });\n        } else {\n            onBindChildpHolder(holder, gp, cp, position, (S) item);\n            holder.childView.setOnClickListener(v -> {\n                if (null != itemClickListener) {\n                    itemClickListener.onChildItemClick(position, gp, cp, holder.childView);\n                }\n            });\n            holder.childView.setOnLongClickListener(v -> {\n                if (null != itemLongClickListener) {\n                    int gp1 = getGroupPosition(position);\n                    itemLongClickListener.onChildItemLongClick(position, gp1, cp, holder.childView);\n                }\n                return true;\n            });\n        }\n    }\n\n\n    /**\n     * setup showing datas\n     */\n    private void setShowingDatas() {\n        if (null != showingDatas) {\n            showingDatas.clear();\n        }\n        if (this.childDatas == null) {\n            this.childDatas = new ArrayList<>();\n        }\n        childDatas.clear();\n        GroupItem groupItem;\n        for (int i = 0; i < allDatas.size(); i++) {\n            if (allDatas.get(i).getGroupItem() != null) {\n                groupItem = allDatas.get(i).getGroupItem();\n            } else {\n                break;\n            }\n            childDatas.add(i, groupItem.getChildDatas());\n            showingDatas.add(groupItem);\n            if (groupItem.hasChilds() && groupItem.isExpand()) {\n                showingDatas.addAll(groupItem.getChildDatas());\n            }\n        }\n    }\n\n    /**\n     * expandGroup\n     *\n     * @param position showingDatas position\n     */\n    public void expandGroup(int position) {\n        Object item = showingDatas.get(position);\n        if (null == item) {\n            return;\n        }\n        if (!(item instanceof GroupItem)) {\n            return;\n        }\n        if (((GroupItem) item).isExpand()) {\n            return;\n        }\n        if (!canExpandAll()) {\n            for (int i = 0; i < showingDatas.size(); i++) {\n                if (i != position) {\n                    int tempPositino = collapseGroup(i);\n                    if (tempPositino != -1) {\n                        position = tempPositino;\n                    }\n                }\n            }\n        }\n\n        List<BaseItem> tempChilds;\n        if (((GroupItem) item).hasChilds()) {\n            if (groupExpandedListener != null) {\n                groupExpandedListener.onGroupExpanded(position);\n            }\n            tempChilds = ((GroupItem) item).getChildDatas();\n            ((GroupItem) item).onExpand();\n            if (canExpandAll()) {\n                showingDatas.addAll(position + 1, tempChilds);\n                notifyItemRangeInserted(position + 1, tempChilds.size());\n                notifyItemRangeChanged(position, showingDatas.size() - (position + 1));\n            } else {\n                int tempPsi = showingDatas.indexOf(item);\n                showingDatas.addAll(tempPsi + 1, tempChilds);\n                notifyItemRangeInserted(tempPsi + 1, tempChilds.size());\n                notifyItemRangeChanged(tempPsi, showingDatas.size() - (tempPsi + 1));\n            }\n        }\n    }\n\n    /**\n     * collapseGroup\n     *\n     * @param position showingDatas position\n     */\n    private int collapseGroup(int position) {\n        Object item = showingDatas.get(position);\n        if (null == item) {\n            return -1;\n        }\n        if (!(item instanceof GroupItem)) {\n            return -1;\n        }\n        if (!((GroupItem) item).isExpand()) {\n            return -1;\n        }\n        int tempSize = showingDatas.size();\n        List<BaseItem> tempChilds;\n        if (((GroupItem) item).hasChilds()) {\n            if (groupCollapseListener != null) {\n                groupCollapseListener.onGroupCollapse(position);\n            }\n            tempChilds = ((GroupItem) item).getChildDatas();\n            ((GroupItem) item).onExpand();\n            showingDatas.removeAll(tempChilds);\n            notifyItemRangeRemoved(position + 1, tempChilds.size());\n            notifyItemRangeChanged(position, tempSize - (position + 1));\n            return position;\n        }\n        return -1;\n    }\n\n    /**\n     * @param position showingDatas position\n     * @return GroupPosition\n     */\n    private int getGroupPosition(int position) {\n        Object item = showingDatas.get(position);\n        if (item instanceof GroupItem) {\n            for (int j = 0; j < allDatas.size(); j++) {\n                if (allDatas.get(j).getGroupItem().equals(item)) {\n                    return j;\n                }\n            }\n        }\n        for (int i = 0; i < childDatas.size(); i++) {\n            if (childDatas.get(i).contains(item)) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    private int getChildPosition(int groupPosition, int showDataPosition) {\n        Object item = showingDatas.get(showDataPosition);\n        try {\n            return childDatas.get(groupPosition).indexOf(item);\n        } catch (IndexOutOfBoundsException ignored) {\n        }\n        return 0;\n    }\n\n    /**\n     * return groupView\n     */\n    public abstract View getGroupView(ViewGroup parent);\n\n    /**\n     * return childView\n     */\n    public abstract View getChildView(ViewGroup parent);\n\n    /**\n     * return <VH extends BaseViewHolder> instance\n     */\n    public abstract VH createRealViewHolder(Context ctx, View view, int viewType);\n\n    /**\n     * onBind groupData to groupView\n     */\n    public abstract void onBindGroupHolder(VH holder, int groupPos, int position, T groupData);\n\n    /**\n     * onBind childData to childView\n     */\n    public abstract void onBindChildpHolder(VH holder, int groupPos, int childPos, int position, S childData);\n\n    /**\n     * if return true Allow all expand otherwise Only one can be expand at the same time\n     */\n    public boolean canExpandAll() {\n        return canExpandAll;\n    }\n\n    public void setCanExpandAll(boolean canExpandAll) {\n        this.canExpandAll = canExpandAll;\n    }\n\n    /**\n     * 对原数据进行增加删除，调用此方法进行notify\n     */\n    public void notifyRecyclerViewData() {\n        notifyDataSetChanged();\n        setShowingDatas();\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/OnRecyclerViewListener.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.expandable;\n\nimport android.view.View;\n\n/**\n * author：Drawthink\n * describe：RecyclerViewListener\n * date: 2017/5/22\n */\n\npublic interface OnRecyclerViewListener {\n\n    /**\n     * 单击事件\n     */\n    interface OnItemClickListener {\n        /**\n         * position 当前在列表中的position\n         */\n        void onGroupItemClick(int position, int groupPosition, View view);\n\n        void onChildItemClick(int position, int groupPosition, int childPosition, View view);\n    }\n\n    /**\n     * 长按事件\n     */\n    interface OnItemLongClickListener {\n        void onGroupItemLongClick(int position, int groupPosition, View view);\n\n        void onChildItemLongClick(int position, int groupPosition, int childPosition, View view);\n    }\n\n    interface OnGroupExpandedListener {\n        /**\n         * 分组展开\n         *\n         * @param groupPosition 分组的位置\n         */\n        void onGroupExpanded(int groupPosition);\n    }\n\n    interface OnGroupCollapseListener {\n        /**\n         * 分组收起\n         *\n         * @param groupPosition 分组的位置\n         */\n        void onGroupCollapse(int groupPosition);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/bean/BaseItem.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.expandable.bean;\n\n/**\n * author：Drawthink\n * describe:\n * date: 2017/5/22\n */\n\npublic abstract class BaseItem {\n\n    public abstract boolean isParent();\n\n//    public abstract boolean isExpand();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/bean/GroupItem.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.expandable.bean;\n\nimport java.util.List;\n\n/**\n * author：Drawthink\n * describe:\n * date: 2017/5/22\n * T 为group数据对象\n * S 为child数据对象\n */\n\npublic class GroupItem<T, S> extends BaseItem {\n\n    /**\n     * head data\n     */\n    private T groupData;\n\n    /**\n     * childDatas\n     */\n    private List<S> childDatas;\n\n    /**\n     * 是否展开,  默认展开\n     */\n    private boolean isExpand = true;\n\n\n    public GroupItem(T groupData, List<S> childDatas, boolean isExpand) {\n        this.groupData = groupData;\n        this.childDatas = childDatas;\n        this.isExpand = isExpand;\n    }\n\n    /**\n     * 返回是否是父节点\n     */\n    @Override\n    public boolean isParent() {\n        return true;\n    }\n\n    public boolean isExpand() {\n        return isExpand;\n    }\n\n    public void onExpand() {\n        isExpand = !isExpand;\n    }\n\n    public boolean hasChilds() {\n        return getChildDatas() != null && !getChildDatas().isEmpty();\n    }\n\n    public List<S> getChildDatas() {\n        return childDatas;\n    }\n\n    public void setChildDatas(List<S> childDatas) {\n        this.childDatas = childDatas;\n    }\n\n    public void removeChild(int childPosition) {\n        if (childDatas == null || childDatas.size() == 0) {\n            return;\n        }\n        childDatas.remove(childPosition);\n    }\n\n    public T getGroupData() {\n        return groupData;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/expandable/bean/RecyclerViewData.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.expandable.bean;\n\nimport java.util.List;\n\n/**\n * author：Drawthink\n * describe:\n * date: 2017/5/22\n * T 为group数据对象\n * S 为child数据对象\n */\n@SuppressWarnings(\"unchecked\")\npublic class RecyclerViewData<T, S> {\n\n    private GroupItem groupItem;\n\n    /**\n     * @param isExpand 初始化展示数据时，该组数据是否展开\n     */\n    public RecyclerViewData(T groupData, List<S> childDatas, boolean isExpand) {\n        this.groupItem = new GroupItem(groupData, childDatas, isExpand);\n    }\n\n    public RecyclerViewData(T groupData, List<S> childDatas) {\n        this.groupItem = new GroupItem(groupData, childDatas, false);\n    }\n\n    public GroupItem getGroupItem() {\n        return groupItem;\n    }\n\n    public void setGroupItem(GroupItem groupItem) {\n        this.groupItem = groupItem;\n    }\n\n    public T getGroupData() {\n        return (T) groupItem.getGroupData();\n    }\n\n    public List<S> getChildList() {\n        return groupItem.getChildDatas();\n    }\n\n    public void removeChild(int position) {\n        if (null == groupItem || !groupItem.hasChilds()) {\n            return;\n        }\n        groupItem.getChildDatas().remove(position);\n    }\n\n    public S getChild(int childPosition) {\n        return (S) groupItem.getChildDatas().get(childPosition);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/BaseRefreshListener.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\npublic interface BaseRefreshListener {\n\n    public void startRefresh();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/OnLoadMoreListener.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\npublic interface OnLoadMoreListener {\n\n    public void startLoadMore();\n\n    public void loadMoreErrorTryAgain();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/OnRefreshWithProgressListener.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\npublic interface OnRefreshWithProgressListener extends BaseRefreshListener {\n\n    public int getMaxProgress();\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshLayout.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\n\nimport com.kunfei.bookshelf.R;\n\n/**\n * Created by newbiechen on 17-4-22.\n * 功能:\n * 1. 加载动画\n * 2. 加载错误点击重新加载\n */\n\npublic class RefreshLayout extends FrameLayout {\n\n    protected static final int STATUS_LOADING = 0;\n    protected static final int STATUS_FINISH = 1;\n    protected static final int STATUS_ERROR = 2;\n    protected static final int STATUS_EMPTY = 3;\n    private static final String TAG = \"RefreshLayout\";\n    private Context mContext;\n\n    private int mEmptyViewId;\n    private int mErrorViewId;\n    private int mLoadingViewId;\n\n    private View mEmptyView;\n    private View mErrorView;\n    private View mLoadingView;\n    private View mContentView;\n\n    private OnReloadingListener mListener;\n    private int mStatus = 0;\n\n    public RefreshLayout(Context context) {\n        this(context, null);\n    }\n\n    public RefreshLayout(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        mContext = context;\n        initAttrs(attrs);\n        initView();\n    }\n\n    private void initAttrs(AttributeSet attrs) {\n        TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.RefreshLayout);\n        mEmptyViewId = typedArray.getResourceId(R.styleable.RefreshLayout_layout_refresh_empty, R.layout.view_empty);\n        mErrorViewId = typedArray.getResourceId(R.styleable.RefreshLayout_layout_refresh_error, R.layout.view_net_error);\n        mLoadingViewId = typedArray.getResourceId(R.styleable.RefreshLayout_layout_refresh_loading, R.layout.view_loading);\n\n        typedArray.recycle();\n    }\n\n    private void initView() {\n\n        //添加在empty、error、loading 情况下的布局\n        mEmptyView = inflateView(mEmptyViewId);\n        mErrorView = inflateView(mErrorViewId);\n        mLoadingView = inflateView(mLoadingViewId);\n\n        addView(mEmptyView);\n        addView(mErrorView);\n        addView(mLoadingView);\n\n        //设置监听器\n        mErrorView.setOnClickListener(\n                (view) -> {\n                    if (mListener != null) {\n                        toggleStatus(STATUS_LOADING);\n                        mListener.onReload();\n                    }\n                }\n        );\n    }\n\n    @Override\n    protected void onFinishInflate() {\n        super.onFinishInflate();\n        toggleStatus(STATUS_LOADING);\n    }\n\n    @Override\n    public void onViewAdded(View child) {\n        super.onViewAdded(child);\n        if (getChildCount() == 4) {\n            mContentView = child;\n        }\n    }\n\n    //除了自带的数据，保证子类只能够添加一个子View\n    @Override\n    public void addView(View child) {\n        if (getChildCount() > 4) {\n            throw new IllegalStateException(\"RefreshLayout can host only one direct child\");\n        }\n        super.addView(child);\n    }\n\n    @Override\n    public void addView(View child, int index) {\n        if (getChildCount() > 4) {\n            throw new IllegalStateException(\"RefreshLayout can host only one direct child\");\n        }\n\n        super.addView(child, index);\n    }\n\n    @Override\n    public void addView(View child, ViewGroup.LayoutParams params) {\n        if (getChildCount() > 4) {\n            throw new IllegalStateException(\"RefreshLayout can host only one direct child\");\n        }\n\n        super.addView(child, params);\n    }\n\n    @Override\n    public void addView(View child, int index, ViewGroup.LayoutParams params) {\n        if (getChildCount() > 4) {\n            throw new IllegalStateException(\"RefreshLayout can host only one direct child\");\n        }\n\n        super.addView(child, index, params);\n    }\n\n    public void showLoading() {\n        if (mStatus != STATUS_LOADING) {\n            toggleStatus(STATUS_LOADING);\n        }\n    }\n\n    public void showFinish() {\n        if (mStatus == STATUS_LOADING) {\n            toggleStatus(STATUS_FINISH);\n        }\n    }\n\n    public void showError() {\n        if (mStatus != STATUS_ERROR) {\n            toggleStatus(STATUS_ERROR);\n        }\n    }\n\n    public void showEmpty() {\n        if (mStatus != STATUS_EMPTY) {\n            toggleStatus(STATUS_EMPTY);\n        }\n    }\n\n    //视图根据状态切换\n    private void toggleStatus(int status) {\n        switch (status) {\n            case STATUS_LOADING:\n                mLoadingView.setVisibility(VISIBLE);\n                mEmptyView.setVisibility(GONE);\n                mErrorView.setVisibility(GONE);\n                if (mContentView != null) {\n                    mContentView.setVisibility(GONE);\n                }\n                break;\n            case STATUS_FINISH:\n                if (mContentView != null) {\n                    mContentView.setVisibility(VISIBLE);\n                }\n                mLoadingView.setVisibility(GONE);\n                mEmptyView.setVisibility(GONE);\n                mErrorView.setVisibility(GONE);\n                break;\n            case STATUS_ERROR:\n                mErrorView.setVisibility(VISIBLE);\n                mLoadingView.setVisibility(GONE);\n                mEmptyView.setVisibility(GONE);\n                if (mContentView != null) {\n                    mContentView.setVisibility(GONE);\n                }\n                break;\n            case STATUS_EMPTY:\n                mEmptyView.setVisibility(VISIBLE);\n                mErrorView.setVisibility(GONE);\n                mLoadingView.setVisibility(GONE);\n                if (mContentView != null) {\n                    mContentView.setVisibility(GONE);\n                }\n                break;\n        }\n        mStatus = status;\n    }\n\n    protected int getStatus() {\n        return mStatus;\n    }\n\n    public void setOnReloadingListener(OnReloadingListener listener) {\n        mListener = listener;\n    }\n\n    private View inflateView(int id) {\n        return LayoutInflater.from(mContext)\n                .inflate(id, this, false);\n    }\n\n    //数据存储\n    @Override\n    protected Parcelable onSaveInstanceState() {\n        Parcelable superParcel = super.onSaveInstanceState();\n        SavedState savedState = new SavedState(superParcel);\n        savedState.status = mStatus;\n        return savedState;\n    }\n\n    @Override\n    protected void onRestoreInstanceState(Parcelable state) {\n        SavedState savedState = (SavedState) state;\n        super.onRestoreInstanceState(savedState.getSuperState());\n        //刷新状态\n        toggleStatus(savedState.status);\n    }\n\n    //添加错误重新加载的监听\n    public interface OnReloadingListener {\n        void onReload();\n    }\n\n    static class SavedState extends BaseSavedState {\n        public static final Creator<SavedState> CREATOR\n                = new Creator<SavedState>() {\n            public SavedState createFromParcel(Parcel in) {\n                return new SavedState(in);\n            }\n\n            public SavedState[] newArray(int size) {\n                return new SavedState[size];\n            }\n        };\n        int status;\n\n        SavedState(Parcelable superState) {\n            super(superState);\n        }\n\n        private SavedState(Parcel in) {\n            super(in);\n            status = in.readInt();\n        }\n\n        @Override\n        public void writeToParcel(Parcel out, int flags) {\n            super.writeToParcel(out, flags);\n            out.writeInt(status);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshProgressBar.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.os.Looper;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport com.kunfei.bookshelf.R;\n\npublic class RefreshProgressBar extends View {\n    int a = 1;\n    private int maxProgress = 100;\n    private int durProgress = 0;\n    private int secondMaxProgress = 100;\n    private int secondDurProgress = 0;\n    private int bgColor = 0x00000000;\n    private int secondColor = 0xFFC1C1C1;\n    private int fontColor = 0xFF363636;\n    private int speed = 1;\n    private int secondFinalProgress = 0;\n    private Paint paint;\n    private Boolean isAutoLoading = false;\n    private Rect bgRect = new Rect();\n    private Rect secondRect = new Rect();\n    private RectF fontRectF = new RectF();\n\n    public RefreshProgressBar(Context context) {\n        this(context, null);\n    }\n\n    public RefreshProgressBar(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public RefreshProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n\n        init(context, attrs, defStyleAttr);\n    }\n\n    public Boolean getIsAutoLoading() {\n        return isAutoLoading;\n    }\n\n    public void setIsAutoLoading(Boolean loading) {\n        if (loading && getVisibility() != View.VISIBLE) {\n            setVisibility(View.VISIBLE);\n        }\n        isAutoLoading = loading;\n        if (!isAutoLoading) {\n            secondDurProgress = 0;\n            secondFinalProgress = 0;\n        }\n        maxProgress = 0;\n\n        invalidate();\n    }\n\n    private void init(Context context, AttributeSet attrs, int defStyleAttr) {\n        paint = new Paint();\n        paint.setStyle(Paint.Style.FILL);\n\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshProgressBar);\n        speed = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_speed, speed);\n        maxProgress = a.getInt(R.styleable.RefreshProgressBar_max_progress, maxProgress);\n        durProgress = a.getInt(R.styleable.RefreshProgressBar_dur_progress, durProgress);\n        secondDurProgress = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_second_dur_progress, secondDurProgress);\n        secondFinalProgress = secondDurProgress;\n        secondMaxProgress = a.getDimensionPixelSize(R.styleable.RefreshProgressBar_second_max_progress, secondMaxProgress);\n        bgColor = a.getColor(R.styleable.RefreshProgressBar_bg_color, bgColor);\n        secondColor = a.getColor(R.styleable.RefreshProgressBar_second_color, secondColor);\n        fontColor = a.getColor(R.styleable.RefreshProgressBar_font_color, fontColor);\n        a.recycle();\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n\n        paint.setColor(bgColor);\n        bgRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());\n        canvas.drawRect(bgRect, paint);\n\n        if (secondDurProgress > 0 && secondMaxProgress > 0) {\n            int secondDur = secondDurProgress;\n            if (secondDur > secondMaxProgress) {\n                secondDur = secondMaxProgress;\n            }\n            paint.setColor(secondColor);\n            int tempW = (int) (getMeasuredWidth() * 1.0f * (secondDur * 1.0f / secondMaxProgress));\n            secondRect.set(getMeasuredWidth() / 2 - tempW / 2, 0, getMeasuredWidth() / 2 + tempW / 2, getMeasuredHeight());\n            canvas.drawRect(secondRect, paint);\n        }\n\n        if (durProgress > 0 && maxProgress > 0) {\n            paint.setColor(fontColor);\n            fontRectF.set(0, 0, getMeasuredWidth() * 1.0f * (durProgress * 1.0f / maxProgress), getMeasuredHeight());\n            canvas.drawRect(fontRectF, paint);\n        }\n\n        if (isAutoLoading) {\n            if (secondDurProgress >= secondMaxProgress) {\n                a = -1;\n            } else if (secondDurProgress <= 0) {\n                a = 1;\n            }\n            secondDurProgress += (a * speed);\n            if (secondDurProgress < 0)\n                secondDurProgress = 0;\n            else if (secondDurProgress > secondMaxProgress)\n                secondDurProgress = secondMaxProgress;\n            secondFinalProgress = secondDurProgress;\n            invalidate();\n        } else {\n            if (secondDurProgress != secondFinalProgress) {\n                if (secondDurProgress > secondFinalProgress) {\n                    secondDurProgress -= speed;\n                    if (secondDurProgress < secondFinalProgress) {\n                        secondDurProgress = secondFinalProgress;\n                    }\n                } else {\n                    secondDurProgress += speed;\n                    if (secondDurProgress > secondFinalProgress) {\n                        secondDurProgress = secondFinalProgress;\n                    }\n                }\n                this.invalidate();\n            }\n            if (secondDurProgress == 0 && durProgress == 0 && secondFinalProgress == 0 && getVisibility() == View.VISIBLE) {\n                setVisibility(View.INVISIBLE);\n            }\n        }\n    }\n\n    public int getMaxProgress() {\n        return maxProgress;\n    }\n\n    public void setMaxProgress(int maxProgress) {\n        this.maxProgress = maxProgress;\n    }\n\n    public int getDurProgress() {\n        return durProgress;\n    }\n\n    public void setDurProgress(int durProgress) {\n        if (durProgress < 0) {\n            durProgress = 0;\n        }\n        if (durProgress > maxProgress) {\n            durProgress = maxProgress;\n        }\n        this.durProgress = durProgress;\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            this.invalidate();\n        } else {\n            this.postInvalidate();\n        }\n    }\n\n    public int getSecondMaxProgress() {\n        return secondMaxProgress;\n    }\n\n    public void setSecondMaxProgress(int secondMaxProgress) {\n        this.secondMaxProgress = secondMaxProgress;\n    }\n\n    public int getSecondDurProgress() {\n        return secondDurProgress;\n    }\n\n    public void setSecondDurProgress(int secondDur) {\n        this.secondDurProgress = secondDur;\n        this.secondFinalProgress = secondDurProgress;\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            this.invalidate();\n        } else {\n            this.postInvalidate();\n        }\n    }\n\n    public void setSecondDurProgressWithAnim(int secondDur) {\n        if (secondDur < 0) {\n            secondDur = 0;\n        }\n        if (secondDur > secondMaxProgress) {\n            secondDur = secondMaxProgress;\n        }\n        this.secondFinalProgress = secondDur;\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            this.invalidate();\n        } else {\n            this.postInvalidate();\n        }\n    }\n\n    public void clean() {\n        durProgress = 0;\n        secondDurProgress = 0;\n        secondFinalProgress = 0;\n        if (Looper.myLooper() == Looper.getMainLooper()) {\n            this.invalidate();\n        } else {\n            this.postInvalidate();\n        }\n    }\n\n    public int getBgColor() {\n        return bgColor;\n    }\n\n    public void setBgColor(int bgColor) {\n        this.bgColor = bgColor;\n    }\n\n    public int getSecondColor() {\n        return secondColor;\n    }\n\n    public void setSecondColor(int secondColor) {\n        this.secondColor = secondColor;\n    }\n\n    public int getFontColor() {\n        return fontColor;\n    }\n\n    public void setFontColor(int fontColor) {\n        this.fontColor = fontColor;\n    }\n\n    public int getSpeed() {\n        return speed;\n    }\n\n    public void setSpeed(int speed) {\n        this.speed = speed;\n    }\n\n    public int getSecondFinalProgress() {\n        return secondFinalProgress;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshRecyclerView.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.ItemTouchHelper;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.databinding.ViewRefreshRecyclerBinding;\n\nimport java.util.Objects;\n\npublic class RefreshRecyclerView extends FrameLayout {\n\n    private ViewRefreshRecyclerBinding binding = ViewRefreshRecyclerBinding.inflate(LayoutInflater.from(getContext()), this, true);\n    private View noDataView;\n    private View refreshErrorView;\n    private float durTouchX = -1000000;\n    private float durTouchY = -1000000;\n    private BaseRefreshListener baseRefreshListener;\n    private OnLoadMoreListener loadMoreListener;\n    private OnTouchListener refreshTouchListener = new OnTouchListener() {\n        @SuppressLint(\"ClickableViewAccessibility\")\n        @Override\n        public boolean onTouch(View v, MotionEvent event) {\n            int action = event.getAction();\n            switch (action) {\n                case MotionEvent.ACTION_DOWN:\n                    durTouchX = event.getX();\n                    durTouchY = event.getY();\n                    break;\n                case MotionEvent.ACTION_MOVE:\n                    if (durTouchX == -1000000) {\n                        durTouchX = event.getX();\n                    }\n                    if (durTouchY == -1000000)\n                        durTouchY = event.getY();\n\n                    float dY = event.getY() - durTouchY;  //>0下拉\n                    durTouchY = event.getY();\n                    if (baseRefreshListener != null\n                            && ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getIsRequesting() == 0\n                            && binding.rpb.getSecondDurProgress() == binding.rpb.getSecondFinalProgress()) {\n                        if (binding.rpb.getVisibility() != View.VISIBLE) {\n                            binding.rpb.setVisibility(View.VISIBLE);\n                        }\n                        if (binding.recyclerView.getAdapter().getItemCount() > 0) {\n                            if (0 == ((LinearLayoutManager) Objects.requireNonNull(binding.recyclerView.getLayoutManager())).findFirstCompletelyVisibleItemPosition()) {\n                                binding.rpb.setSecondDurProgress((int) (binding.rpb.getSecondDurProgress() + dY));\n                            }\n                        } else {\n                            binding.rpb.setSecondDurProgress((int) (binding.rpb.getSecondDurProgress() + dY));\n                        }\n                        return binding.rpb.getSecondDurProgress() > 0;\n                    }\n                    break;\n                case MotionEvent.ACTION_UP:\n                    if (baseRefreshListener != null && binding.rpb.getSecondMaxProgress() > 0 && binding.rpb.getSecondDurProgress() > 0) {\n                        if (binding.rpb.getSecondDurProgress() >= binding.rpb.getSecondMaxProgress() && ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getIsRequesting() == 0) {\n                            if (baseRefreshListener instanceof OnRefreshWithProgressListener) {\n                                //带有进度的\n                                //执行刷新响应\n                                ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(false, false);\n                                ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsRequesting(1, true);\n                                binding.rpb.setMaxProgress(((OnRefreshWithProgressListener) baseRefreshListener).getMaxProgress());\n                                baseRefreshListener.startRefresh();\n                                if (noDataView != null) {\n                                    noDataView.setVisibility(GONE);\n                                }\n                                if (refreshErrorView != null) {\n                                    refreshErrorView.setVisibility(GONE);\n                                }\n                            } else {\n                                //不带进度的\n                                ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(false, false);\n                                ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsRequesting(1, true);\n                                baseRefreshListener.startRefresh();\n                                if (noDataView != null) {\n                                    noDataView.setVisibility(GONE);\n                                }\n                                if (refreshErrorView != null) {\n                                    refreshErrorView.setVisibility(GONE);\n                                }\n                                binding.rpb.setIsAutoLoading(true);\n                            }\n                        } else {\n                            if (((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getIsRequesting() != 1)\n                                binding.rpb.setSecondDurProgressWithAnim(0);\n                        }\n                    }\n                    durTouchX = -1000000;\n                    durTouchY = -1000000;\n                    break;\n            }\n            return false;\n        }\n    };\n\n    public RefreshRecyclerView(Context context) {\n        this(context, null);\n    }\n\n    public RefreshRecyclerView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public RefreshRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n\n        @SuppressLint(\"CustomViewStyleable\")\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshProgressBar);\n        binding.rpb.setSpeed(a.getDimensionPixelSize(R.styleable.RefreshProgressBar_speed, binding.rpb.getSpeed()));\n        binding.rpb.setMaxProgress(a.getInt(R.styleable.RefreshProgressBar_max_progress, binding.rpb.getMaxProgress()));\n        binding.rpb.setSecondMaxProgress(a.getDimensionPixelSize(R.styleable.RefreshProgressBar_second_max_progress, binding.rpb.getSecondMaxProgress()));\n        binding.rpb.setBgColor(a.getColor(R.styleable.RefreshProgressBar_bg_color, binding.rpb.getBgColor()));\n        binding.rpb.setSecondColor(a.getColor(R.styleable.RefreshProgressBar_second_color, binding.rpb.getSecondColor()));\n        binding.rpb.setFontColor(a.getColor(R.styleable.RefreshProgressBar_font_color, binding.rpb.getFontColor()));\n        a.recycle();\n\n        bindEvent();\n    }\n\n    public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {\n        binding.recyclerView.addItemDecoration(decor);\n    }\n\n    public void setBaseRefreshListener(BaseRefreshListener baseRefreshListener) {\n        this.baseRefreshListener = baseRefreshListener;\n    }\n\n    public void setLoadMoreListener(OnLoadMoreListener loadMoreListener) {\n        this.loadMoreListener = loadMoreListener;\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private void bindEvent() {\n        binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {\n\n            @Override\n            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {\n                super.onScrolled(recyclerView, dx, dy);\n                if (((RefreshRecyclerViewAdapter) Objects.requireNonNull(recyclerView.getAdapter())).canLoadMore()\n                        && recyclerView.getAdapter().getItemCount() - 1 == ((LinearLayoutManager) Objects.requireNonNull(recyclerView.getLayoutManager())).findLastVisibleItemPosition()) {\n                    if (!((RefreshRecyclerViewAdapter) recyclerView.getAdapter()).getLoadMoreError()) {\n                        if (null != loadMoreListener) {\n                            ((RefreshRecyclerViewAdapter) recyclerView.getAdapter()).setIsRequesting(2, false);\n                            loadMoreListener.startLoadMore();\n                        }\n                    }\n                }\n            }\n        });\n        binding.recyclerView.setOnTouchListener(refreshTouchListener);\n    }\n\n    public RefreshProgressBar getRpb() {\n        return binding.rpb;\n    }\n\n    public RecyclerView getRecyclerView() {\n        return binding.recyclerView;\n    }\n\n    public void refreshError() {\n        binding.rpb.setIsAutoLoading(false);\n        binding.rpb.clean();\n        ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, true);\n        if (noDataView != null) {\n            noDataView.setVisibility(GONE);\n        }\n        if (refreshErrorView != null) {\n            refreshErrorView.setVisibility(VISIBLE);\n        }\n    }\n\n    public void startRefresh() {\n        if (baseRefreshListener instanceof OnRefreshWithProgressListener) {\n            ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsAll(false, false);\n            ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsRequesting(1, false);\n            binding.rpb.setSecondDurProgress(binding.rpb.getSecondMaxProgress());\n            binding.rpb.setMaxProgress(((OnRefreshWithProgressListener) baseRefreshListener).getMaxProgress());\n        } else {\n            ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(1, true);\n            binding.rpb.setIsAutoLoading(true);\n            if (noDataView != null) {\n                noDataView.setVisibility(GONE);\n            }\n            if (refreshErrorView != null) {\n                refreshErrorView.setVisibility(GONE);\n            }\n        }\n    }\n\n    public void finishRefresh(Boolean needNotify) {\n        finishRefresh(((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).getICount() == 0, needNotify);\n    }\n\n    public void finishRefresh(Boolean isAll, Boolean needNotify) {\n        binding.rpb.setDurProgress(0);\n        if (isAll) {\n            ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, false);\n            binding.rpb.setIsAutoLoading(false);\n            ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(true, needNotify);\n        } else {\n            binding.rpb.setIsAutoLoading(false);\n            ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, needNotify);\n        }\n\n        if (isAll) {\n            if (noDataView != null) {\n                binding.recyclerView.post(() -> {\n                    if (((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).getICount() == 0) {\n                        noDataView.setVisibility(VISIBLE);\n                    } else {\n                        noDataView.setVisibility(GONE);\n                    }\n                });\n            }\n            if (refreshErrorView != null) {\n                refreshErrorView.setVisibility(GONE);\n            }\n        }\n    }\n\n    public void finishLoadMore(Boolean isAll, Boolean needNoti) {\n        if (isAll) {\n            ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, false);\n            ((RefreshRecyclerViewAdapter) binding.recyclerView.getAdapter()).setIsAll(true, needNoti);\n        } else {\n            ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setIsRequesting(0, needNoti);\n        }\n\n        if (noDataView != null) {\n            noDataView.setVisibility(GONE);\n        }\n        if (refreshErrorView != null) {\n            refreshErrorView.setVisibility(GONE);\n        }\n    }\n\n    public void setRefreshRecyclerViewAdapter(RefreshRecyclerViewAdapter refreshRecyclerViewAdapter, RecyclerView.LayoutManager layoutManager) {\n        refreshRecyclerViewAdapter.setClickTryAgainListener(() -> {\n            if (loadMoreListener != null)\n                loadMoreListener.loadMoreErrorTryAgain();\n        });\n        binding.recyclerView.setLayoutManager(layoutManager);\n        binding.recyclerView.setAdapter(refreshRecyclerViewAdapter);\n    }\n\n    public void setRefreshRecyclerViewAdapter(View headerView, RefreshRecyclerViewAdapter refreshRecyclerViewAdapter, RecyclerView.LayoutManager layoutManager) {\n        refreshRecyclerViewAdapter.setClickTryAgainListener(() -> {\n            if (loadMoreListener != null)\n                loadMoreListener.loadMoreErrorTryAgain();\n        });\n        binding.llContent.addView(headerView, 0);\n        binding.recyclerView.setLayoutManager(layoutManager);\n        binding.recyclerView.setAdapter(refreshRecyclerViewAdapter);\n    }\n\n    public void setItemTouchHelperCallback(ItemTouchHelper.Callback callback) {\n        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);\n        itemTouchHelper.attachToRecyclerView(binding.recyclerView);\n    }\n\n    public void loadMoreError() {\n        binding.rpb.setIsAutoLoading(false);\n        binding.rpb.clean();\n        ((RefreshRecyclerViewAdapter) Objects.requireNonNull(binding.recyclerView.getAdapter())).setLoadMoreError(true, true);\n    }\n\n    public void setNoDataAndRefreshErrorView(View noData, View refreshError) {\n        if (noData != null) {\n            noDataView = noData;\n            noDataView.setVisibility(GONE);\n            addView(noDataView, getChildCount() - 1);\n        }\n        if (refreshError != null) {\n            refreshErrorView = refreshError;\n            addView(refreshErrorView, 2);\n            refreshErrorView.setVisibility(GONE);\n        }\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshRecyclerViewAdapter.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\n\npublic abstract class RefreshRecyclerViewAdapter extends RecyclerView.Adapter {\n    private final int LOAD_MORE_TYPE = 2001;\n\n    private Handler handler;\n    private int isRequesting = 0;   //0是未执行网络请求  1是正在下拉刷新  2是正在加载更多\n    private Boolean needLoadMore;\n    private Boolean isAll = false;  //判断是否还有更多\n    private Boolean loadMoreError = false;\n\n    private OnClickTryAgainListener clickTryAgainListener;\n\n    public RefreshRecyclerViewAdapter(Boolean needLoadMore) {\n        this.needLoadMore = needLoadMore;\n        handler = new Handler();\n    }\n\n    public int getIsRequesting() {\n        return isRequesting;\n    }\n\n    public void setIsRequesting(int isRequesting, Boolean needNotify) {\n        this.isRequesting = isRequesting;\n        if (this.isRequesting == 1) {\n            isAll = false;\n        }\n        if (needNotify) {\n            if (Looper.myLooper() == Looper.getMainLooper()) {\n                notifyItemRangeChanged(getItemCount(), getItemCount() - getICount());\n            } else {\n                handler.post(this::notifyDataSetChanged);\n            }\n        }\n    }\n\n    @NonNull\n    @Override\n    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        if (viewType == LOAD_MORE_TYPE) {\n            return new LoadMoreViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.view_refresh_load_more, parent, false));\n        } else\n            return onCreateIViewHolder(parent, viewType);\n    }\n\n    public abstract RecyclerView.ViewHolder onCreateIViewHolder(ViewGroup parent, int viewType);\n\n    @Override\n    public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {\n        if (holder.getItemViewType() == LOAD_MORE_TYPE) {\n            LoadMoreViewHolder loadHolder = (LoadMoreViewHolder) holder;\n            if (!loadMoreError) {\n                loadHolder.tvLoadMore.setText(\"正在加载...\");\n            } else {\n                loadHolder.tvLoadMore.setText(\"加载失败,点击重试\");\n            }\n            ((LoadMoreViewHolder) holder).llLoadMore.setOnClickListener(v -> {\n                if (null != clickTryAgainListener && loadMoreError) {\n                    clickTryAgainListener.loadMoreErrorTryAgain();\n                    loadMoreError = false;\n                    ((LoadMoreViewHolder) holder).tvLoadMore.setText(\"正在加载...\");\n                }\n            });\n        } else\n            onBindIViewHolder(holder, position);\n    }\n\n    public abstract void onBindIViewHolder(RecyclerView.ViewHolder holder, int position);\n\n    @Override\n    public int getItemViewType(int position) {\n        if (needLoadMore && isRequesting != 1 && !isAll && position == getItemCount() - 1 && getICount() > 0) {\n            return LOAD_MORE_TYPE;\n        } else {\n            return getIViewType(position);\n        }\n    }\n\n    public abstract int getIViewType(int position);\n\n    @Override\n    public int getItemCount() {\n        if (needLoadMore && isRequesting != 1 && !isAll && getICount() > 0) {\n            return getICount() + 1;\n        } else\n            return getICount();\n    }\n\n    public abstract int getICount();\n\n    public void setIsAll(Boolean isAll, Boolean needNotify) {\n        this.isAll = isAll;\n        if (needNotify) {\n            if (Looper.myLooper() == Looper.getMainLooper()) {\n                if (getItemCount() > getICount()) {\n                    notifyItemRangeChanged(getItemCount(), getItemCount() - getICount());\n                } else\n                    notifyItemRemoved(getItemCount() + 1);\n            } else {\n                handler.post(this::notifyDataSetChanged);\n            }\n        }\n    }\n\n    public Boolean canLoadMore() {\n        return needLoadMore && isRequesting == 0 && !isAll && getICount() > 0;\n    }\n\n    public OnClickTryAgainListener getClickTryAgainListener() {\n        return clickTryAgainListener;\n    }\n\n    public void setClickTryAgainListener(OnClickTryAgainListener clickTryAgainListener) {\n        this.clickTryAgainListener = clickTryAgainListener;\n    }\n\n    public Boolean getLoadMoreError() {\n        return loadMoreError;\n    }\n\n    public void setLoadMoreError(Boolean loadMoreError, Boolean needNotify) {\n        this.isRequesting = 0;\n        this.loadMoreError = loadMoreError;\n        if (needNotify) {\n            if (Looper.myLooper() == Looper.getMainLooper()) {\n                notifyDataSetChanged();\n            } else {\n                handler.post(this::notifyDataSetChanged);\n            }\n        }\n    }\n\n    public interface OnClickTryAgainListener {\n        void loadMoreErrorTryAgain();\n    }\n\n    class LoadMoreViewHolder extends RecyclerView.ViewHolder {\n        FrameLayout llLoadMore;\n        TextView tvLoadMore;\n\n        LoadMoreViewHolder(View itemView) {\n            super(itemView);\n            llLoadMore = itemView.findViewById(R.id.ll_loadmore);\n            tvLoadMore = itemView.findViewById(R.id.tv_loadmore);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/refresh/RefreshScrollView.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.refresh;\n\nimport android.annotation.SuppressLint;\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.widget.ScrollView;\n\nimport androidx.annotation.NonNull;\n\npublic class RefreshScrollView extends ScrollView {\n    private RefreshProgressBar rpb;\n    private float durTouchY = -1000000;\n    private BaseRefreshListener baseRefreshListener;\n    private Boolean isRefreshing = false;\n\n    public RefreshScrollView(Context context) {\n        this(context, null);\n    }\n\n    public RefreshScrollView(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public RefreshScrollView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public RefreshScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n    }\n\n    public void setRpb(@NonNull RefreshProgressBar rpb) {\n        this.rpb = rpb;\n        init();\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private void init() {\n        this.setOnTouchListener((View v, MotionEvent event) -> {\n            int action = event.getAction();\n            switch (action) {\n                case MotionEvent.ACTION_DOWN:\n                    durTouchY = event.getY();\n                    break;\n                case MotionEvent.ACTION_MOVE:\n                    if (durTouchY == -1000000)\n                        durTouchY = event.getY();\n                    float dY = event.getY() - durTouchY;  //>0下拉\n                    durTouchY = event.getY();\n                    if (baseRefreshListener != null && !isRefreshing && rpb.getSecondDurProgress() == rpb.getSecondFinalProgress() && getScrollY() <= 0) {\n                        if (rpb.getVisibility() != View.VISIBLE) {\n                            rpb.setVisibility(View.VISIBLE);\n                        }\n                        rpb.setSecondDurProgress((int) (rpb.getSecondDurProgress() + dY));\n                        return rpb.getSecondDurProgress() > 0;\n                    }\n                    break;\n                case MotionEvent.ACTION_UP:\n                    if (baseRefreshListener != null && rpb.getSecondMaxProgress() > 0 && rpb.getSecondDurProgress() > 0) {\n                        if (rpb.getSecondDurProgress() >= rpb.getSecondMaxProgress() && !isRefreshing) {\n                            startRefresh();\n                        } else {\n                            rpb.setSecondDurProgressWithAnim(0);\n                        }\n                    }\n                    durTouchY = -1000000;\n                    break;\n            }\n            return false;\n        });\n    }\n\n    public void setBaseRefreshListener(BaseRefreshListener baseRefreshListener) {\n        this.baseRefreshListener = baseRefreshListener;\n    }\n\n    public void startRefresh() {\n        if (baseRefreshListener != null) {\n            isRefreshing = true;\n            rpb.setIsAutoLoading(true);\n            baseRefreshListener.startRefresh();\n        }\n    }\n\n    public void finishRefresh() {\n        isRefreshing = false;\n        rpb.setDurProgress(0);\n        rpb.setIsAutoLoading(false);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/scroller/FastScrollRecyclerView.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.scroller;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.ViewGroup;\nimport android.view.ViewParent;\n\nimport androidx.annotation.ColorInt;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.kunfei.bookshelf.R;\n\n@SuppressWarnings(\"unused\")\npublic class FastScrollRecyclerView extends RecyclerView {\n\n\n    private FastScroller mFastScroller;\n\n\n    public FastScrollRecyclerView(Context context) {\n\n        super(context);\n\n        layout(context, null);\n\n        setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));\n\n    }\n\n\n    public FastScrollRecyclerView(Context context, AttributeSet attrs) {\n\n        this(context, attrs, 0);\n\n    }\n\n\n    public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {\n\n        super(context, attrs, defStyleAttr);\n\n        layout(context, attrs);\n\n    }\n\n\n    @Override\n\n    public void setAdapter(Adapter adapter) {\n\n        super.setAdapter(adapter);\n\n\n        if (adapter instanceof FastScroller.SectionIndexer) {\n\n            setSectionIndexer((FastScroller.SectionIndexer) adapter);\n\n        } else if (adapter == null) {\n\n            setSectionIndexer(null);\n\n        }\n\n    }\n\n\n    @Override\n\n    public void setVisibility(int visibility) {\n\n        super.setVisibility(visibility);\n\n        mFastScroller.setVisibility(visibility);\n\n    }\n\n\n    /**\n     * Set the {@link FastScroller.SectionIndexer} for the {@link FastScroller}.\n     *\n     * @param sectionIndexer The SectionIndexer that provides section text for the FastScroller\n     */\n\n    public void setSectionIndexer(FastScroller.SectionIndexer sectionIndexer) {\n\n        mFastScroller.setSectionIndexer(sectionIndexer);\n\n    }\n\n\n    /**\n     * Set the enabled state of fast scrolling.\n     *\n     * @param enabled True to enable fast scrolling, false otherwise\n     */\n\n    public void setFastScrollEnabled(boolean enabled) {\n\n        mFastScroller.setEnabled(enabled);\n\n    }\n\n\n    /**\n     * Hide the scrollbar when not scrolling.\n     *\n     * @param hideScrollbar True to hide the scrollbar, false to show\n     */\n\n    public void setHideScrollbar(boolean hideScrollbar) {\n\n        mFastScroller.setFadeScrollbar(hideScrollbar);\n\n    }\n\n\n    /**\n     * Display a scroll track while scrolling.\n     *\n     * @param visible True to show scroll track, false to hide\n     */\n\n    public void setTrackVisible(boolean visible) {\n\n        mFastScroller.setTrackVisible(visible);\n\n    }\n\n\n    /**\n     * Set the color of the scroll track.\n     *\n     * @param color The color for the scroll track\n     */\n\n    public void setTrackColor(@ColorInt int color) {\n\n        mFastScroller.setTrackColor(color);\n\n    }\n\n\n    /**\n     * Set the color for the scroll handle.\n     *\n     * @param color The color for the scroll handle\n     */\n\n    public void setHandleColor(@ColorInt int color) {\n\n        mFastScroller.setHandleColor(color);\n\n    }\n\n\n    /**\n     * Show the section bubble while scrolling.\n     *\n     * @param visible True to show the bubble, false to hide\n     */\n\n    public void setBubbleVisible(boolean visible) {\n\n        mFastScroller.setBubbleVisible(visible);\n\n    }\n\n\n    /**\n     * Set the background color of the index bubble.\n     *\n     * @param color The background color for the index bubble\n     */\n\n    public void setBubbleColor(@ColorInt int color) {\n\n        mFastScroller.setBubbleColor(color);\n\n    }\n\n\n    /**\n     * Set the text color of the index bubble.\n     *\n     * @param color The text color for the index bubble\n     */\n\n    public void setBubbleTextColor(@ColorInt int color) {\n\n        mFastScroller.setBubbleTextColor(color);\n\n    }\n\n\n    /**\n     * Set the fast scroll state change listener.\n     *\n     * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events\n     */\n\n    public void setFastScrollStateChangeListener(FastScrollStateChangeListener fastScrollStateChangeListener) {\n\n        mFastScroller.setFastScrollStateChangeListener(fastScrollStateChangeListener);\n\n    }\n\n\n    @Override\n\n    protected void onAttachedToWindow() {\n\n        super.onAttachedToWindow();\n\n        mFastScroller.attachRecyclerView(this);\n\n\n        ViewParent parent = getParent();\n\n\n        if (parent instanceof ViewGroup) {\n\n            ViewGroup viewGroup = (ViewGroup) parent;\n\n            viewGroup.addView(mFastScroller);\n\n            mFastScroller.setLayoutParams(viewGroup);\n\n        }\n\n    }\n\n\n    @Override\n\n    protected void onDetachedFromWindow() {\n\n        mFastScroller.detachRecyclerView();\n\n        super.onDetachedFromWindow();\n\n    }\n\n\n    private void layout(Context context, AttributeSet attrs) {\n\n        mFastScroller = new FastScroller(context, attrs);\n\n        mFastScroller.setId(R.id.fast_scroller);\n\n    }\n\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/scroller/FastScrollStateChangeListener.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.scroller;\n\npublic interface FastScrollStateChangeListener {\n\n    /**\n     * Called when fast scrolling begins\n     */\n    void onFastScrollStart(FastScroller fastScroller);\n\n    /**\n     * Called when fast scrolling ends\n     */\n    void onFastScrollStop(FastScroller fastScroller);\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/scroller/FastScroller.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.scroller;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Color;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewPropertyAnimator;\nimport android.widget.FrameLayout;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.IdRes;\nimport androidx.annotation.NonNull;\nimport androidx.constraintlayout.widget.ConstraintLayout;\nimport androidx.constraintlayout.widget.ConstraintSet;\nimport androidx.coordinatorlayout.widget.CoordinatorLayout;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.graphics.drawable.DrawableCompat;\nimport androidx.core.view.GravityCompat;\nimport androidx.core.view.ViewCompat;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n\npublic class FastScroller extends LinearLayout {\n    private static final int sBubbleAnimDuration = 100;\n    private static final int sScrollbarAnimDuration = 300;\n    private static final int sScrollbarHideDelay = 1000;\n    private static final int sTrackSnapRange = 5;\n    @ColorInt\n    private int mBubbleColor;\n    @ColorInt\n    private int mHandleColor;\n    private int mBubbleHeight;\n    private int mHandleHeight;\n    private int mViewHeight;\n    private boolean mFadeScrollbar;\n    private boolean mShowBubble;\n    private SectionIndexer mSectionIndexer;\n    private ViewPropertyAnimator mScrollbarAnimator;\n    private ViewPropertyAnimator mBubbleAnimator;\n    private RecyclerView mRecyclerView;\n    private TextView mBubbleView;\n    private ImageView mHandleView;\n    private ImageView mTrackView;\n    private View mScrollbar;\n    private Drawable mBubbleImage;\n    private Drawable mHandleImage;\n    private Drawable mTrackImage;\n    private FastScrollStateChangeListener mFastScrollStateChangeListener;\n    private Runnable mScrollbarHider = this::hideScrollbar;\n\n    private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {\n        @Override\n        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {\n            if (!mHandleView.isSelected() && isEnabled()) {\n                setViewPositions(getScrollProportion(recyclerView));\n            }\n        }\n\n        @Override\n        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {\n            super.onScrollStateChanged(recyclerView, newState);\n            if (isEnabled()) {\n                switch (newState) {\n                    case RecyclerView.SCROLL_STATE_DRAGGING:\n                        getHandler().removeCallbacks(mScrollbarHider);\n                        cancelAnimation(mScrollbarAnimator);\n                        if (!isViewVisible(mScrollbar)) {\n                            showScrollbar();\n                        }\n                        break;\n                    case RecyclerView.SCROLL_STATE_IDLE:\n                        if (mFadeScrollbar && !mHandleView.isSelected()) {\n                            getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay);\n                        }\n                        break;\n                }\n            }\n        }\n    };\n\n    public FastScroller(Context context) {\n        super(context);\n        layout(context, null);\n        setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));\n    }\n\n    public FastScroller(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public FastScroller(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        layout(context, attrs);\n        setLayoutParams(generateLayoutParams(attrs));\n    }\n\n    @Override\n    public void setLayoutParams(@NonNull ViewGroup.LayoutParams params) {\n        params.width = LayoutParams.WRAP_CONTENT;\n        super.setLayoutParams(params);\n    }\n\n    public void setLayoutParams(@NonNull ViewGroup viewGroup) {\n        @IdRes int recyclerViewId = mRecyclerView != null ? mRecyclerView.getId() : NO_ID;\n        int marginTop = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_top);\n        int marginBottom = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_margin_bottom);\n        if (recyclerViewId == NO_ID) {\n            throw new IllegalArgumentException(\"RecyclerView must have a view ID\");\n        }\n        if (viewGroup instanceof ConstraintLayout) {\n            ConstraintSet constraintSet = new ConstraintSet();\n            @IdRes int layoutId = getId();\n            constraintSet.clone((ConstraintLayout) viewGroup);\n            constraintSet.connect(layoutId, ConstraintSet.TOP, recyclerViewId, ConstraintSet.TOP);\n            constraintSet.connect(layoutId, ConstraintSet.BOTTOM, recyclerViewId, ConstraintSet.BOTTOM);\n            constraintSet.connect(layoutId, ConstraintSet.END, recyclerViewId, ConstraintSet.END);\n            constraintSet.applyTo((ConstraintLayout) viewGroup);\n            ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) getLayoutParams();\n            layoutParams.setMargins(0, marginTop, 0, marginBottom);\n            setLayoutParams(layoutParams);\n        } else if (viewGroup instanceof CoordinatorLayout) {\n            CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) getLayoutParams();\n            layoutParams.setAnchorId(recyclerViewId);\n            layoutParams.anchorGravity = GravityCompat.END;\n            layoutParams.setMargins(0, marginTop, 0, marginBottom);\n            setLayoutParams(layoutParams);\n        } else if (viewGroup instanceof FrameLayout) {\n            FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();\n            layoutParams.gravity = GravityCompat.END;\n            layoutParams.setMargins(0, marginTop, 0, marginBottom);\n            setLayoutParams(layoutParams);\n        } else if (viewGroup instanceof RelativeLayout) {\n            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();\n            int endRule = RelativeLayout.ALIGN_END;\n            layoutParams.addRule(RelativeLayout.ALIGN_TOP, recyclerViewId);\n            layoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, recyclerViewId);\n            layoutParams.addRule(endRule, recyclerViewId);\n            layoutParams.setMargins(0, marginTop, 0, marginBottom);\n            setLayoutParams(layoutParams);\n        } else {\n            throw new IllegalArgumentException(\"Parent ViewGroup must be a ConstraintLayout, CoordinatorLayout, FrameLayout, or RelativeLayout\");\n        }\n        updateViewHeights();\n    }\n\n    public void setSectionIndexer(SectionIndexer sectionIndexer) {\n        mSectionIndexer = sectionIndexer;\n    }\n\n    public void attachRecyclerView(RecyclerView recyclerView) {\n        mRecyclerView = recyclerView;\n        if (mRecyclerView != null) {\n            mRecyclerView.addOnScrollListener(mScrollListener);\n            post(() -> {\n                // set initial positions for bubble and handle\n                setViewPositions(getScrollProportion(mRecyclerView));\n            });\n        }\n    }\n\n    public void detachRecyclerView() {\n        if (mRecyclerView != null) {\n            mRecyclerView.removeOnScrollListener(mScrollListener);\n            mRecyclerView = null;\n        }\n    }\n\n    /**\n     * Hide the scrollbar when not scrolling.\n     *\n     * @param fadeScrollbar True to hide the scrollbar, false to show\n     */\n    public void setFadeScrollbar(boolean fadeScrollbar) {\n        mFadeScrollbar = fadeScrollbar;\n        mScrollbar.setVisibility(fadeScrollbar ? GONE : VISIBLE);\n    }\n\n    /**\n     * Show the section bubble while scrolling.\n     *\n     * @param visible True to show the bubble, false to hide\n     */\n    public void setBubbleVisible(boolean visible) {\n        mShowBubble = visible;\n    }\n\n    /**\n     * Display a scroll track while scrolling.\n     *\n     * @param visible True to show scroll track, false to hide\n     */\n    public void setTrackVisible(boolean visible) {\n        mTrackView.setVisibility(visible ? VISIBLE : GONE);\n    }\n\n    /**\n     * Set the color of the scroll track.\n     *\n     * @param color The color for the scroll track\n     */\n    public void setTrackColor(@ColorInt int color) {\n        @ColorInt int trackColor = color;\n        if (mTrackImage == null) {\n            Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_track);\n            if (drawable != null) {\n                mTrackImage = DrawableCompat.wrap(drawable);\n            }\n        }\n        DrawableCompat.setTint(mTrackImage, trackColor);\n        mTrackView.setImageDrawable(mTrackImage);\n    }\n\n    /**\n     * Set the color for the scroll handle.\n     *\n     * @param color The color for the scroll handle\n     */\n    public void setHandleColor(@ColorInt int color) {\n        mHandleColor = color;\n        if (mHandleImage == null) {\n            Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_handle);\n            if (drawable != null) {\n                mHandleImage = DrawableCompat.wrap(drawable);\n            }\n        }\n        DrawableCompat.setTint(mHandleImage, mHandleColor);\n        mHandleView.setImageDrawable(mHandleImage);\n    }\n\n    /**\n     * Set the background color of the index bubble.\n     *\n     * @param color The background color for the index bubble\n     */\n    public void setBubbleColor(@ColorInt int color) {\n        mBubbleColor = color;\n        if (mBubbleImage == null) {\n            Drawable drawable = ContextCompat.getDrawable(getContext(), R.drawable.fastscroll_bubble);\n            if (drawable != null) {\n                mBubbleImage = DrawableCompat.wrap(drawable);\n            }\n        }\n        DrawableCompat.setTint(mBubbleImage, mBubbleColor);\n        mBubbleView.setBackground(mBubbleImage);\n    }\n\n    /**\n     * Set the text color of the index bubble.\n     *\n     * @param color The text color for the index bubble\n     */\n    public void setBubbleTextColor(@ColorInt int color) {\n        mBubbleView.setTextColor(color);\n    }\n\n    /**\n     * Set the fast scroll state change listener.\n     *\n     * @param fastScrollStateChangeListener The interface that will listen to fastscroll state change events\n     */\n    public void setFastScrollStateChangeListener(FastScrollStateChangeListener fastScrollStateChangeListener) {\n        mFastScrollStateChangeListener = fastScrollStateChangeListener;\n    }\n\n    @Override\n    public void setEnabled(boolean enabled) {\n        super.setEnabled(enabled);\n        setVisibility(enabled ? VISIBLE : GONE);\n    }\n\n    @Override\n    @SuppressLint(\"ClickableViewAccessibility\")\n    public boolean onTouchEvent(MotionEvent event) {\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                if (event.getX() < mHandleView.getX() - ViewCompat.getPaddingStart(mHandleView)) {\n                    return false;\n                }\n                requestDisallowInterceptTouchEvent(true);\n                setHandleSelected(true);\n                getHandler().removeCallbacks(mScrollbarHider);\n                cancelAnimation(mScrollbarAnimator);\n                cancelAnimation(mBubbleAnimator);\n                if (!isViewVisible(mScrollbar)) {\n                    showScrollbar();\n                }\n                if (mShowBubble && mSectionIndexer != null) {\n                    showBubble();\n                }\n                if (mFastScrollStateChangeListener != null) {\n                    mFastScrollStateChangeListener.onFastScrollStart(this);\n                }\n            case MotionEvent.ACTION_MOVE:\n                final float y = event.getY();\n                setViewPositions(y);\n                setRecyclerViewPosition(y);\n                return true;\n            case MotionEvent.ACTION_UP:\n            case MotionEvent.ACTION_CANCEL:\n                requestDisallowInterceptTouchEvent(false);\n                setHandleSelected(false);\n                if (mFadeScrollbar) {\n                    getHandler().postDelayed(mScrollbarHider, sScrollbarHideDelay);\n                }\n                hideBubble();\n                if (mFastScrollStateChangeListener != null) {\n                    mFastScrollStateChangeListener.onFastScrollStop(this);\n                }\n                return true;\n        }\n        return super.onTouchEvent(event);\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n        super.onSizeChanged(w, h, oldw, oldh);\n        mViewHeight = h;\n    }\n\n    private void setRecyclerViewPosition(float y) {\n        if (mRecyclerView != null && mRecyclerView.getAdapter() != null) {\n            int itemCount = mRecyclerView.getAdapter().getItemCount();\n            float proportion;\n            if (mHandleView.getY() == 0) {\n                proportion = 0f;\n            } else if (mHandleView.getY() + mHandleHeight >= mViewHeight - sTrackSnapRange) {\n                proportion = 1f;\n            } else {\n                proportion = y / (float) mViewHeight;\n            }\n            int scrolledItemCount = Math.round(proportion * itemCount);\n            if (isLayoutReversed(mRecyclerView.getLayoutManager())) {\n                scrolledItemCount = itemCount - scrolledItemCount;\n            }\n            int targetPos = getValueInRange(0, itemCount - 1, scrolledItemCount);\n            mRecyclerView.getLayoutManager().scrollToPosition(targetPos);\n            if (mShowBubble && mSectionIndexer != null) {\n                mBubbleView.setText(mSectionIndexer.getSectionText(targetPos));\n            }\n        }\n    }\n\n    private float getScrollProportion(RecyclerView recyclerView) {\n        if (recyclerView == null) {\n            return 0;\n        }\n        final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();\n        final int verticalScrollRange = recyclerView.computeVerticalScrollRange();\n        final float rangeDiff = verticalScrollRange - mViewHeight;\n        float proportion = (float) verticalScrollOffset / (rangeDiff > 0 ? rangeDiff : 1f);\n        return mViewHeight * proportion;\n    }\n\n    @SuppressWarnings(\"SameParameterValue\")\n    private int getValueInRange(int min, int max, int value) {\n        int minimum = Math.max(min, value);\n        return Math.min(minimum, max);\n    }\n\n    private void setViewPositions(float y) {\n        mBubbleHeight = mBubbleView.getHeight();\n        mHandleHeight = mHandleView.getHeight();\n        int bubbleY = getValueInRange(0, mViewHeight - mBubbleHeight - mHandleHeight / 2, (int) (y - mBubbleHeight));\n        int handleY = getValueInRange(0, mViewHeight - mHandleHeight, (int) (y - mHandleHeight / 2));\n        if (mShowBubble) {\n            mBubbleView.setY(bubbleY);\n        }\n        mHandleView.setY(handleY);\n    }\n\n    private void updateViewHeights() {\n        int measureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);\n        mBubbleView.measure(measureSpec, measureSpec);\n        mBubbleHeight = mBubbleView.getMeasuredHeight();\n        mHandleView.measure(measureSpec, measureSpec);\n        mHandleHeight = mHandleView.getMeasuredHeight();\n    }\n\n    private boolean isLayoutReversed(@NonNull final RecyclerView.LayoutManager layoutManager) {\n        if (layoutManager instanceof LinearLayoutManager) {\n            return ((LinearLayoutManager) layoutManager).getReverseLayout();\n        } else if (layoutManager instanceof StaggeredGridLayoutManager) {\n            return ((StaggeredGridLayoutManager) layoutManager).getReverseLayout();\n        }\n        return false;\n    }\n\n    private boolean isViewVisible(View view) {\n        return view != null && view.getVisibility() == VISIBLE;\n    }\n\n    private void cancelAnimation(ViewPropertyAnimator animator) {\n        if (animator != null) {\n            animator.cancel();\n        }\n    }\n\n    private void showBubble() {\n        if (!isViewVisible(mBubbleView)) {\n            mBubbleView.setVisibility(VISIBLE);\n            mBubbleAnimator = mBubbleView.animate().alpha(1f)\n                    .setDuration(sBubbleAnimDuration)\n                    .setListener(new AnimatorListenerAdapter() {\n                        // adapter required for new alpha value to stick\n                    });\n        }\n    }\n\n    private void hideBubble() {\n        if (isViewVisible(mBubbleView)) {\n            mBubbleAnimator = mBubbleView.animate().alpha(0f)\n                    .setDuration(sBubbleAnimDuration)\n                    .setListener(new AnimatorListenerAdapter() {\n                        @Override\n                        public void onAnimationEnd(Animator animation) {\n                            super.onAnimationEnd(animation);\n                            mBubbleView.setVisibility(GONE);\n                            mBubbleAnimator = null;\n                        }\n\n                        @Override\n                        public void onAnimationCancel(Animator animation) {\n                            super.onAnimationCancel(animation);\n                            mBubbleView.setVisibility(GONE);\n                            mBubbleAnimator = null;\n                        }\n                    });\n        }\n    }\n\n    private void showScrollbar() {\n        if (mRecyclerView.computeVerticalScrollRange() - mViewHeight > 0) {\n            float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end);\n            mScrollbar.setTranslationX(transX);\n            mScrollbar.setVisibility(VISIBLE);\n            mScrollbarAnimator = mScrollbar.animate().translationX(0f).alpha(1f)\n                    .setDuration(sScrollbarAnimDuration)\n                    .setListener(new AnimatorListenerAdapter() {\n                        // adapter required for new alpha value to stick\n                    });\n        }\n    }\n\n    private void hideScrollbar() {\n        float transX = getResources().getDimensionPixelSize(R.dimen.fastscroll_scrollbar_padding_end);\n        mScrollbarAnimator = mScrollbar.animate().translationX(transX).alpha(0f)\n                .setDuration(sScrollbarAnimDuration)\n                .setListener(new AnimatorListenerAdapter() {\n                    @Override\n                    public void onAnimationEnd(Animator animation) {\n                        super.onAnimationEnd(animation);\n                        mScrollbar.setVisibility(GONE);\n                        mScrollbarAnimator = null;\n                    }\n\n                    @Override\n                    public void onAnimationCancel(Animator animation) {\n                        super.onAnimationCancel(animation);\n                        mScrollbar.setVisibility(GONE);\n                        mScrollbarAnimator = null;\n                    }\n                });\n    }\n\n    private void setHandleSelected(boolean selected) {\n        mHandleView.setSelected(selected);\n        DrawableCompat.setTint(mHandleImage, selected ? mBubbleColor : mHandleColor);\n    }\n\n    @SuppressWarnings(\"ConstantConditions\")\n    private void layout(Context context, AttributeSet attrs) {\n        inflate(context, R.layout.view_fastscroller, this);\n        setClipChildren(false);\n        setOrientation(HORIZONTAL);\n        mBubbleView = findViewById(R.id.fastscroll_bubble);\n        mHandleView = findViewById(R.id.fastscroll_handle);\n        mTrackView = findViewById(R.id.fastscroll_track);\n        mScrollbar = findViewById(R.id.fastscroll_scrollbar);\n        @ColorInt int bubbleColor = ColorUtils.adjustAlpha(ThemeStore.accentColor(context), 0.8f);\n        @ColorInt int handleColor = ThemeStore.accentColor(context);\n        @ColorInt int trackColor = context.getResources().getColor(R.color.transparent30);\n        @ColorInt int textColor = ColorUtils.isColorLight(bubbleColor) ? Color.BLACK : Color.WHITE;\n        boolean fadeScrollbar = true;\n        boolean showBubble = false;\n        boolean showTrack = true;\n        if (attrs != null) {\n            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, 0, 0);\n            if (typedArray != null) {\n                try {\n                    bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor);\n                    handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor);\n                    trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor);\n                    textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor);\n                    fadeScrollbar = typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar);\n                    showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble);\n                    showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack);\n                } finally {\n                    typedArray.recycle();\n                }\n            }\n        }\n        setTrackColor(trackColor);\n        setHandleColor(handleColor);\n        setBubbleColor(bubbleColor);\n        setBubbleTextColor(textColor);\n        setFadeScrollbar(fadeScrollbar);\n        setBubbleVisible(showBubble);\n        setTrackVisible(showTrack);\n    }\n\n    public interface SectionIndexer {\n        String getSectionText(int position);\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/sectioned/GridSpacingItemDecoration.java",
    "content": "package com.kunfei.bookshelf.widget.recycler.sectioned;\n\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.view.View;\n\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.LinearLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * Created by wsw on 2017/12/12.\n */\n\npublic class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {\n\n    private int space;\n    private int color = -1;\n    private Drawable mDivider;\n    private Paint mPaint;\n    private int type;\n\n    public int getColor() {\n        return color;\n    }\n\n    public void setColor(@ColorRes int color) {\n        this.color = color;\n    }\n\n    public GridSpacingItemDecoration(int space) {\n        this.space = space;\n    }\n\n    public GridSpacingItemDecoration(int space, int color) {\n        this.space = space;\n        this.color = color;\n        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        mPaint.setColor(color);\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setStrokeWidth(space * 2);\n    }\n\n    public GridSpacingItemDecoration(int space, int color, int type) {\n        this.space = space;\n        this.color = color;\n        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        mPaint.setColor(color);\n        mPaint.setStyle(Paint.Style.FILL);\n        mPaint.setStrokeWidth(space * 2);\n        this.type = type;\n    }\n\n    public GridSpacingItemDecoration(int space, Drawable mDivider) {\n        this.space = space;\n        this.mDivider = mDivider;\n    }\n\n    @Override\n    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,\n                               @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {\n        if (parent.getLayoutManager() != null) {\n            if (parent.getLayoutManager() instanceof LinearLayoutManager && !(parent.getLayoutManager() instanceof GridLayoutManager)) {\n                if (((LinearLayoutManager) parent.getLayoutManager()).getOrientation() == LinearLayoutManager.HORIZONTAL) {\n                    outRect.set(space, 0, space, 0);\n                } else {\n                    outRect.set(0, space, 0, space);\n                }\n            } else {\n                outRect.set(space, space, space, space);\n            }\n        }\n\n    }\n\n    @Override\n    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {\n        super.onDraw(c, parent, state);\n        if (parent.getLayoutManager() != null) {\n            if (parent.getLayoutManager() instanceof LinearLayoutManager && !(parent.getLayoutManager() instanceof GridLayoutManager)) {\n                if (((LinearLayoutManager) parent.getLayoutManager()).getOrientation() == LinearLayoutManager.HORIZONTAL) {\n                    drawHorizontal(c, parent);\n                } else {\n                    drawVertical(c, parent);\n                }\n            } else {\n                if (type == 0) {\n                    drawGrideview(c, parent);\n                } else {\n                    drawGrideview1(c, parent);\n                }\n            }\n        }\n    }\n\n    //绘制纵向 item 分割线\n\n    private void drawVertical(Canvas canvas, RecyclerView parent) {\n        final int top = parent.getPaddingTop();\n        final int bottom = parent.getMeasuredHeight() - parent.getPaddingBottom();\n        final int childSize = parent.getChildCount();\n        for (int i = 0; i < childSize; i++) {\n            final View child = parent.getChildAt(i);\n            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();\n            final int left = child.getRight() + layoutParams.rightMargin;\n            final int right = left + space;\n            if (mDivider != null) {\n                mDivider.setBounds(left, top, right, bottom);\n                mDivider.draw(canvas);\n            }\n            if (mPaint != null) {\n                canvas.drawRect(left, top, right, bottom, mPaint);\n            }\n        }\n    }\n\n    //绘制横向 item 分割线\n    private void drawHorizontal(Canvas canvas, RecyclerView parent) {\n        int left = parent.getPaddingLeft();\n        int right = parent.getMeasuredWidth() - parent.getPaddingRight();\n        final int childSize = parent.getChildCount();\n        for (int i = 0; i < childSize; i++) {\n            final View child = parent.getChildAt(i);\n            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();\n            int top = child.getBottom() + layoutParams.bottomMargin;\n            int bottom = top + space;\n            if (mDivider != null) {\n                mDivider.setBounds(left, top, right, bottom);\n                mDivider.draw(canvas);\n            }\n            if (mPaint != null) {\n                canvas.drawRect(left, top, right, bottom, mPaint);\n            }\n\n        }\n    }\n\n    //绘制grideview item 分割线 不是填充满的\n    private void drawGrideview(Canvas canvas, RecyclerView parent) {\n        GridLayoutManager linearLayoutManager = (GridLayoutManager) parent.getLayoutManager();\n        int childSize = parent.getChildCount();\n        assert linearLayoutManager != null;\n        int other = parent.getChildCount() / linearLayoutManager.getSpanCount() - 1;\n        if (other < 1) {\n            other = 1;\n        }\n        other = other * linearLayoutManager.getSpanCount();\n        if (parent.getChildCount() < linearLayoutManager.getSpanCount()) {\n            other = parent.getChildCount();\n        }\n        int top, bottom, left, right, spancount;\n        spancount = linearLayoutManager.getSpanCount() - 1;\n        for (int i = 0; i < childSize; i++) {\n            final View child = parent.getChildAt(i);\n            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();\n            if (i < other) {\n                top = child.getBottom() + layoutParams.bottomMargin;\n                bottom = top + space;\n                left = (layoutParams.leftMargin + space) * (i + 1);\n                right = child.getMeasuredWidth() * (i + 1) + left + space * i;\n                if (mDivider != null) {\n                    mDivider.setBounds(left, top, right, bottom);\n                    mDivider.draw(canvas);\n                }\n                if (mPaint != null) {\n                    canvas.drawRect(left, top, right, bottom, mPaint);\n                }\n            }\n            if (i != spancount) {\n                top = (layoutParams.topMargin + space) * (i / linearLayoutManager.getSpanCount() + 1);\n                bottom = (child.getMeasuredHeight() + space) * (i / linearLayoutManager.getSpanCount() + 1) + space;\n                left = child.getRight() + layoutParams.rightMargin;\n                right = left + space;\n                if (mDivider != null) {\n                    mDivider.setBounds(left, top, right, bottom);\n                    mDivider.draw(canvas);\n                }\n                if (mPaint != null) {\n                    canvas.drawRect(left, top, right, bottom, mPaint);\n                }\n            } else {\n                spancount += 4;\n            }\n        }\n    }\n\n    /***/\n    private void drawGrideview1(Canvas canvas, RecyclerView parent) {\n        GridLayoutManager linearLayoutManager = (GridLayoutManager) parent.getLayoutManager();\n        int childSize = parent.getChildCount();\n        int top, bottom, left, right, spancount;\n        spancount = linearLayoutManager.getSpanCount();\n        for (int i = 0; i < childSize; i++) {\n            final View child = parent.getChildAt(i);\n            //画横线\n            RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();\n            top = child.getBottom() + layoutParams.bottomMargin;\n            bottom = top + space;\n            left = layoutParams.leftMargin + child.getPaddingLeft() + space;\n            right = child.getMeasuredWidth() * (i + 1) + left + space * i;\n            if (mDivider != null) {\n                mDivider.setBounds(left, top, right, bottom);\n                mDivider.draw(canvas);\n            }\n            if (mPaint != null) {\n                canvas.drawRect(left, top, right, bottom, mPaint);\n            }\n            //画竖线\n            top = (layoutParams.topMargin + space) * (i / linearLayoutManager.getSpanCount() + 1);\n            bottom = (child.getMeasuredHeight() + space) * (i / linearLayoutManager.getSpanCount() + 1) + space;\n            left = child.getRight() + layoutParams.rightMargin;\n            right = left + space;\n            if (mDivider != null) {\n                mDivider.setBounds(left, top, right, bottom);\n                mDivider.draw(canvas);\n            }\n            if (mPaint != null) {\n                canvas.drawRect(left, top, right, bottom, mPaint);\n            }\n\n            //画上缺失的线框\n            if (i < spancount) {\n                top = child.getTop() + layoutParams.topMargin;\n                bottom = top + space;\n                left = (layoutParams.leftMargin + space) * (i + 1);\n                right = child.getMeasuredWidth() * (i + 1) + left + space * i;\n                if (mDivider != null) {\n                    mDivider.setBounds(left, top, right, bottom);\n                    mDivider.draw(canvas);\n                }\n                if (mPaint != null) {\n                    canvas.drawRect(left, top, right, bottom, mPaint);\n                }\n            }\n            if (i % spancount == 0) {\n                top = (layoutParams.topMargin + space) * (i / linearLayoutManager.getSpanCount() + 1);\n                bottom = (child.getMeasuredHeight() + space) * (i / linearLayoutManager.getSpanCount() + 1) + space;\n                left = child.getLeft() + layoutParams.leftMargin;\n                right = left + space;\n                if (mDivider != null) {\n                    mDivider.setBounds(left, top, right, bottom);\n                    mDivider.draw(canvas);\n                }\n                if (mPaint != null) {\n                    canvas.drawRect(left, top, right, bottom, mPaint);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/sectioned/SectionedRecyclerViewAdapter.java",
    "content": "/*\n * Copyright (C) 2015 Tomás Ruiz-López.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.kunfei.bookshelf.widget.recycler.sectioned;\n\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\n\n/**\n * An extension to RecyclerView.Adapter to provide sections with headers and footers to a\n * RecyclerView. Each section can have an arbitrary number of items.\n *\n * @param <H>  Class extending RecyclerView.ViewHolder to hold and bind the header view\n * @param <VH> Class extending RecyclerView.ViewHolder to hold and bind the items view\n * @param <F>  Class extending RecyclerView.ViewHolder to hold and bind the footer view\n */\n@SuppressWarnings(\"unused\")\npublic abstract class SectionedRecyclerViewAdapter<H extends RecyclerView.ViewHolder,\n        VH extends RecyclerView.ViewHolder,\n        F extends RecyclerView.ViewHolder>\n        extends RecyclerView.Adapter<RecyclerView.ViewHolder> {\n\n    protected static final int TYPE_SECTION_HEADER = -1;\n    protected static final int TYPE_SECTION_FOOTER = -2;\n    protected static final int TYPE_ITEM = -3;\n\n    private int[] sectionForPosition = null;\n    private int[] positionWithinSection = null;\n    private boolean[] isHeader = null;\n    private boolean[] isFooter = null;\n    private int count = 0;\n\n    public SectionedRecyclerViewAdapter() {\n        super();\n        registerAdapterDataObserver(new SectionDataObserver());\n    }\n\n    @Override\n    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {\n        super.onAttachedToRecyclerView(recyclerView);\n        setupIndices();\n    }\n\n    /**\n     * Returns the sum of number of items for each section plus headers and footers if they\n     * are provided.\n     */\n    @Override\n    public int getItemCount() {\n        return count;\n    }\n\n    private void setupIndices() {\n        count = countItems();\n        allocateAuxiliaryArrays(count);\n        precomputeIndices();\n    }\n\n    private int countItems() {\n        int count = 0;\n        int sections = getSectionCount();\n\n        for (int i = 0; i < sections; i++) {\n            count += 1 + getItemCountForSection(i) + (hasFooterInSection(i) ? 1 : 0);\n        }\n        return count;\n    }\n\n    private void precomputeIndices() {\n        int sections = getSectionCount();\n        int index = 0;\n\n        for (int i = 0; i < sections; i++) {\n            setPrecomputedItem(index, true, false, i, 0);\n            index++;\n\n            for (int j = 0; j < getItemCountForSection(i); j++) {\n                setPrecomputedItem(index, false, false, i, j);\n                index++;\n            }\n\n            if (hasFooterInSection(i)) {\n                setPrecomputedItem(index, false, true, i, 0);\n                index++;\n            }\n        }\n    }\n\n    private void allocateAuxiliaryArrays(int count) {\n        sectionForPosition = new int[count];\n        positionWithinSection = new int[count];\n        isHeader = new boolean[count];\n        isFooter = new boolean[count];\n    }\n\n    private void setPrecomputedItem(int index, boolean isHeader, boolean isFooter, int section, int position) {\n        this.isHeader[index] = isHeader;\n        this.isFooter[index] = isFooter;\n        sectionForPosition[index] = section;\n        positionWithinSection[index] = position;\n    }\n\n    @NonNull\n    @Override\n    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {\n        RecyclerView.ViewHolder viewHolder;\n\n        if (isSectionHeaderViewType(viewType)) {\n            viewHolder = onCreateSectionHeaderViewHolder(parent, viewType);\n        } else if (isSectionFooterViewType(viewType)) {\n            viewHolder = onCreateSectionFooterViewHolder(parent, viewType);\n        } else {\n            viewHolder = onCreateItemViewHolder(parent, viewType);\n        }\n\n        return viewHolder;\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {\n        int section = sectionForPosition[position];\n        int index = positionWithinSection[position];\n\n        if (isSectionHeaderPosition(position)) {\n            onBindSectionHeaderViewHolder((H) holder, section);\n        } else if (isSectionFooterPosition(position)) {\n            onBindSectionFooterViewHolder((F) holder, section);\n        } else {\n            onBindItemViewHolder((VH) holder, section, index);\n        }\n\n    }\n\n    @Override\n    public int getItemViewType(int position) {\n\n        if (sectionForPosition == null) {\n            setupIndices();\n        }\n\n        int section = sectionForPosition[position];\n        int index = positionWithinSection[position];\n\n        if (isSectionHeaderPosition(position)) {\n            return getSectionHeaderViewType(section);\n        } else if (isSectionFooterPosition(position)) {\n            return getSectionFooterViewType(section);\n        } else {\n            return getSectionItemViewType(section, index);\n        }\n\n    }\n\n    protected int getSectionHeaderViewType(int section) {\n        return TYPE_SECTION_HEADER;\n    }\n\n    protected int getSectionFooterViewType(int section) {\n        return TYPE_SECTION_FOOTER;\n    }\n\n    protected int getSectionItemViewType(int section, int position) {\n        return TYPE_ITEM;\n    }\n\n    /**\n     * Returns true if the argument position corresponds to a header\n     */\n    public boolean isSectionHeaderPosition(int position) {\n        if (isHeader == null) {\n            setupIndices();\n        }\n        return isHeader[position];\n    }\n\n    /**\n     * Returns true if the argument position corresponds to a footer\n     */\n    public boolean isSectionFooterPosition(int position) {\n        if (isFooter == null) {\n            setupIndices();\n        }\n        return isFooter[position];\n    }\n\n    protected boolean isSectionHeaderViewType(int viewType) {\n        return viewType == TYPE_SECTION_HEADER;\n    }\n\n    protected boolean isSectionFooterViewType(int viewType) {\n        return viewType == TYPE_SECTION_FOOTER;\n    }\n\n    /**\n     * Returns the number of sections in the RecyclerView\n     */\n    protected abstract int getSectionCount();\n\n    /**\n     * Returns the number of items for a given section\n     */\n    protected abstract int getItemCountForSection(int section);\n\n    /**\n     * Returns true if a given section should have a footer\n     */\n    protected abstract boolean hasFooterInSection(int section);\n\n    /**\n     * Creates a ViewHolder of class H for a Header\n     */\n    protected abstract H onCreateSectionHeaderViewHolder(ViewGroup parent, int viewType);\n\n    /**\n     * Creates a ViewHolder of class F for a Footer\n     */\n    protected abstract F onCreateSectionFooterViewHolder(ViewGroup parent, int viewType);\n\n    /**\n     * Creates a ViewHolder of class VH for an Item\n     */\n    protected abstract VH onCreateItemViewHolder(ViewGroup parent, int viewType);\n\n    /**\n     * Binds data to the header view of a given section\n     */\n    protected abstract void onBindSectionHeaderViewHolder(H holder, int section);\n\n    /**\n     * Binds data to the footer view of a given section\n     */\n    protected abstract void onBindSectionFooterViewHolder(F holder, int section);\n\n    /**\n     * Binds data to the item view for a given position within a section\n     */\n    protected abstract void onBindItemViewHolder(VH holder, int section, int position);\n\n    class SectionDataObserver extends RecyclerView.AdapterDataObserver {\n        @Override\n        public void onChanged() {\n            setupIndices();\n        }\n    }\n\n    public int getItemPosition(int position) {\n        return positionWithinSection[position];\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/recycler/sectioned/SectionedSpanSizeLookup.java",
    "content": "/*\n * Copyright (C) 2015 Tomás Ruiz-López.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.kunfei.bookshelf.widget.recycler.sectioned;\n\n\nimport androidx.recyclerview.widget.GridLayoutManager;\n\n/**\n * A SpanSizeLookup to draw section headers or footer spanning the whole width of the RecyclerView\n * when using a GridLayoutManager\n */\npublic class SectionedSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {\n\n    protected SectionedRecyclerViewAdapter<?, ?, ?> adapter = null;\n    protected GridLayoutManager layoutManager = null;\n\n    public SectionedSpanSizeLookup(SectionedRecyclerViewAdapter<?, ?, ?> adapter, GridLayoutManager layoutManager) {\n        this.adapter = adapter;\n        this.layoutManager = layoutManager;\n    }\n\n    @Override\n    public int getSpanSize(int position) {\n\n        if (adapter.isSectionHeaderPosition(position) || adapter.isSectionFooterPosition(position)) {\n            return layoutManager.getSpanCount();\n        } else {\n            return 1;\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/seekbar/VerticalSeekBar.kt",
    "content": "package com.kunfei.bookshelf.widget.seekbar\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport android.view.KeyEvent\nimport android.view.MotionEvent\nimport android.widget.ProgressBar\nimport androidx.appcompat.widget.AppCompatSeekBar\nimport androidx.core.view.ViewCompat\nimport com.kunfei.bookshelf.R\nimport com.kunfei.bookshelf.utils.theme.ATH\nimport com.kunfei.bookshelf.utils.theme.ThemeStore\nimport java.lang.reflect.InvocationTargetException\nimport java.lang.reflect.Method\n\nclass VerticalSeekBar : AppCompatSeekBar {\n\n    private var mIsDragging: Boolean = false\n    private var mThumb: Drawable? = null\n    private var mMethodSetProgressFromUser: Method? = null\n    private var mRotationAngle = ROTATION_ANGLE_CW_90\n\n    var rotationAngle: Int\n        get() = mRotationAngle\n        set(angle) {\n            require(isValidRotationAngle(angle)) { \"Invalid angle specified :$angle\" }\n\n            if (mRotationAngle == angle) {\n                return\n            }\n\n            mRotationAngle = angle\n\n            if (useViewRotation()) {\n                val wrapper = wrapper\n                wrapper?.applyViewRotation()\n            } else {\n                requestLayout()\n            }\n        }\n\n    private val wrapper: VerticalSeekBarWrapper?\n        get() {\n            val parent = parent\n\n            return if (parent is VerticalSeekBarWrapper) {\n                parent\n            } else {\n                null\n            }\n        }\n\n    constructor(context: Context) : super(context) {\n        initialize(context, null, 0, 0)\n    }\n\n    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {\n        initialize(context, attrs, 0, 0)\n    }\n\n    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(\n            context,\n            attrs,\n            defStyle\n    ) {\n        initialize(context, attrs, defStyle, 0)\n    }\n\n    private fun initialize(\n            context: Context,\n            attrs: AttributeSet?,\n            defStyleAttr: Int,\n            defStyleRes: Int\n    ) {\n        ATH.setTint(this, ThemeStore.accentColor(context))\n        ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR)\n\n        if (attrs != null) {\n            val a = context.obtainStyledAttributes(\n                    attrs,\n                    R.styleable.VerticalSeekBar,\n                    defStyleAttr,\n                    defStyleRes\n            )\n            val rotationAngle = a.getInteger(R.styleable.VerticalSeekBar_seekBarRotation, 0)\n            if (isValidRotationAngle(rotationAngle)) {\n                mRotationAngle = rotationAngle\n            }\n            a.recycle()\n        }\n    }\n\n    override fun setThumb(thumb: Drawable) {\n        mThumb = thumb\n        super.setThumb(thumb)\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onTouchEvent(event: MotionEvent): Boolean {\n        return if (useViewRotation()) {\n            onTouchEventUseViewRotation(event)\n        } else {\n            onTouchEventTraditionalRotation(event)\n        }\n    }\n\n    private fun onTouchEventTraditionalRotation(event: MotionEvent): Boolean {\n        if (!isEnabled) {\n            return false\n        }\n\n        when (event.action) {\n            MotionEvent.ACTION_DOWN -> {\n                isPressed = true\n                onStartTrackingTouch()\n                trackTouchEvent(event)\n                attemptClaimDrag(true)\n                invalidate()\n            }\n\n            MotionEvent.ACTION_MOVE -> if (mIsDragging) {\n                trackTouchEvent(event)\n            }\n\n            MotionEvent.ACTION_UP -> {\n                if (mIsDragging) {\n                    trackTouchEvent(event)\n                    onStopTrackingTouch()\n                    isPressed = false\n                } else {\n                    // Touch up when we never crossed the touch slop threshold\n                    // should\n                    // be interpreted as a tap-seek to that location.\n                    onStartTrackingTouch()\n                    trackTouchEvent(event)\n                    onStopTrackingTouch()\n                    attemptClaimDrag(false)\n                }\n                // ProgressBar doesn't know to repaint the thumb drawable\n                // in its inactive state when the touch stops (because the\n                // value has not apparently changed)\n                invalidate()\n            }\n\n            MotionEvent.ACTION_CANCEL -> {\n                if (mIsDragging) {\n                    onStopTrackingTouch()\n                    isPressed = false\n                }\n                invalidate() // see above explanation\n            }\n        }\n        return true\n    }\n\n    private fun onTouchEventUseViewRotation(event: MotionEvent): Boolean {\n        val handled = super.onTouchEvent(event)\n\n        if (handled) {\n            when (event.action) {\n                MotionEvent.ACTION_DOWN -> attemptClaimDrag(true)\n\n                MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> attemptClaimDrag(false)\n            }\n        }\n\n        return handled\n    }\n\n    private fun trackTouchEvent(event: MotionEvent) {\n        val paddingLeft = super.getPaddingLeft()\n        val paddingRight = super.getPaddingRight()\n        val height = height\n\n        val available = height - paddingLeft - paddingRight\n        val y = event.y.toInt()\n\n        val scale: Float\n        var value = 0f\n\n        when (mRotationAngle) {\n            ROTATION_ANGLE_CW_90 -> value = (y - paddingLeft).toFloat()\n            ROTATION_ANGLE_CW_270 -> value = (height - paddingLeft - y).toFloat()\n        }\n\n        scale = if (value < 0 || available == 0) {\n            0.0f\n        } else if (value > available) {\n            1.0f\n        } else {\n            value / available.toFloat()\n        }\n\n        val max = max\n        val progress = scale * max\n\n        setProgressFromUser(progress.toInt(), true)\n    }\n\n    /**\n     * Tries to claim the user's drag motion, and requests disallowing any\n     * ancestors from stealing events in the drag.\n     */\n    private fun attemptClaimDrag(active: Boolean) {\n        val parent = parent\n        parent?.requestDisallowInterceptTouchEvent(active)\n    }\n\n    /**\n     * This is called when the user has started touching this widget.\n     */\n    private fun onStartTrackingTouch() {\n        mIsDragging = true\n    }\n\n    /**\n     * This is called when the user either releases his touch or the touch is\n     * canceled.\n     */\n    private fun onStopTrackingTouch() {\n        mIsDragging = false\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        if (isEnabled) {\n            val handled: Boolean\n            var direction = 0\n\n            when (keyCode) {\n                KeyEvent.KEYCODE_DPAD_DOWN -> {\n                    direction = if (mRotationAngle == ROTATION_ANGLE_CW_90) 1 else -1\n                    handled = true\n                }\n                KeyEvent.KEYCODE_DPAD_UP -> {\n                    direction = if (mRotationAngle == ROTATION_ANGLE_CW_270) 1 else -1\n                    handled = true\n                }\n                KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT ->\n                    // move view focus to previous/next view\n                    return false\n                else -> handled = false\n            }\n\n            if (handled) {\n                val keyProgressIncrement = keyProgressIncrement\n                var progress = progress\n\n                progress += direction * keyProgressIncrement\n\n                if (progress in 0..max) {\n                    setProgressFromUser(progress, true)\n                }\n\n                return true\n            }\n        }\n\n        return super.onKeyDown(keyCode, event)\n    }\n\n    @Synchronized\n    override fun setProgress(progress: Int) {\n        super.setProgress(progress)\n        if (!useViewRotation()) {\n            refreshThumb()\n        }\n    }\n\n    @Synchronized\n    private fun setProgressFromUser(progress: Int, fromUser: Boolean) {\n        if (mMethodSetProgressFromUser == null) {\n            try {\n                val m: Method = ProgressBar::class.java.getDeclaredMethod(\n                        \"setProgress\",\n                        Int::class.javaPrimitiveType,\n                        Boolean::class.javaPrimitiveType\n                )\n                m.isAccessible = true\n                mMethodSetProgressFromUser = m\n            } catch (e: NoSuchMethodException) {\n            }\n\n        }\n\n        if (mMethodSetProgressFromUser != null) {\n            try {\n                mMethodSetProgressFromUser!!.invoke(this, progress, fromUser)\n            } catch (e: IllegalArgumentException) {\n            } catch (e: IllegalAccessException) {\n            } catch (e: InvocationTargetException) {\n            }\n\n        } else {\n            super.setProgress(progress)\n        }\n        refreshThumb()\n    }\n\n    @Synchronized\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        if (useViewRotation()) {\n            super.onMeasure(widthMeasureSpec, heightMeasureSpec)\n        } else {\n            super.onMeasure(heightMeasureSpec, widthMeasureSpec)\n\n            val lp = layoutParams\n\n            if (isInEditMode && lp != null && lp.height >= 0) {\n                setMeasuredDimension(super.getMeasuredHeight(), lp.height)\n            } else {\n                setMeasuredDimension(super.getMeasuredHeight(), super.getMeasuredWidth())\n            }\n        }\n    }\n\n    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {\n        if (useViewRotation()) {\n            super.onSizeChanged(w, h, oldw, oldh)\n        } else {\n            super.onSizeChanged(h, w, oldh, oldw)\n        }\n    }\n\n    @Synchronized\n    override fun onDraw(canvas: Canvas) {\n        if (!useViewRotation()) {\n            when (mRotationAngle) {\n                ROTATION_ANGLE_CW_90 -> {\n                    canvas.rotate(90f)\n                    canvas.translate(0f, (-super.getWidth()).toFloat())\n                }\n                ROTATION_ANGLE_CW_270 -> {\n                    canvas.rotate(-90f)\n                    canvas.translate((-super.getHeight()).toFloat(), 0f)\n                }\n            }\n        }\n\n        super.onDraw(canvas)\n    }\n\n    // refresh thumb position\n    private fun refreshThumb() {\n        onSizeChanged(super.getWidth(), super.getHeight(), 0, 0)\n    }\n\n    /*package*/\n    internal fun useViewRotation(): Boolean {\n        return !isInEditMode\n    }\n\n    companion object {\n        const val ROTATION_ANGLE_CW_90 = 90\n        const val ROTATION_ANGLE_CW_270 = 270\n\n        private fun isValidRotationAngle(angle: Int): Boolean {\n            return angle == ROTATION_ANGLE_CW_90 || angle == ROTATION_ANGLE_CW_270\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/seekbar/VerticalSeekBarWrapper.kt",
    "content": "package com.kunfei.bookshelf.widget.seekbar\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.Gravity\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\n\nimport androidx.core.view.ViewCompat\nimport kotlin.math.max\n\nclass VerticalSeekBarWrapper @JvmOverloads constructor(\n        context: Context,\n        attrs: AttributeSet? = null,\n        defStyleAttr: Int = 0\n) : FrameLayout(context, attrs, defStyleAttr) {\n\n    private val childSeekBar: VerticalSeekBar?\n        get() {\n            val child = if (childCount > 0) getChildAt(0) else null\n            return if (child is VerticalSeekBar) child else null\n        }\n\n    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {\n        if (useViewRotation()) {\n            onSizeChangedUseViewRotation(w, h, oldw, oldh)\n        } else {\n            onSizeChangedTraditionalRotation(w, h, oldw, oldh)\n        }\n    }\n\n    @SuppressLint(\"RtlHardcoded\")\n    private fun onSizeChangedTraditionalRotation(w: Int, h: Int, oldw: Int, oldh: Int) {\n        val seekBar = childSeekBar\n\n        if (seekBar != null) {\n            val hPadding = paddingLeft + paddingRight\n            val vPadding = paddingTop + paddingBottom\n            val lp = seekBar.layoutParams as LayoutParams\n\n            lp.width = ViewGroup.LayoutParams.WRAP_CONTENT\n            lp.height = max(0, h - vPadding)\n            seekBar.layoutParams = lp\n\n            seekBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)\n\n            val seekBarMeasuredWidth = seekBar.measuredWidth\n            seekBar.measure(\n                    MeasureSpec.makeMeasureSpec(\n                            max(0, w - hPadding),\n                            MeasureSpec.AT_MOST\n                    ),\n                    MeasureSpec.makeMeasureSpec(\n                            max(0, h - vPadding),\n                            MeasureSpec.EXACTLY\n                    )\n            )\n\n            lp.gravity = Gravity.TOP or Gravity.LEFT\n            lp.leftMargin = (max(0, w - hPadding) - seekBarMeasuredWidth) / 2\n            seekBar.layoutParams = lp\n        }\n\n        super.onSizeChanged(w, h, oldw, oldh)\n    }\n\n    private fun onSizeChangedUseViewRotation(w: Int, h: Int, oldw: Int, oldh: Int) {\n        val seekBar = childSeekBar\n\n        if (seekBar != null) {\n            val hPadding = paddingLeft + paddingRight\n            val vPadding = paddingTop + paddingBottom\n            seekBar.measure(\n                    MeasureSpec.makeMeasureSpec(\n                            max(0, h - vPadding),\n                            MeasureSpec.EXACTLY\n                    ),\n                    MeasureSpec.makeMeasureSpec(\n                            max(0, w - hPadding),\n                            MeasureSpec.AT_MOST\n                    )\n            )\n        }\n\n        applyViewRotation(w, h)\n        super.onSizeChanged(w, h, oldw, oldh)\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        val seekBar = childSeekBar\n        val widthMode = MeasureSpec.getMode(widthMeasureSpec)\n        val heightMode = MeasureSpec.getMode(heightMeasureSpec)\n        val widthSize = MeasureSpec.getSize(widthMeasureSpec)\n        val heightSize = MeasureSpec.getSize(heightMeasureSpec)\n\n        if (seekBar != null && widthMode != MeasureSpec.EXACTLY) {\n            val seekBarWidth: Int\n            val seekBarHeight: Int\n            val hPadding = paddingLeft + paddingRight\n            val vPadding = paddingTop + paddingBottom\n            val innerContentWidthMeasureSpec =\n                    MeasureSpec.makeMeasureSpec(max(0, widthSize - hPadding), widthMode)\n            val innerContentHeightMeasureSpec =\n                    MeasureSpec.makeMeasureSpec(max(0, heightSize - vPadding), heightMode)\n\n            if (useViewRotation()) {\n                seekBar.measure(innerContentHeightMeasureSpec, innerContentWidthMeasureSpec)\n                seekBarWidth = seekBar.measuredHeight\n                seekBarHeight = seekBar.measuredWidth\n            } else {\n                seekBar.measure(innerContentWidthMeasureSpec, innerContentHeightMeasureSpec)\n                seekBarWidth = seekBar.measuredWidth\n                seekBarHeight = seekBar.measuredHeight\n            }\n\n            val measuredWidth =\n                    View.resolveSizeAndState(seekBarWidth + hPadding, widthMeasureSpec, 0)\n            val measuredHeight =\n                    View.resolveSizeAndState(seekBarHeight + vPadding, heightMeasureSpec, 0)\n\n            setMeasuredDimension(measuredWidth, measuredHeight)\n        } else {\n            super.onMeasure(widthMeasureSpec, heightMeasureSpec)\n        }\n    }\n\n    /*package*/\n    internal fun applyViewRotation() {\n        applyViewRotation(width, height)\n    }\n\n    private fun applyViewRotation(w: Int, h: Int) {\n        val seekBar = childSeekBar\n\n        if (seekBar != null) {\n            val isLTR = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR\n            val rotationAngle = seekBar.rotationAngle\n            val seekBarMeasuredWidth = seekBar.measuredWidth\n            val seekBarMeasuredHeight = seekBar.measuredHeight\n            val hPadding = paddingLeft + paddingRight\n            val vPadding = paddingTop + paddingBottom\n            val hOffset = (max(0, w - hPadding) - seekBarMeasuredHeight) * 0.5f\n            val lp = seekBar.layoutParams\n\n            lp.width = max(0, h - vPadding)\n            lp.height = ViewGroup.LayoutParams.WRAP_CONTENT\n\n            seekBar.layoutParams = lp\n\n            seekBar.pivotX = (if (isLTR) 0 else max(0, h - vPadding)).toFloat()\n            seekBar.pivotY = 0f\n\n            when (rotationAngle) {\n                VerticalSeekBar.ROTATION_ANGLE_CW_90 -> {\n                    seekBar.rotation = 90f\n                    if (isLTR) {\n                        seekBar.translationX = seekBarMeasuredHeight + hOffset\n                        seekBar.translationY = 0f\n                    } else {\n                        seekBar.translationX = -hOffset\n                        seekBar.translationY = seekBarMeasuredWidth.toFloat()\n                    }\n                }\n                VerticalSeekBar.ROTATION_ANGLE_CW_270 -> {\n                    seekBar.rotation = 270f\n                    if (isLTR) {\n                        seekBar.translationX = hOffset\n                        seekBar.translationY = seekBarMeasuredWidth.toFloat()\n                    } else {\n                        seekBar.translationX = -(seekBarMeasuredHeight + hOffset)\n                        seekBar.translationY = 0f\n                    }\n                }\n            }\n        }\n    }\n\n    private fun useViewRotation(): Boolean {\n        val seekBar = childSeekBar\n        return seekBar?.useViewRotation() ?: false\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEAccentBgTextView.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\nimport com.kunfei.bookshelf.utils.ColorUtils;\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.utils.Selector;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\npublic class ATEAccentBgTextView extends AppCompatTextView {\n    public ATEAccentBgTextView(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATEAccentBgTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATEAccentBgTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        setBackground(Selector.shapeBuild()\n                .setCornerRadius(ScreenUtils.dpToPx(3))\n                .setDefaultBgColor(ThemeStore.accentColor(context))\n                .setPressedBgColor(ColorUtils.darkenColor(ThemeStore.accentColor(context)))\n                .create());\n        setTextColor(Color.WHITE);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEAccentStrokeTextView.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.utils.Selector;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\npublic class ATEAccentStrokeTextView extends AppCompatTextView {\n    public ATEAccentStrokeTextView(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATEAccentStrokeTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATEAccentStrokeTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        setBackground(Selector.shapeBuild()\n                .setCornerRadius(ScreenUtils.dpToPx(3))\n                .setStrokeWidth(ScreenUtils.dpToPx(1))\n                .setDisabledStrokeColor(context.getResources().getColor(R.color.md_grey_500))\n                .setDefaultStrokeColor(ThemeStore.accentColor(context))\n                .setPressedBgColor(context.getResources().getColor(R.color.transparent30))\n                .create());\n        setTextColor(Selector.colorBuild()\n                .setDefaultColor(ThemeStore.accentColor(context))\n                .setDisabledColor(context.getResources().getColor(R.color.md_grey_500))\n                .create());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEAutoCompleteTextView.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\n\nimport androidx.appcompat.widget.AppCompatAutoCompleteTextView;\n\nimport com.kunfei.bookshelf.utils.Selector;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n\npublic class ATEAutoCompleteTextView extends AppCompatAutoCompleteTextView {\n\n    public ATEAutoCompleteTextView(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public ATEAutoCompleteTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public ATEAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    private void init(Context context) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            setBackgroundTintList(Selector.colorBuild()\n                    .setFocusedColor(ThemeStore.accentColor(context))\n                    .setDefaultColor(ThemeStore.textColorPrimary(context))\n                    .create());\n        }\n    }\n\n    @Override\n    public boolean enoughToFilter() {\n        return true;\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        if (event.getAction() == MotionEvent.ACTION_DOWN) {\n            showDropDown();\n        }\n        return super.onTouchEvent(event);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATECheckBox.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatCheckBox;\n\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATECheckBox extends AppCompatCheckBox {\n\n    public ATECheckBox(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATECheckBox(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATECheckBox(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        ATH.setTint(this, ThemeStore.accentColor(context));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEEditText.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatEditText;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATEEditText extends AppCompatEditText {\n\n    public ATEEditText(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATEEditText(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATEEditText(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        //ATH.setTint(this, ThemeStore.accentColor(context));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEPrimaryTextView.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATEPrimaryTextView extends AppCompatTextView {\n\n    public ATEPrimaryTextView(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATEPrimaryTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATEPrimaryTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        setTextColor(ThemeStore.textColorPrimary(context));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEProgressBar.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.widget.ProgressBar;\n\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATEProgressBar extends ProgressBar {\n\n    public ATEProgressBar(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATEProgressBar(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATEProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    public ATEProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {\n        super(context, attrs, defStyleAttr, defStyleRes);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        ATH.setTint(this, ThemeStore.accentColor(context));\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATERadioButton.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatRadioButton;\n\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATERadioButton extends AppCompatRadioButton {\n\n    public ATERadioButton(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATERadioButton(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATERadioButton(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        ATH.setTint(this, ThemeStore.accentColor(context));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATERadioNoButton.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatRadioButton;\n\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.utils.Selector;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATERadioNoButton extends AppCompatRadioButton {\n\n    public ATERadioNoButton(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATERadioNoButton(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATERadioNoButton(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        setBackground(Selector.shapeBuild()\n                .setCornerRadius(ScreenUtils.dpToPx(3))\n                .setStrokeWidth(ScreenUtils.dpToPx(1))\n                .setCheckedBgColor(ThemeStore.accentColor(context))\n                .setCheckedStrokeColor(ThemeStore.accentColor(context))\n                .setDefaultStrokeColor(Color.WHITE)\n                .create());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATESecondaryTextView.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATESecondaryTextView extends AppCompatTextView {\n\n    public ATESecondaryTextView(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATESecondaryTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATESecondaryTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        setTextColor(ThemeStore.textColorSecondary(context));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATESeekBar.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatSeekBar;\n\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATESeekBar extends AppCompatSeekBar {\n\n    public ATESeekBar(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATESeekBar(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATESeekBar(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        ATH.setTint(this, ThemeStore.accentColor(context));\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEStockSwitch.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.widget.Switch;\n\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATEStockSwitch extends Switch {\n\n    public ATEStockSwitch(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATEStockSwitch(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATEStockSwitch(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        ATH.setTint(this, ThemeStore.accentColor(context));\n    }\n\n    @Override\n    public boolean isShown() {\n        return getParent() != null && getVisibility() == View.VISIBLE;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATEStrokeTextView.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\nimport com.kunfei.bookshelf.R;\nimport com.kunfei.bookshelf.utils.ScreenUtils;\nimport com.kunfei.bookshelf.utils.Selector;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\npublic class ATEStrokeTextView extends AppCompatTextView {\n    public ATEStrokeTextView(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATEStrokeTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATEStrokeTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ATEStrokeTextView);\n\n        setBackground(Selector.shapeBuild()\n                .setCornerRadius(a.getDimensionPixelSize(R.styleable.ATEStrokeTextView_cornerRadius, 1))\n                .setStrokeWidth(ScreenUtils.dpToPx(1))\n                .setDisabledStrokeColor(context.getResources().getColor(R.color.md_grey_500))\n                .setDefaultStrokeColor(ThemeStore.textColorSecondary(context))\n                .setSelectedStrokeColor(ThemeStore.accentColor(context))\n                .setPressedBgColor(context.getResources().getColor(R.color.transparent30))\n                .create());\n        setTextColor(Selector.colorBuild()\n                .setDefaultColor(ThemeStore.textColorSecondary(context))\n                .setSelectedColor(ThemeStore.accentColor(context))\n                .setDisabledColor(context.getResources().getColor(R.color.md_grey_500))\n                .create());\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATESwitch.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.widget.Switch;\n\nimport com.kunfei.bookshelf.utils.theme.ATH;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\n/**\n * @author Aidan Follestad (afollestad)\n */\npublic class ATESwitch extends Switch {\n\n    public ATESwitch(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public ATESwitch(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public ATESwitch(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        ATH.setTint(this, ThemeStore.accentColor(context));\n    }\n\n    @Override\n    public boolean isShown() {\n        return getParent() != null && getVisibility() == View.VISIBLE;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/kunfei/bookshelf/widget/views/ATETextInputLayout.java",
    "content": "package com.kunfei.bookshelf.widget.views;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.util.AttributeSet;\n\nimport androidx.annotation.Nullable;\n\nimport com.google.android.material.textfield.TextInputLayout;\nimport com.kunfei.bookshelf.utils.Selector;\nimport com.kunfei.bookshelf.utils.theme.ThemeStore;\n\npublic class ATETextInputLayout extends TextInputLayout {\n    public ATETextInputLayout(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public ATETextInputLayout(Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public ATETextInputLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    private void init(Context context) {\n        setHintTextColor(Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create());\n    }\n\n    @Override\n    public void draw(Canvas canvas) {\n\n        super.draw(canvas);\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/anim/anim_none.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n</set>"
  },
  {
    "path": "app/src/main/res/anim/anim_readbook_bottom_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"150\">\n    <translate\n        android:fromYDelta=\"100%\"\n        android:toYDelta=\"0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/anim_readbook_bottom_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromYDelta=\"0%\"\n        android:toYDelta=\"100%\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/anim_readbook_top_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromYDelta=\"-100%\"\n        android:toYDelta=\"0\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/anim_readbook_top_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"200\">\n    <translate\n        android:fromYDelta=\"0\"\n        android:toYDelta=\"-100%\" />\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_bottom_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"300\">\n\n    <translate\n        android:fromYDelta=\"100%\"\n        android:toYDelta=\"0%\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_bottom_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\"\n    android:duration=\"300\">\n\n    <translate\n        android:fromYDelta=\"0%\"\n        android:toYDelta=\"100%\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\"\n    android:duration=\"300\">\n\n    <scale\n        android:fromXScale=\"120%\"\n        android:toXScale=\"100%\"\n        android:fromYScale=\"120%\"\n        android:toYScale=\"100%\"\n        android:pivotX=\"50%\"\n        android:pivotY=\"50%\"/>\n\n    <alpha\n        android:fromAlpha=\"0\"\n        android:toAlpha=\"1\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_in_bottom_right.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\"\n    android:duration=\"300\">\n\n    <scale\n        android:fromXScale=\"20%\"\n        android:toXScale=\"100%\"\n        android:fromYScale=\"20%\"\n        android:toYScale=\"100%\"\n        android:pivotX=\"100%\"\n        android:pivotY=\"100%\"/>\n\n    <alpha\n        android:fromAlpha=\"0\"\n        android:toAlpha=\"1\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_in_top_right.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\"\n    android:duration=\"300\">\n\n    <scale\n        android:fromXScale=\"20%\"\n        android:toXScale=\"100%\"\n        android:fromYScale=\"20%\"\n        android:toYScale=\"100%\"\n        android:pivotX=\"100%\"\n        android:pivotY=\"0%\"/>\n\n    <alpha\n        android:fromAlpha=\"0\"\n        android:toAlpha=\"1\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\"\n    android:duration=\"300\">\n\n    <scale\n        android:fromXScale=\"100%\"\n        android:toXScale=\"80%\"\n        android:fromYScale=\"100%\"\n        android:toYScale=\"80%\"\n        android:pivotX=\"50%\"\n        android:pivotY=\"50%\"/>\n\n    <alpha\n        android:fromAlpha=\"1\"\n        android:toAlpha=\"0\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_out_bottom_right.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\"\n    android:duration=\"300\">\n\n    <scale\n        android:fromXScale=\"100%\"\n        android:toXScale=\"20%\"\n        android:fromYScale=\"100%\"\n        android:toYScale=\"20%\"\n        android:pivotX=\"100%\"\n        android:pivotY=\"100%\"/>\n\n    <alpha\n        android:fromAlpha=\"1\"\n        android:toAlpha=\"0\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/anim/moprogress_out_top_right.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\"\n    android:duration=\"300\">\n\n    <scale\n        android:fromXScale=\"100%\"\n        android:toXScale=\"20%\"\n        android:fromYScale=\"100%\"\n        android:toYScale=\"20%\"\n        android:pivotX=\"100%\"\n        android:pivotY=\"0%\"/>\n\n    <alpha\n        android:fromAlpha=\"1\"\n        android:toAlpha=\"0\"/>\n</set>"
  },
  {
    "path": "app/src/main/res/color/selector_menu_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/btn_bg_press\" android:state_enabled=\"false\" />\n    <item android:color=\"@color/menu_color_default\" android:state_enabled=\"true\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/bg_chapter_item_divider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"line\">\n    <stroke\n        android:width=\"1dp\"\n        android:color=\"@color/bg_divider_line\"\n        android:dashWidth=\"3dp\"\n        android:dashGap=\"3dp\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/bg_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <stroke android:width=\"1dp\"\n        android:color=\"@color/btn_bg_press\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/bg_ib_pre.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/transparent\" android:state_pressed=\"false\"/>\n    <item android:drawable=\"@color/btn_bg_press\" android:state_pressed=\"true\"/>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/bg_ib_pre_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:drawable=\"@android:color/transparent\"/>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/bg_textfield_search.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\" xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"PrivateResource\">\n    <item android:drawable=\"@drawable/abc_textfield_search_activated_mtrl_alpha\" android:state_enabled=\"true\" android:state_focused=\"true\" />\n    <item android:drawable=\"@drawable/abc_textfield_search_activated_mtrl_alpha\" android:state_activated=\"true\" android:state_enabled=\"true\" />\n    <item android:drawable=\"@drawable/abc_textfield_search_default_mtrl_alpha\" android:state_enabled=\"true\" />\n    <item android:drawable=\"@drawable/abc_textfield_search_default_mtrl_alpha\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/fastscroll_bubble.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2016 L4 Digital LLC. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~      http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:shape=\"rectangle\">\n\n    <tools:solid android:color=\"#777777\" />\n\n    <corners\n        android:topLeftRadius=\"@dimen/fastscroll_bubble_radius\"\n        android:topRightRadius=\"@dimen/fastscroll_bubble_radius\"\n        android:bottomLeftRadius=\"@dimen/fastscroll_bubble_radius\"\n        android:bottomRightRadius=\"0dp\" />\n\n    <size\n        android:height=\"@dimen/fastscroll_bubble_size\"\n        android:width=\"@dimen/fastscroll_bubble_size\" />\n\n    <padding\n        android:left=\"@dimen/fastscroll_bubble_padding\"\n        android:right=\"@dimen/fastscroll_bubble_padding\" />\n\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/fastscroll_handle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2016 L4 Digital LLC. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~      http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:shape=\"rectangle\">\n\n    <tools:solid android:color=\"#555555\" />\n\n    <corners android:radius=\"@dimen/fastscroll_handle_radius\" />\n\n    <size\n        android:height=\"@dimen/fastscroll_handle_height\"\n        android:width=\"@dimen/fastscroll_handle_width\" />\n\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/fastscroll_track.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2016 L4 Digital LLC. All rights reserved.\n  ~\n  ~ Licensed under the Apache License, Version 2.0 (the \"License\");\n  ~ you may not use this file except in compliance with the License.\n  ~ You may obtain a copy of the License at\n  ~\n  ~      http://www.apache.org/licenses/LICENSE-2.0\n  ~\n  ~ Unless required by applicable law or agreed to in writing, software\n  ~ distributed under the License is distributed on an \"AS IS\" BASIS,\n  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  ~ See the License for the specific language governing permissions and\n  ~ limitations under the License.\n  -->\n\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:shape=\"rectangle\">\n\n    <tools:solid android:color=\"#CCCCCC\" />\n\n    <size android:width=\"@dimen/fastscroll_track_width\" />\n\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_about.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M7.699,18.758 L7.699,13.219 L4,9.742 L20,5.242 L13.332,18.574 L10.785,16.125 L7.699,18.758 Z M9.455,13.035 L12.888,16.267 L16.987,8.288 L9.455,13.035 Z M9.035,14.477 L9.035,15.887 L9.81,15.209 L9.035,14.477 Z M6.655,10.398 L8.449,12.086 L14.544,8.248 L6.655,10.398 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_add.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12.822,20 L11.177,20 L11.177,12.821 L4,12.821 L4,11.178 L11.178,11.178 L11.178,4 L12.823,4 L12.823,11.179 L20,11.179 L20,12.822 L12.822,12.822 L12.822,20 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_add_online.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.689,20 C5.164,20,4.74,19.844,4.432,19.537 C3.242,18.345,4.772,15.546,5.863,13.892 C5.683,13.308,5.595,12.688,5.595,12 C5.595,8.469,8.467,5.596,11.998,5.596 C12.684,5.596,13.308,5.685,13.898,5.868 C15.013,5.131,16.948,4,18.339,4 C18.84,4,19.242,4.146,19.531,4.435 C20.308,5.212,20.118,6.648,18.965,8.697 C18.724,9.127,18.433,9.588,18.099,10.076 C18.296,10.703,18.396,11.345,18.396,11.986 C18.396,15.524,15.526,18.403,11.998,18.403 C11.349,18.403,10.7,18.304,10.067,18.103 C8.774,18.991,7.013,20,5.689,20 Z M6.51,15.317 C5.331,17.292,5.196,18.449,5.357,18.611 C5.418,18.673,5.543,18.706,5.709,18.706 C6.337,18.706,7.422,18.249,8.669,17.469 C7.791,16.935,7.046,16.193,6.51,15.317 Z M11.473,17.068 C11.651,17.087,11.826,17.096,11.998,17.096 C14.806,17.096,17.089,14.803,17.089,11.985 C17.089,11.82,17.081,11.651,17.063,11.482 C16.282,12.473,15.398,13.461,14.428,14.43 C13.48,15.38,12.47,16.28,11.473,17.068 Z M11.998,6.901 C9.188,6.901,6.902,9.19,6.902,12 C6.902,14.017,8.066,15.818,9.885,16.641 C11.084,15.764,12.301,14.711,13.504,13.508 C14.686,12.326,15.765,11.076,16.635,9.883 C15.811,8.068,14.011,6.901,11.998,6.901 Z M15.317,6.516 C16.19,7.051,16.929,7.791,17.461,8.666 C17.598,8.448,17.72,8.247,17.827,8.055 C18.848,6.24,18.729,5.482,18.607,5.36 C18.605,5.357,18.549,5.304,18.335,5.304 C18.084,5.304,17.163,5.396,15.317,6.516 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrange.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12.167,4h-2.025c-0.604,0-1.08,0.529-1.08,1.206v13.588c0,0.677,0.476,1.206,1.08,1.206h2.025 c0.605,0,1.08-0.529,1.08-1.206V5.206C13.247,4.529,12.772,4,12.167,4z M11.792,5.454v9.531h-1.273V5.454H11.792z M10.519,18.546 v-2.106h1.273v2.106H10.519z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M7.104,6.693H5.079c-0.596,0-1.078,0.501-1.078,1.117v11.072C4.001,19.499,4.483,20,5.079,20h2.025 c0.596,0,1.081-0.501,1.081-1.117V7.811C8.186,7.195,7.7,6.693,7.104,6.693z M6.729,8.148v6.837H5.455V8.148H6.729z M5.455,18.546 v-2.106h1.274v2.106H5.455z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M17.976,7.433c-0.098-0.534-0.551-0.921-1.075-0.921l-2.183,0.383 c-0.584,0.108-0.969,0.689-0.858,1.294l2.006,10.89c0.099,0.534,0.55,0.92,1.074,0.92l2.183-0.383 c0.585-0.108,0.969-0.689,0.859-1.294L17.976,7.433z M15.354,8.256l1.25-0.234l1.289,7.004l-1.256,0.209L15.354,8.256z M17.235,18.484L16.9,16.667l1.257-0.209l0.33,1.797L17.235,18.484z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_back.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_drop_down.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M7,10l5,5 5,-5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_drop_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M7,14l5,-5 5,5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_author.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M12,3c-4.963,0 -9,4.037 -9,9s4.037,9 9,9s9,-4.037 9,-9S16.963,3 12,3zM12,13.826c-2.277,0 -4.371,1.177 -5.542,3.092C5.245,15.554 4.581,13.82 4.581,12c0,-4.091 3.328,-7.419 7.419,-7.419S19.419,7.909 19.419,12c0,1.82 -0.664,3.554 -1.877,4.918C16.371,15.003 14.277,13.826 12,13.826zM12,15.407c1.811,0 3.489,1.013 4.33,2.598c-1.27,0.926 -2.762,1.414 -4.33,1.414c-1.568,0 -3.062,-0.488 -4.332,-1.414C8.511,16.42 10.189,15.407 12,15.407z\"\n      android:fillColor=\"#4A4B4A\"/>\n  <path\n      android:pathData=\"M12,6.014c-2.178,0 -3.951,1.746 -3.951,3.893c0,2.149 1.772,3.899 3.951,3.899s3.951,-1.75 3.951,-3.899C15.951,7.76 14.178,6.014 12,6.014zM12,12.225c-1.307,0 -2.37,-1.041 -2.37,-2.318c0,-1.274 1.063,-2.312 2.37,-2.312s2.37,1.037 2.37,2.312C14.37,11.184 13.307,12.225 12,12.225z\"\n      android:fillColor=\"#4A4B4A\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_auto_page.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M19.109,6.212h-2.16l-0.002-0.817c0-0.366-0.164-0.66-0.44-0.788 c-0.133-0.062-0.278-0.091-0.445-0.091c-0.295,0-0.595,0.092-0.885,0.181l-5.121,1.516H4.891c-0.569,0-1.016,0.407-1.016,0.927 v9.907c0,0.52,0.446,0.926,1.016,0.926h5.283c0.693,0.19,4.949,1.359,5.308,1.463c0.115,0.033,0.229,0.05,0.339,0.05 c0.529,0,1.083-0.422,1.098-0.84l0.002-0.673h2.189c0.569,0,1.016-0.406,1.016-0.926V7.139C20.125,6.619,19.679,6.212,19.109,6.212 z M18.687,7.65v8.883h-1.763l0.023-8.883H18.687z M15.516,17.95c-0.723-0.204-2.282-0.65-3.459-0.988 c-0.794-0.228-1.414-0.405-1.485-0.425l-5.258-0.004V7.65h5.225l4.972-1.524L15.516,17.95z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.544 8.915 L 6.366 12.092 L 9.544 15.269 L 10.562 14.252 L 9.12 12.811 L 14.217 12.811 L 14.217 11.372 L 9.12 11.372 L 10.562 9.932 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_auto_page_stop.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M19.109,6.212h-2.16l-0.002-0.817c0-0.366-0.164-0.66-0.44-0.788 c-0.133-0.062-0.278-0.091-0.445-0.091c-0.295,0-0.595,0.092-0.885,0.181l-5.121,1.516H4.891c-0.569,0-1.016,0.407-1.016,0.927 v9.907c0,0.52,0.446,0.926,1.016,0.926h5.283c0.693,0.19,4.949,1.359,5.308,1.463c0.115,0.033,0.229,0.05,0.339,0.05 c0.529,0,1.083-0.422,1.098-0.84l0.002-0.673h2.189c0.569,0,1.016-0.406,1.016-0.926V7.139C20.125,6.619,19.679,6.212,19.109,6.212 z M12.057,16.962c-0.794-0.228-1.414-0.405-1.485-0.425l-5.258-0.004V7.65h5.225l4.972-1.524l0.006,11.824 C14.793,17.746,13.233,17.3,12.057,16.962z M18.687,16.533h-1.763l0.023-8.883h1.739V16.533z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 8.208 9.5 H 9.646 V 14.5 H 8.208 V 9.5 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 11.396 9.5 H 12.834 V 14.5 H 11.396 V 9.5 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_back_last.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"64.063\"\n    android:viewportHeight=\"64\">\n\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M54.054,49.348H24.72c-3.033,0-5.5-2.466-5.5-5.499V18.94l5.12,5.121c0.358,0.358,0.828,0.537,1.296,0.537 c0.469,0,0.938-0.179,1.297-0.537c0.715-0.716,0.715-1.877,0-2.592l-8.25-8.25c-0.716-0.716-1.876-0.716-2.592,0l-8.25,8.25 c-0.715,0.715-0.715,1.876,0,2.592c0.717,0.716,1.877,0.716,2.593,0l5.12-5.121v24.908c0,5.054,4.113,9.166,9.167,9.166h29.333 c1.014,0,1.833-0.819,1.833-1.833S55.067,49.348,54.054,49.348L54.054,49.348z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_backup.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M15.923,11.848 C13.675,11.848,11.846,13.678,11.846,15.924 C11.846,18.172,13.675,20,15.923,20 S20,18.172,20,15.924 C20,13.678,18.171,11.848,15.923,11.848 Z M15.923,18.713 C14.385,18.713,13.133,17.461,13.133,15.924 C13.133,14.385,14.385,13.135,15.923,13.135 C17.461,13.135,18.713,14.385,18.713,15.924 C18.713,17.461,17.461,18.713,15.923,18.713 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M16.567,13.961 L15.279,13.961 L15.279,16.568 L17.886,16.568 L17.886,15.279 L16.567,15.279 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.287,5.623 C5.287,5.439,5.439,5.289,5.625,5.289 L16.412,5.289 C16.599,5.289,16.751,5.439,16.751,5.623 L16.751,11.019 L18.038,11.019 L18.038,5.623 C18.038,4.729,17.309,4,16.412,4 L5.625,4 C4.73,4,4,4.729,4,5.623 L4,18.375 C4,19.273,4.73,20,5.625,20 L11.018,20 L11.018,18.713 L5.625,18.713 C5.439,18.713,5.287,18.561,5.287,18.375 L5.287,18.039 L11.018,18.039 L11.018,16.752 L5.287,16.752 L5.287,5.623 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.96,6.115 L7.247,6.115 L7.247,7.095 L5.96,7.095 L5.96,6.115 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M14.788,6.115 L16.075,6.115 L16.075,7.095 L14.788,7.095 L14.788,6.115 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.96,14.943 L7.247,14.943 L7.247,15.923 L5.96,15.923 L5.96,14.943 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M11.474,11.475 L13.387,9.561 C13.65,9.987,13.809,10.483,13.809,11.02 L15.096,11.02 C15.096,10.127,14.799,9.308,14.311,8.636 L14.908,8.039 L13.997,7.131 L13.4,7.727 C12.728,7.24,11.909,6.944,11.018,6.944 C8.77,6.944,6.941,8.772,6.941,11.02 S8.77,15.096,11.018,15.096 L11.018,13.807 C9.48,13.807,8.228,12.557,8.228,11.02 C8.228,9.479,9.479,8.229,11.018,8.229 C11.554,8.229,12.05,8.388,12.476,8.651 L10.563,10.563 L11.474,11.475 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_label.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M17.63,5.84C17.27,5.33 16.67,5 16,5L5,5.01C3.9,5.01 3,5.9 3,7v10c0,1.1 0.9,1.99 2,1.99L16,19c0.67,0 1.27,-0.33 1.63,-0.84L22,12l-4.37,-6.16z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_book_has.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"680.31\"\n    android:viewportWidth=\"680.31\">\n\n    <path\n        android:fillColor=\"#2C2C2C\"\n        android:pathData=\"M339.865,79.038c-147.014,0-266.518,119.198-266.518,266.519 c0,147.011,119.198,266.519,266.518,266.519c147.013,0,266.52-119.201,266.52-266.519 C606.078,198.542,486.878,79.038,339.865,79.038L339.865,79.038z M339.865,587.929c-133.871,0-242.373-108.505-242.373-242.373 c0-133.872,108.502-242.066,242.373-242.066c133.868,0,242.372,108.5,242.372,242.373 C582.237,479.731,473.432,587.929,339.865,587.929L339.865,587.929z M339.865,587.929\" />\n    <path\n        android:fillColor=\"#2C2C2C\"\n        android:pathData=\"M305.329,491.346c20.782,0,28.118,33.619,28.118,33.619h12.836c0,0,7.337-33.619,28.12-33.619h133.869 c7.95,0,14.673-6.725,14.673-14.671V242.25c0-33.316-33.316-33.316-33.316-33.316H361.261c-10.697,0-21.396,13.449-21.396,20.479 c0-7.03-10.391-20.479-21.088-20.479H190.103c0,0-33.317,0-33.317,33.316v234.426c0,8.253,6.418,14.671,14.67,14.671H305.329z M348.117,236.75c0-6.115,4.89-11.31,11.005-11.31h139.065c3.973,0,7.336,3.361,7.336,7.64l0.607,235.647 c0,4.279-3.36,7.644-7.333,7.644H372.873c-14.06,0-24.756,15.589-24.756,15.589V236.75z M173.902,233.08 c0-4.279,3.364-7.64,7.336-7.64h139.066c6.112,0,11.003,4.891,11.003,11.31v254.903c0,0-11.003-15.589-24.759-15.589H180.627 c-3.974,0-7.337-3.36-7.337-7.639L173.902,233.08z M173.902,233.08\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_book_last.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M12,12m-0.756,0a0.756,0.756 0,1 1,1.512 0a0.756,0.756 0,1 1,-1.512 0\"\n      android:fillColor=\"#4A4B4A\"/>\n  <path\n      android:pathData=\"M7.038,16.963l6.94,-2.985l2.984,-6.939l-6.938,2.985L7.038,16.963zM14.195,9.804l-1.321,3.071l-3.069,1.322l1.321,-3.07L14.195,9.804z\"\n      android:fillColor=\"#4A4B4A\"/>\n  <path\n      android:pathData=\"M12,3c-4.963,0 -9,4.037 -9,9s4.037,9 9,9s9,-4.037 9,-9S16.963,3 12,3zM12,19.419c-4.091,0 -7.419,-3.328 -7.419,-7.419S7.909,4.581 12,4.581S19.419,7.909 19.419,12S16.091,19.419 12,19.419z\"\n      android:fillColor=\"#4A4B4A\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_book_source_manage.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,7.046 L4,7.046 L4,5.522 L20,5.522 L20,7.046 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,10.855 L4,10.855 L4,9.334 L20,9.334 L20,10.855 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,14.666 L4,14.666 L4,13.145 L20,13.145 L20,14.666 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,18.477 L4,18.477 L4,16.954 L20,16.954 L20,18.477 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_bookmark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.938,4v16h1.414l4.648-2.789L16.649,20h1.411V4H5.938z M16.606,18.278l-4.605-2.763l-4.609,2.764 V5.454h9.215V18.278z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_brightness.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,20 C7.59,20,4,16.412,4,12 S7.59,4,12,4 L12.848,4 L12.698,4.835 C12.693,4.862,12.221,7.656,13.793,9.529 C14.782,10.711,16.402,11.306,18.606,11.307 L18.606,11.307 C18.816,11.307,19.032,11.304,19.255,11.29 L20,11.256 L20,12 C20,16.412,16.411,20,12,20 Z M11.215,5.463 C7.955,5.854,5.418,8.635,5.418,12 C5.418,15.631,8.371,18.583,12,18.583 C15.384,18.583,18.18,16.015,18.543,12.727 C15.925,12.713,13.958,11.943,12.698,10.43 C11.305,8.762,11.165,6.641,11.215,5.463 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_bug_report_black_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_cancel.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n  <path\n      android:fillColor=\"#595757\"\n      android:pathData=\"M12,4c-4.412,0-8,3.588-8,8s3.588,8,8,8c4.41,0,8-3.588,8-8S16.41,4,12,4z M12,18.545 c-3.608,0-6.546-2.936-6.546-6.545S8.392,5.455,12,5.455c3.607,0,6.545,2.936,6.545,6.545S15.607,18.545,12,18.545z\" />\n  <path\n      android:fillColor=\"#595757\"\n      android:pathData=\"M 13.426 9.546 L 12.002 10.972 L 10.576 9.546 L 9.547 10.575 L 10.973 12 L 9.547 13.425 L 10.576 14.454 L 12.002 13.028 L 13.426 14.454 L 14.454 13.425 L 13.029 12 L 14.454 10.575 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_chapter_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,7.046H4V5.522h16V7.046z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,10.855H4V9.334h16V10.855z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,14.666H4v-1.521h16V14.666z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,18.477H4v-1.523h16V18.477z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_check.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_check_source.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,6.182h-1.454v1.249C17.067,5.315,14.646,4,12,4c-4.412,0-8,3.589-8,8s3.588,8,8,8 c3.789,0,7.086-2.691,7.84-6.401l-1.426-0.289C17.798,16.344,15.1,18.546,12,18.546c-3.608,0-6.546-2.937-6.546-6.546 S8.392,5.454,12,5.454c2.399,0,4.576,1.32,5.721,3.395h-1.842v1.454H20V6.182z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M11.273,8.363c-1.604,0-2.909,1.305-2.909,2.91c0,1.604,1.305,2.909,2.909,2.909 c0.537,0,1.035-0.157,1.468-0.412l2.14,2.139l1.027-1.029l-2.139-2.139c0.256-0.432,0.412-0.93,0.412-1.468 C14.182,9.668,12.877,8.363,11.273,8.363z M9.818,11.273c0-0.802,0.652-1.455,1.455-1.455c0.801,0,1.453,0.653,1.453,1.455 c0,0.801-0.652,1.454-1.453,1.454C10.471,12.727,9.818,12.074,9.818,11.273z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_clear_all.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M14.434,20 L9.566,20 C7.535,20,5.863,18.665,5.761,16.961 L5.18,7.199 L4,7.199 L4,5.919 L8.402,5.919 L8.644,5.315 C8.957,4.526,9.827,4,10.813,4 L13.188,4 C14.172,4,15.043,4.526,15.355,5.313 L15.598,5.918 L20,5.918 L20,7.198 L18.82,7.198 L18.238,16.96 C18.137,18.665,16.466,20,14.434,20 Z M6.706,7.199 L7.283,16.898 C7.344,17.917,8.347,18.719,9.566,18.719 L14.433,18.719 C15.653,18.719,16.656,17.916,16.716,16.898 L17.293,7.199 L6.706,7.199 Z M10.01,5.919 L13.99,5.919 L13.91,5.718 C13.806,5.457,13.515,5.28,13.187,5.28 L10.812,5.28 C10.484,5.28,10.193,5.457,10.089,5.718 L10.01,5.919 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M9.542,16.509 L9.127,8.83 L10.648,8.773 L11.064,16.449 L9.542,16.509 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M14.457,16.509 L12.936,16.449 L13.352,8.773 L14.873,8.83 L14.457,16.509 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_copy.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 18.303 8.091 L 14.939 8.091 L 14.939 4.727 L 15.848 4.727 L 15.848 7.182 L 18.303 7.182 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M19.03,18.061H6.91V4h8.785l3.335,3.335V18.061z M8.363,16.606h9.213V7.938l-2.483-2.484H8.363 V16.606z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 17.09 20 L 4.969 20 L 4.969 5.939 L 7.636 5.939 L 7.636 7.394 L 6.424 7.394 L 6.424 18.546 L 15.636 18.546 L 15.636 17.333 L 17.09 17.333 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_cursor_left.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,4h8v8c0,4.418-3.582,8-8,8s-8-3.582-8-8S7.582,4,12,4z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_cursor_right.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,4.208H4v8c0,4.418,3.582,8,8,8s8-3.582,8-8S16.418,4.208,12,4.208z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_daytime.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M16.638,8.494l1.585-1.586l-1.132-1.131l-1.585,1.586C15.933,7.687,16.313,8.066,16.638,8.494z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M8.495,7.363L6.909,5.777L5.778,6.909l1.584,1.585C7.687,8.067,8.067,7.687,8.495,7.363z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12.001,6.181c0.271,0,0.537,0.025,0.799,0.062V4h-1.6v2.242 C11.462,6.206,11.728,6.181,12.001,6.181z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M6.181,12c0-0.271,0.025-0.537,0.062-0.8H4v1.6h2.242C6.206,12.538,6.181,12.272,6.181,12z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M15.506,16.638l1.585,1.585l1.131-1.131l-1.584-1.586C16.313,15.934,15.933,16.313,15.506,16.638z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M17.759,11.2c0.035,0.263,0.061,0.528,0.061,0.8c0,0.272-0.025,0.538-0.061,0.8H20v-1.6H17.759z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M7.363,15.506l-1.585,1.586l1.131,1.131l1.585-1.586C8.067,16.313,7.687,15.934,7.363,15.506z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12.001,17.819c-0.273,0-0.539-0.024-0.801-0.062V20h1.6v-2.242 C12.538,17.795,12.272,17.819,12.001,17.819z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,16.6c-2.536,0-4.6-2.063-4.6-4.6S9.464,7.4,12,7.4s4.6,2.063,4.6,4.6S14.536,16.6,12,16.6z M12,9 c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S13.654,9,12,9z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_disclaimer.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M11.999,4c-4.411,0-8,3.588-8,8s3.589,8,8,8c4.41,0,8-3.588,8-8S16.409,4,11.999,4z M11.999,18.546 c-3.609,0-6.547-2.937-6.547-6.546s2.938-6.546,6.547-6.546S18.544,8.391,18.544,12S15.608,18.546,11.999,18.546z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 12.727 10.546 L 11.271 10.546 L 11.271 14.667 L 11.028 14.667 L 11.028 16.121 L 12.969 16.121 L 12.969 14.667 L 12.727 14.667 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 11.271 8.121 H 12.727 V 9.091 H 11.271 V 8.121 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_donate.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M15.045,19.68 L8.955,19.68 C7.274,19.68,5.854,18.367,5.726,16.696 L5.322,11.43 L4,11.43 L4,7.492 L6.722,7.492 C6.603,7.211,6.541,6.913,6.541,6.602 C6.541,5.344,7.567,4.321,8.826,4.321 C9.935,4.321,10.678,4.755,11.391,5.82 C11.481,5.956,11.683,6.285,12.001,6.82 C12.319,6.285,12.521,5.956,12.609,5.821 C13.322,4.755,14.066,4.321,15.175,4.321 C16.434,4.321,17.46,5.344,17.46,6.602 C17.46,6.914,17.397,7.211,17.279,7.493 L20,7.493 L20,11.431 L18.678,11.431 L18.273,16.697 C18.145,18.367,16.726,19.68,15.045,19.68 Z M12.698,18.287 L15.045,18.287 C16.002,18.287,16.808,17.539,16.88,16.586 L17.276,11.43 L12.698,11.43 L12.698,18.287 Z M6.723,11.43 L7.12,16.586 C7.191,17.539,7.998,18.287,8.955,18.287 L11.301,18.287 L11.301,11.43 L6.723,11.43 Z M12.698,10.033 L18.603,10.033 L18.603,8.89 L12.698,8.89 L12.698,10.033 Z M5.397,10.033 L11.301,10.033 L11.301,8.89 L5.397,8.89 L5.397,10.033 Z M13.224,7.492 L15.175,7.492 C15.665,7.492,16.065,7.094,16.065,6.601 C16.065,6.114,15.666,5.712,15.175,5.712 C14.609,5.712,14.272,5.84,13.772,6.593 C13.683,6.727,13.454,7.105,13.224,7.492 Z M8.825,5.713 C8.335,5.713,7.935,6.114,7.935,6.602 C7.935,7.094,8.334,7.493,8.825,7.493 L10.776,7.493 C10.546,7.106,10.318,6.727,10.23,6.597 C9.728,5.841,9.392,5.713,8.825,5.713 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_download.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_download_line.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M16.712,20 L7.288,20 C5.474,20,4,18.633,4,16.952 L4,14.287 L5.524,14.287 L5.524,16.952 C5.524,17.792,6.316,18.476,7.288,18.476 L16.713,18.476 C17.685,18.476,18.477,17.791,18.477,16.952 L18.477,14.287 L20,14.287 L20,16.952 C20,18.633,18.526,20,16.712,20 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,15.742 L7.382,11.127 L8.459,10.05 L11.239,12.827 L11.239,4 L12.761,4 L12.761,12.827 L15.541,10.05 L16.618,11.127 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.6,20.009c-0.881,0-1.6-0.789-1.6-1.758V5.749C4,4.78,4.719,3.991,5.6,3.991h10.62 c0.882,0,1.599,0.789,1.599,1.758v1.258h-1.483l-0.076-1.258c0-0.122-0.062-0.172-0.062-0.172L5.6,5.582 c0.002,0.015-0.039,0.069-0.039,0.167v12.502c0,0.107,0.051,0.164,0.063,0.172l10.596-0.005c-0.002-0.014,0.039-0.067,0.039-0.167 l0.016-4.739h1.469l0.075,4.739c0,0.969-0.717,1.758-1.599,1.758H5.6z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 12.549 13.354 L 13.738 12.323 L 13.738 12.323 L 18.967 7.553 L 20 8.646 L 14.658 13.514 L 13.54 14.515 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_exchange.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M17.139,8.073l-1.06,1.059l1.058,1.058L20,7.326l-2.863-2.863L16.079,5.52l1.06,1.06h-3.801 c-1.465,0-2.613,1.423-2.613,3.24v1.869v0.624v0.626v1.243c0,0.946-0.514,1.746-1.121,1.746H4v1.494h5.604 c1.467,0,2.614-1.423,2.614-3.24v-1.869v-0.624V9.819c0-0.946,0.515-1.746,1.12-1.746H17.139z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 16.079 14.867 L 17.139 15.926 L 13.338 15.926 L 13.338 17.42 L 17.139 17.42 L 16.079 18.48 L 17.137 19.537 L 20 16.673 L 17.137 13.81 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 4 6.586 H 9.604 V 8.08 H 4 V 6.586 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_expand_less_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14l-6,-6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_expand_more_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:fillColor=\"#FF000000\"\n      android:pathData=\"M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6 -1.41,-1.41z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_faq.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M4,4v16h16V4H4z M18.546,5.454v0.485H5.454V5.454H18.546z M5.454,18.546V7.394h13.092v11.152H5.454z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 10.729 9.819 L 9.819 10.729 L 9.819 12 L 11.273 12 L 11.273 11.332 L 11.329 11.273 L 12.668 11.273 L 12.728 11.333 L 12.728 11.699 L 11.273 13.153 L 11.273 14.425 L 12.727 14.425 L 12.727 13.756 L 14.182 12.302 L 14.182 10.729 L 13.27 9.819 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 11.273 14.909 H 12.726 V 15.879 H 11.273 V 14.909 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_find_replace.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M8.829,10.126 L7.502,11.451 C7.447,11.162,7.415,10.868,7.415,10.57 C7.415,7.997,9.506,5.907,12.077,5.907 C13.573,5.907,14.99,6.633,15.864,7.851 L16.947,7.071 C15.822,5.508,14.002,4.574,12.077,4.574 C8.772,4.574,6.083,7.263,6.083,10.57 C6.083,10.684,6.107,10.795,6.113,10.909 L5.33,10.126 L4.388,11.068 L7.078,13.757 L9.769,11.069 L8.829,10.126 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20.388,18.483 L16.501,14.596 C17.472,13.531,18.07,12.12,18.07,10.57 C18.07,10.536,18.064,10.503,18.064,10.468 L18.955,11.359 L19.897,10.417 L17.206,7.727 L14.515,10.416 L15.455,11.359 L16.708,10.107 C16.723,10.26,16.737,10.415,16.737,10.57 C16.737,13.138,14.647,15.228,12.076,15.228 C10.742,15.228,9.469,14.656,8.584,13.656 L7.586,14.539 C8.724,15.823,10.36,16.561,12.076,16.561 C13.349,16.561,14.528,16.159,15.5,15.48 L19.446,19.425 L20.388,18.483 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_folder.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#707070\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_format_line_spacing.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M7.451,18.062 L7.451,15.375 L7.451,13.6 L7.451,5.938 L8.736,7.23 L9.871,6.103 L6.651,2.866 L3.433,6.103 L4.567,7.23 L5.852,5.939 L5.852,13.6 L5.852,15.375 L5.852,18.061 L4.567,16.77 L3.433,17.897 L6.651,21.134 L9.871,17.897 L8.736,16.77 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M10.895,4.8 L20,4.8 L20,6.4 L10.895,6.4 L10.895,4.8 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M10.895,11.021 L20,11.021 L20,12.621 L10.895,12.621 L10.895,11.021 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M10.895,17.512 L20,17.512 L20,19.112 L10.895,19.112 L10.895,17.512 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_groups.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 11.221 5.832 L 11.221 6.608 L 11.221 11.094 L 9.57 11.094 L 9.57 12.647 L 11.221 12.647 L 11.221 17.909 L 11.229 17.909 L 11.229 18.686 L 13.939 18.686 L 13.939 17.132 L 12.773 17.132 L 12.773 6.608 L 13.939 6.608 L 13.939 5.055 L 11.221 5.055 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M6.707,7.27H4.414C3.396,7.27,2.6,7.963,2.6,8.848v6.044c0,0.885,0.797,1.578,1.814,1.578h2.293 c1.019,0,1.816-0.693,1.816-1.578V8.848C8.523,7.963,7.726,7.27,6.707,7.27z M6.707,14.917H4.414c-0.131,0-0.215-0.041-0.246-0.041 c-0.01,0-0.014,0.003-0.015,0.012L4.137,8.913C4.15,8.895,4.24,8.823,4.414,8.823h2.293c0.17,0,0.26,0.067,0.263,0.029l0.017,5.975 C6.973,14.845,6.881,14.917,6.707,14.917z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M16.91,8.793h2.805c0.86,0,1.561-0.699,1.561-1.559V4.429c0-0.86-0.7-1.559-1.561-1.559H16.91 c-0.859,0-1.559,0.699-1.559,1.559v2.805C15.352,8.094,16.051,8.793,16.91,8.793z M16.91,4.423l2.812,0.006L19.717,7.24 c0,0,0,0-0.002,0l-2.81-0.006L16.91,4.423z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M19.84,14.947h-2.805c-0.859,0-1.558,0.699-1.558,1.559v2.805c0,0.86,0.698,1.559,1.558,1.559h2.805 c0.861,0,1.561-0.699,1.561-1.559v-2.805C21.4,15.646,20.701,14.947,19.84,14.947z M19.848,19.312c0,0-0.004,0.005-0.008,0.005 l-2.809-0.006l0.004-2.811l2.813,0.006V19.312z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_history.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M12,3c-4.963,0 -9,4.037 -9,9s4.037,9 9,9s9,-4.037 9,-9S16.963,3 12,3zM12,19.419c-4.091,0 -7.419,-3.328 -7.419,-7.419S7.909,4.581 12,4.581S19.419,7.909 19.419,12S16.091,19.419 12,19.419z\"\n      android:fillColor=\"#4A4B4A\"/>\n  <path\n      android:pathData=\"M12.082,12.53l0,-5.545l-1.582,0l0,6.456l4.441,2.559l0.788,-1.372z\"\n      android:fillColor=\"#4a4b4a\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_import.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:viewportHeight=\"48\" android:viewportWidth=\"48\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#373838\"\n        android:pathData=\"M20.4,12.2c0,-0.3 -0.1,-0.5 -0.3,-0.7c-0.4,-0.4 -1.1,-0.4 -1.5,0L6.9,23.3c-0.2,0.2 -0.3,0.5 -0.3,0.7c0,0.1 0,0.3 0.1,0.4c0,0.1 0.1,0.2 0.2,0.3c0,0 0,0 0,0.1l0,0c0,0 0,0 0,0c0,0 0,0 0,0l11.8,11.8c0.2,0.2 0.5,0.3 0.7,0.3c0.3,0 0.5,-0.1 0.7,-0.3c0.2,-0.2 0.3,-0.5 0.3,-0.7s-0.1,-0.5 -0.3,-0.7l-11,-11l11,-11C20.3,12.8 20.4,12.5 20.4,12.2z\"\n        android:strokeColor=\"#39393A\" android:strokeWidth=\"1\"/>\n    <path android:fillColor=\"#373838\"\n        android:pathData=\"M41.1,23.3L41.1,23.3L41.1,23.3L41.1,23.3L29.3,11.5c-0.2,-0.2 -0.5,-0.3 -0.7,-0.3c-0.3,0 -0.5,0.1 -0.7,0.3c-0.2,0.2 -0.3,0.5 -0.3,0.7c0,0.3 0.1,0.5 0.3,0.7l11,11l-11,11c-0.2,0.2 -0.3,0.5 -0.3,0.7c0,0.3 0.1,0.5 0.3,0.7c0.4,0.4 1.1,0.4 1.5,0l11.8,-11.8c0.2,-0.2 0.3,-0.5 0.3,-0.7C41.4,23.7 41.3,23.5 41.1,23.3z\"\n        android:strokeColor=\"#39393A\" android:strokeWidth=\"1\"/>\n    <path android:fillColor=\"#373838\"\n        android:pathData=\"M16.2,24c0,0.7 0.6,1.3 1.3,1.3c0.7,0 1.3,-0.6 1.3,-1.3c0,-0.7 -0.6,-1.3 -1.3,-1.3C16.7,22.7 16.2,23.3 16.2,24z\"\n        android:strokeColor=\"#39393A\" android:strokeWidth=\"1\"/>\n    <path android:fillColor=\"#373838\"\n        android:pathData=\"M24,22.7c-0.7,0 -1.3,0.6 -1.3,1.3c0,0.7 0.6,1.3 1.3,1.3c0.7,0 1.3,-0.6 1.3,-1.3C25.3,23.3 24.7,22.7 24,22.7z\"\n        android:strokeColor=\"#39393A\" android:strokeWidth=\"1\"/>\n    <path android:fillColor=\"#373838\"\n        android:pathData=\"M32.3,24c0,-0.7 -0.6,-1.3 -1.3,-1.3c-0.7,0 -1.3,0.6 -1.3,1.3c0,0.7 0.6,1.3 1.3,1.3C31.7,25.3 32.3,24.7 32.3,24z\"\n        android:strokeColor=\"#39393A\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_interface_setting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M8.348,3.999L4,19.977h1.59l1.244-4.841h4.732l1.321,4.841h1.706L9.96,3.999H8.348z M7.266,13.414 l1.243-4.687c0.263-1.01,0.466-2.026,0.61-3.052c0.174,0.865,0.443,1.97,0.803,3.313l1.184,4.426H7.266z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20.261,19.962l-0.107-0.231c-0.116-0.251-0.195-0.515-0.235-0.783 c-0.027-0.2-0.06-0.721-0.06-2.064v-1.581c0-0.516-0.019-0.905-0.055-1.131c-0.065-0.358-0.176-0.647-0.338-0.883 c-0.167-0.243-0.425-0.445-0.765-0.599c-0.339-0.153-0.763-0.229-1.297-0.229c-0.529,0-1.001,0.088-1.399,0.263 c-0.418,0.18-0.731,0.435-0.961,0.779c-0.212,0.322-0.354,0.74-0.422,1.227c-0.005,0.015-0.009,0.028-0.014,0.045l-0.047,0.208 l1.34,0.181l0.032-0.153c0.096-0.444,0.244-0.766,0.43-0.929c0.187-0.163,0.48-0.242,0.898-0.242c0.444,0,0.766,0.111,0.983,0.34 c0.146,0.155,0.225,0.462,0.225,0.882l-0.001,0.175c-0.362,0.133-0.898,0.251-1.599,0.352c-0.409,0.059-0.707,0.119-0.909,0.187 c-0.284,0.094-0.544,0.235-0.773,0.422c-0.233,0.188-0.424,0.441-0.568,0.753c-0.14,0.311-0.21,0.654-0.21,1.021 c0,0.634,0.19,1.163,0.566,1.574c0.38,0.416,0.926,0.627,1.623,0.627c0.41,0,0.804-0.084,1.17-0.249 c0.269-0.122,0.549-0.329,0.826-0.61c0.042,0.226,0.105,0.423,0.193,0.605l0.048,0.1l0.848-0.032h0.563l-0.01-0.021L20.261,19.962z M18.464,16.601v0.136c0,0.494-0.049,0.869-0.145,1.111c-0.127,0.315-0.317,0.556-0.581,0.733c-0.26,0.176-0.563,0.265-0.904,0.265 c-0.333,0-0.565-0.081-0.733-0.255c-0.163-0.173-0.243-0.386-0.243-0.653c0-0.176,0.038-0.333,0.117-0.473 c0.068-0.134,0.168-0.233,0.302-0.303c0.15-0.077,0.421-0.15,0.803-0.217C17.645,16.848,18.109,16.732,18.464,16.601z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_last_read.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:viewportHeight=\"48\" android:viewportWidth=\"48\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"#39393A\" android:fillType=\"evenOdd\"\n        android:pathData=\"M39.4,11H8.6c-0.7,0 -1.2,-0.5 -1.2,-1.2c0,-0.7 0.5,-1.2 1.2,-1.2h30.8c0.7,0 1.2,0.5 1.2,1.2C40.6,10.4 40.1,11 39.4,11zM8.6,18.1h21.3c0.7,0 1.2,0.5 1.2,1.2c0,0.7 -0.5,1.2 -1.2,1.2H8.6c-0.7,0 -1.2,-0.5 -1.2,-1.2C7.4,18.6 7.9,18.1 8.6,18.1zM8.6,27.6h30.8c0.7,0 1.2,0.5 1.2,1.2c0,0.7 -0.5,1.2 -1.2,1.2H8.6c-0.7,0 -1.2,-0.5 -1.2,-1.2C7.4,28.1 7.9,27.6 8.6,27.6zM8.6,37h17.8c0.7,0 1.2,0.5 1.2,1.2c0,0.7 -0.5,1.2 -1.2,1.2H8.6c-0.7,0 -1.2,-0.5 -1.2,-1.2C7.4,37.6 7.9,37 8.6,37z\"\n        android:strokeColor=\"#39393A\" android:strokeWidth=\"1\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launch.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M18.545,18.304c0,0.131-0.11,0.242-0.241,0.242H5.696c-0.131,0-0.242-0.111-0.242-0.242V5.697 c0-0.131,0.111-0.243,0.242-0.243H12V4H5.696C4.76,4,4,4.762,4,5.697v12.606C4,19.239,4.76,20,5.696,20h12.607 C19.239,20,20,19.239,20,18.304v-6.303h-1.455V18.304z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 19.272 4 L 13.843 4 L 13.843 5.454 L 17.517 5.454 L 7.464 15.508 L 8.491 16.536 L 18.545 6.482 L 18.545 10.157 L 20 10.157 L 20 4.727 L 20 4 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.938,4v0.97H4v13.334C4,19.239,4.762,20,5.696,20h12.607C19.239,20,20,19.239,20,18.304V4H5.938z M5.454,18.304V6.424h0.484v11.88C5.938,18.57,5.454,18.57,5.454,18.304z M18.546,18.304c0,0.133-0.108,0.242-0.242,0.242H7.368 c0.013-0.08,0.025-0.159,0.025-0.242V5.454h11.152V18.304z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.089 11.061 H 16.849 V 11.97 H 9.089 V 11.061 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.089 13 H 16.849 V 13.909 H 9.089 V 13 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.089 7.182 H 16.849 V 8.091 H 9.089 V 7.182 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.089 9.12 H 16.849 V 10.03 H 9.089 V 9.12 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.089 14.939 H 16.849 V 15.848 H 9.089 V 14.939 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_mail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M18.648,5.818H5.352C4.594,5.818,4,6.343,4,7.013v9.974c0,0.67,0.594,1.195,1.352,1.195h13.297 c0.758,0,1.352-0.525,1.352-1.195V7.013C20,6.343,19.406,5.818,18.648,5.818z M12,11.117L6.392,7.271l11.214,0.003L12,11.117z M5.454,16.725V8.394l6.134,4.206h0.823l6.136-4.206v8.334L5.454,16.725z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_more_vert.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_network_check.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M15.9,5c-0.17,0 -0.32,0.09 -0.41,0.23l-0.07,0.15 -5.18,11.65c-0.16,0.29 -0.26,0.61 -0.26,0.96 0,1.11 0.9,2.01 2.01,2.01 0.96,0 1.77,-0.68 1.96,-1.59l0.01,-0.03L16.4,5.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM1,9l2,2c2.88,-2.88 6.79,-4.08 10.53,-3.62l1.19,-2.68C9.89,3.84 4.74,5.27 1,9zM21,11l2,-2c-1.64,-1.64 -3.55,-2.82 -5.59,-3.57l-0.53,2.82c1.5,0.62 2.9,1.53 4.12,2.75zM17,15l2,-2c-0.8,-0.8 -1.7,-1.42 -2.66,-1.89l-0.55,2.92c0.42,0.27 0.83,0.59 1.21,0.97zM5,13l2,2c1.13,-1.13 2.56,-1.79 4.03,-2l1.28,-2.88c-2.63,-0.08 -5.3,0.87 -7.31,2.88z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_pause_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_pause_outline_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 8 6 H 9.6 V 18 H 8 V 6 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 14.4 6 H 16 V 18 H 14.4 V 6 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_play_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M8,5v14l11,-7z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_play_outline_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M6.262,20C6.131,20,6,19.966,5.882,19.897c-0.237-0.137-0.384-0.392-0.384-0.667V5.77 c0-0.275,0.146-0.53,0.384-0.667C6.119,4.965,6.41,4.966,6.648,5.104l11.471,6.73c0.234,0.139,0.379,0.392,0.379,0.665 s-0.145,0.526-0.379,0.665l-11.471,6.73C6.528,19.965,6.396,20,6.262,20z M7.027,7.108v10.783l9.188-5.392L7.027,7.108z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_qq_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M18.19,10.775c-0.191-0.375-0.375-0.729-0.469-0.986c-0.393-4.563-3.752-5.785-5.752-5.785 c0,0-0.002,0-0.003,0L11.762,4C7.92,4,6.315,7.689,5.981,9.73c-2.133,4.808-1.374,6.268-0.494,7.258 c0.016,0.017,0.029,0.032,0.045,0.048C5.701,18.924,6.947,20,8.985,20c1.127,0,2.283-0.318,2.914-0.509 c2.922,0.821,4.848,0.626,5.781-0.544c0.492-0.62,0.549-1.326,0.523-1.756c0.094-0.109,0.211-0.248,0.339-0.397 C20.251,14.763,19.012,12.363,18.19,10.775z M16.541,18.041c-0.328,0.411-1.034,0.498-1.567,0.498c-0.764,0-1.732-0.17-2.877-0.504 l-0.209-0.061l-0.209,0.063c-0.493,0.151-1.648,0.508-2.693,0.508c-1.209,0-1.817-0.429-1.974-1.411l0.906-0.593l-0.795-1.219 l-0.729,0.478c-0.4-0.555-0.76-1.729,0.957-5.565l0.055-0.191c0.006-0.046,0.715-4.589,4.356-4.589l0.267,0.002 c0.65,0,3.921,0.219,4.25,4.537l0.033,0.168c0.121,0.376,0.336,0.795,0.588,1.28c0.875,1.694,1.516,3.195,0.555,4.378l-0.757-0.497 l-0.796,1.219l0.826,0.54l0.01,0.045C16.737,17.133,16.837,17.669,16.541,18.041z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_read.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"402.876dp\"\n    android:height=\"221dp\"\n    android:viewportWidth=\"402.876\"\n    android:viewportHeight=\"221\">\n\n    <path\n        android:fillColor=\"#B5B5B6\"\n        android:pathData=\"M47.681,49.969 L59.064,55.93 L55.269,59.725 L55.269,152.954 L47.681,156.749 C48.036,143.739,48.222,124.228,48.222,98.21 S48.037,56.117,47.681,49.969 Z M58.521,30.997 C68.278,36.418,73.698,40.127,74.782,42.108 C75.866,44.099,76.408,45.454,76.408,46.173 C76.408,47.986,75.773,49.968,74.512,52.136 C73.241,54.304,72.25,55.388,71.531,55.388 C70.447,55.388,69.363,53.761,68.279,50.509 C66.466,45.089,62.858,39.127,57.438,32.622 L58.521,30.997 Z M130.069,115.012 L132.237,115.012 C131.873,126.58,133.499,132.543,137.115,132.899 C135.302,137.235,131.873,139.403,126.816,139.403 L110.555,139.403 C104.407,139.048,101.519,135.796,101.883,129.647 L101.883,105.255 L94.294,105.255 C92.847,117.18,89.772,126.216,85.08,132.356 C80.38,138.505,72.071,143.926,60.147,148.618 L59.604,146.992 C69.716,141.216,76.678,135.067,80.472,128.563 C84.266,122.058,86.52,114.292,87.247,105.255 L79.66,105.255 L79.66,109.049 L72.614,112.301 C72.97,106.526,73.156,100.471,73.156,94.143 C73.156,87.826,72.97,82.135,72.614,77.071 L79.66,81.407 L101.884,81.407 C106.22,72.014,109.108,63.706,110.556,56.473 L120.856,62.977 L115.977,65.687 C111.641,71.471,107.846,76.714,104.594,81.407 L116.519,81.407 L120.313,75.987 L127.36,81.948 L123.565,85.201 C123.565,94.958,123.743,102.546,124.107,107.966 L117.603,110.134 L117.603,105.256 L108.389,105.256 L108.389,127.479 C108.024,132.179,110.556,134.348,115.977,133.984 L124.107,133.984 C126.631,133.984,128.172,132.451,128.714,129.377 C129.255,126.309,129.705,121.516,130.069,115.012 Z M80.203,58.099 C86.707,62.079,90.586,64.874,91.856,66.5 C93.118,68.126,93.753,69.668,93.753,71.107 C93.753,72.92,93.21,74.723,92.126,76.527 C91.042,78.34,90.136,79.237,89.416,79.237 C88.687,79.237,87.968,77.798,87.248,74.901 C85.8,70.564,82.912,65.331,78.575,59.182 L80.203,58.099 Z M79.66,84.658 L79.66,102.002 L117.602,102.002 L117.602,84.658 L79.66,84.658 Z M123.022,183.337 L123.022,181.168 C132.415,181.897,138.284,182.252,140.638,182.252 C142.984,182.252,144.161,180.449,144.161,176.832 L144.161,45.09 L100.798,45.09 C97.902,45.09,94.472,45.632,90.5,46.716 L85.622,41.838 L143.077,41.838 L146.87,36.418 L155.544,42.922 L151.207,46.716 L151.207,179 C151.562,188.029,147.591,193.449,139.282,195.262 C139.283,189.841,133.863,185.86,123.022,183.337 Z\" />\n    <path\n        android:fillColor=\"#B5B5B6\"\n        android:pathData=\"M222.771,138.318 L242.827,125.851 L243.911,127.477 C231.986,137.962,224.033,145.906,220.062,151.327 L213.557,143.197 C214.996,141.392,215.725,138.86,215.725,135.608 L215.725,82.49 L214.099,82.49 C209.324,82.49,182.693,82.493,177.783,82.493 L177.783,79.237 L215.725,79.237 L220.603,74.359 L227.65,81.406 L222.771,85.2 L222.771,138.318 Z M207.595,37.502 L208.679,35.875 C213.735,38.771,217.978,41.66,221.416,44.548 C224.846,47.444,226.294,50.332,225.752,53.221 C225.21,56.117,223.855,58.099,221.687,59.183 S217.893,58.286,216.809,53.221 C215.725,48.529,212.65,43.286,207.595,37.502 Z M301.908,85.742 L308.954,72.191 L247.163,72.191 C244.267,72.191,240.836,72.733,236.864,73.817 L231.986,68.939 L270.47,68.939 L270.47,52.137 L256.919,52.137 C252.218,52.137,247.882,52.679,243.91,53.762 L239.032,48.885 L270.47,48.885 C270.47,42.744,270.283,36.604,269.927,30.455 L281.853,35.875 L277.516,39.67 L277.516,48.885 L297.029,48.885 L302.992,42.922 L311.664,52.137 L277.517,52.137 L277.517,68.94 L307.87,68.94 L313.291,62.977 L323.048,72.734 C317.984,73.098,311.665,77.798,304.077,86.827 L301.908,85.742 Z M288.357,87.368 C287.628,102.908,286.73,113.385,285.646,118.806 L340.197,118.806 L347.243,111.76 L357,122.058 L285.105,122.058 C284.02,124.954,282.572,128.206,280.769,131.815 C291.245,135.067,299.011,138.141,304.076,141.029 C309.132,143.926,312.019,146.72,312.748,149.43 C313.469,152.141,313.375,154.037,312.477,155.121 C311.571,156.205,310.758,156.748,310.039,156.748 C308.227,156.748,306.059,154.935,303.534,151.327 C299.919,146.263,291.966,140.486,279.685,133.982 C270.285,146.991,255.108,155.121,234.155,158.373 L233.614,156.205 C257.463,149.701,272.096,138.318,277.517,122.058 L249.873,122.058 C246.977,122.058,243.547,122.6,239.574,123.684 L234.696,118.806 L278.058,118.806 C279.497,109.778,280.226,96.041,280.226,77.612 L292.693,83.575 L288.357,87.368 Z M242.827,94.957 C251.499,97.853,256.733,100.199,258.545,102.002 C260.35,103.815,261.256,105.619,261.256,107.423 C261.256,108.507,260.799,109.862,259.901,111.488 C258.995,113.114,258.36,113.927,258.005,113.927 C256.921,113.927,255.651,112.3,254.21,109.048 C251.679,104.712,247.706,100.562,242.286,96.581 L242.827,94.957 Z M250.957,80.863 L251.499,79.237 C259.807,81.769,264.779,84.031,266.405,86.013 C268.031,88.003,268.843,89.722,268.843,91.161 C268.843,92.609,268.386,94.057,267.489,95.497 C266.583,96.946,265.947,97.665,265.592,97.665 C264.508,97.665,263.424,96.226,262.34,93.329 C260.171,89.357,256.376,85.2,250.957,80.863 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_read_aloud.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,4c-4.412,0-8,3.559-8,7.933v1.269v2.117v2.874C4,19.189,4.816,20,5.818,20h2.424 c1.002,0,1.818-0.811,1.818-1.808v-4.991c0-0.997-0.816-1.809-1.818-1.809H5.818c-0.116,0-0.229,0.014-0.339,0.034 C5.742,8.091,8.564,5.454,12,5.454s6.258,2.637,6.52,5.973c-0.109-0.021-0.222-0.034-0.338-0.034h-2.424 c-1.002,0-1.818,0.812-1.818,1.809v4.991c0,0.997,0.816,1.808,1.818,1.808h2.424C19.186,20,20,19.189,20,18.192v-2.874v-2.117 v-1.269C20,7.559,16.412,4,12,4z M5.818,12.847h2.424c0.201,0,0.363,0.159,0.363,0.354v4.991c0,0.194-0.162,0.354-0.363,0.354H5.818 c-0.201,0-0.364-0.159-0.364-0.354v-2.874v-2.117C5.454,13.006,5.617,12.847,5.818,12.847z M18.546,18.192 c0,0.194-0.163,0.354-0.364,0.354h-2.424c-0.201,0-0.365-0.159-0.365-0.354v-4.991c0-0.195,0.164-0.354,0.365-0.354h2.424 c0.201,0,0.364,0.159,0.364,0.354v2.117V18.192z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_refresh_black_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M20,10.302 L15.879,10.302 L15.879,8.848 L18.546,8.848 L18.546,6.182 L20,6.182 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,20 C7.588,20,4,16.411,4,12 S7.588,4,12,4 C15.183,4,18.063,5.884,19.336,8.801 L18.003,9.384 C16.961,6.997,14.604,5.454,12,5.454 C8.392,5.454,5.454,8.391,5.454,12 S8.392,18.546,12,18.546 C15.1,18.546,17.798,16.344,18.414,13.31 L19.84,13.6 C19.086,17.309,15.789,20,12,20 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_refresh_white_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_remove.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M19,13H5v-2h14v2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_restore.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M18.807,13.043 C18.035,12.273,17.011,11.848,15.924,11.848 C14.876,11.848,13.893,12.25,13.134,12.966 L13.134,12.281 L11.847,12.281 L11.847,15.095 L12.49,15.095 L13.135,15.095 L14.908,15.095 L14.908,13.806 L14.128,13.806 C14.632,13.378,15.256,13.132,15.924,13.132 C16.668,13.132,17.369,13.425,17.896,13.952 C18.423,14.477,18.713,15.179,18.713,15.925 C18.713,16.667,18.423,17.366,17.896,17.896 C17.37,18.421,16.668,18.71,15.924,18.71 C15.178,18.71,14.479,18.421,13.949,17.896 C13.425,17.367,13.135,16.669,13.135,15.925 L11.848,15.925 C11.848,17.013,12.27,18.038,13.041,18.804 C13.81,19.576,14.834,20,15.924,20 C17.011,20,18.035,19.576,18.807,18.805 C19.576,18.037,20,17.012,20,15.926 C20,14.836,19.576,13.813,18.807,13.043 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.287,5.625 C5.287,5.437,5.439,5.287,5.625,5.287 L16.412,5.287 C16.596,5.287,16.751,5.441,16.751,5.625 L16.751,11.02 L18.038,11.02 L18.038,5.625 C18.038,4.727,17.308,3.998,16.412,3.998 L5.625,3.998 C4.73,3.998,4,4.727,4,5.625 L4,18.377 C4,19.273,4.73,20.002,5.625,20.002 L11.018,20.002 L11.018,18.713 L5.625,18.713 C5.439,18.713,5.287,18.563,5.287,18.377 L5.287,18.039 L11.018,18.039 L11.018,16.754 L5.287,16.754 L5.287,5.625 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.96,6.117 L7.247,6.117 L7.247,7.097 L5.96,7.097 L5.96,6.117 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M14.789,6.117 L16.076,6.117 L16.076,7.097 L14.789,7.097 L14.789,6.117 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.96,14.945 L7.247,14.945 L7.247,15.925 L5.96,15.925 L5.96,14.945 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M11.473,11.475 L13.387,9.562 C13.65,9.988,13.809,10.484,13.809,11.02 L15.097,11.02 C15.097,10.128,14.801,9.309,14.312,8.637 L14.908,8.041 L13.996,7.133 L13.401,7.728 C12.729,7.24,11.91,6.944,11.018,6.944 C8.769,6.944,6.941,8.774,6.941,11.02 C6.941,13.268,8.77,15.096,11.018,15.096 L11.018,13.807 C9.48,13.807,8.228,12.557,8.228,11.02 C8.228,9.481,9.48,8.229,11.018,8.229 C11.554,8.229,12.051,8.388,12.476,8.652 L10.562,10.565 L11.473,11.475 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_save.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M19.2,3.2h-3.417H8.212H4.8c-0.882,0-1.6,0.718-1.6,1.6v14.4c0,0.882,0.718,1.6,1.6,1.6h14.4 c0.882,0,1.6-0.718,1.6-1.6V4.8C20.8,3.918,20.082,3.2,19.2,3.2z M15.798,4.8L15.8,8.993C15.799,8.995,15.794,9,15.783,9H8.197 l0.015-4.2H15.798z M19.199,19.2H4.8V4.8h1.798V9c0,0.882,0.724,1.6,1.614,1.6h7.571c0.891,0,1.614-0.718,1.614-1.6V4.8H19.2 L19.199,19.2z M19.2,20v-0.8h0.001L19.2,20z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 13.291 5.354 H 14.541 V 8.354 H 13.291 V 5.354 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_scan.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M725.333333 128h149.184C886.421333 128 896 137.578667 896 149.482667V298.666667a42.666667 42.666667 0 1 0 85.333333 0V149.482667A106.752 106.752 0 0 0 874.517333 42.666667H725.333333a42.666667 42.666667 0 1 0 0 85.333333z m170.666667 597.333333v149.184c0 11.904-9.578667 21.482667-21.482667 21.482667H725.333333a42.666667 42.666667 0 1 0 0 85.333333h149.184A106.752 106.752 0 0 0 981.333333 874.517333V725.333333a42.666667 42.666667 0 1 0-85.333333 0z m-597.333333 170.666667H149.482667A21.418667 21.418667 0 0 1 128 874.517333V725.333333a42.666667 42.666667 0 1 0-85.333333 0v149.184A106.752 106.752 0 0 0 149.482667 981.333333H298.666667a42.666667 42.666667 0 1 0 0-85.333333zM128 298.666667a42.666667 42.666667 0 1 1-85.333333 0V149.482667A106.752 106.752 0 0 1 149.482667 42.666667H298.666667a42.666667 42.666667 0 1 1 0 85.333333H149.482667C137.578667 128 128 137.578667 128 149.482667V298.666667zM85.333333 554.666667a42.666667 42.666667 0 1 1 0-85.333334h853.333334a42.666667 42.666667 0 1 1 0 85.333334H85.333333z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_scoring.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M16.948,19.437L12,16.838l-4.946,2.599L8,13.932l-4-3.9l5.528-0.808L12,4.219l2.472,5.005L20,10.031 l-4,3.9L16.948,19.437z M12,15.349l3.198,1.679l-0.613-3.557l2.584-2.52l-3.572-0.52L12,7.192l-1.599,3.239l-3.572,0.52l2.588,2.52 l-0.613,3.557L12,15.349z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_search.xml",
    "content": "<vector android:autoMirrored=\"true\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"48\"\n    android:viewportWidth=\"48\"\n    android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#39393A\"\n        android:pathData=\"M20,32.7c-6.9,0 -12.6,-5.6 -12.6,-12.6c0,-6.9 5.6,-12.6 12.6,-12.6c6.9,0 12.6,5.6 12.6,12.6C32.6,27 27,32.7 20,32.7M20,9C13.9,9 8.9,14 8.9,20.1c0,6.1 5,11.1 11.1,11.1c6.1,0 11.1,-5 11.1,-11.1C31.1,14 26.2,9 20,9\"\n        android:strokeColor=\"#39393A\"\n        android:strokeWidth=\"2\" />\n    <path\n        android:fillColor=\"#39393A\"\n        android:pathData=\"M39.8,40.4c-0.2,0 -0.4,-0.1 -0.5,-0.2L28.1,29.1c-0.3,-0.3 -0.3,-0.7 0,-1c0.3,-0.3 0.7,-0.3 1,0l11.1,11.1c0.3,0.3 0.3,0.7 0,1C40.2,40.4 40,40.4 39.8,40.4\"\n        android:strokeColor=\"#39393A\"\n        android:strokeWidth=\"2\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_select_all.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.6,20.009 C4.719,20.009,4,19.22,4,18.252 L4,5.749 C4,4.78,4.719,3.991,5.6,3.991 L16.22,3.991 C17.102,3.991,17.819,4.78,17.819,5.748 L17.819,6.932 L16.336,7.007 L16.259,5.75 C16.259,5.627,16.197,5.578,16.197,5.578 L5.6,5.582 C5.602,5.597,5.561,5.652,5.561,5.749 L5.561,18.252 C5.561,18.359,5.612,18.415,5.624,18.423 L16.22,18.418 C16.218,18.404,16.259,18.351,16.259,18.251 L16.259,13.585 L17.743,13.511 L17.818,18.25 C17.818,19.219,17.101,20.008,16.219,20.008 L5.6,20.008 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M10.592,11.063 L11.66,10.011 L13.738,12.324 L18.967,7.553 L20,8.645 L14.658,13.514 L13.54,14.514 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M10.61,11.047 L11.678,10.029 L13.72,12.34 L18.984,7.571 L19.982,8.662 L14.642,13.496 L13.542,14.479 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M10.63,11.031 L11.694,10.048 L13.699,12.357 L19,7.59 L19.962,8.678 L14.625,13.478 L13.545,14.443 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M19.016,7.608 L13.682,12.373 L11.711,10.066 L10.648,11.015 L12.619,13.321 L13.547,14.408 L14.608,13.459 L19.943,8.694 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12,15.43 C10.109,15.43,8.572,13.891,8.572,12 S10.109,8.57,12,8.57 S15.428,10.109,15.428,12 S13.891,15.43,12,15.43 Z M12,10.096 C10.949,10.096,10.096,10.95,10.096,12 S10.95,13.904,12,13.904 S13.904,13.05,13.904,12 S13.051,10.096,12,10.096 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M13.735,20 L10.261,20 L10.12,18.065 C10.102,17.807,9.944,17.585,9.721,17.492 C9.472,17.387,9.214,17.429,9.024,17.592 L7.568,18.848 L5.113,16.391 L6.369,14.937 C6.531,14.749,6.573,14.49,6.477,14.258 C6.376,14.013,6.166,13.857,5.913,13.836 L4,13.697 L4,10.226 L5.916,10.085 C6.164,10.066,6.378,9.913,6.473,9.683 C6.574,9.438,6.536,9.185,6.371,8.992 L5.113,7.539 L7.573,5.094 L9.026,6.376 C9.222,6.544,9.495,6.605,9.703,6.522 L9.844,6.461 C10.026,6.273,10.107,6.141,10.119,5.974 L10.262,4 L13.734,4 L13.877,5.894 C13.895,6.139,14.041,6.335,14.277,6.43 C14.527,6.537,14.786,6.494,14.975,6.332 L16.427,5.073 L18.886,7.531 L17.629,8.984 C17.467,9.172,17.426,9.431,17.521,9.664 C17.622,9.909,17.833,10.066,18.085,10.085 L20,10.227 L20,13.698 L18.085,13.837 C17.837,13.858,17.623,14.011,17.528,14.242 C17.426,14.487,17.465,14.747,17.63,14.94 L18.886,16.391 L16.429,18.85 L14.975,17.594 C14.799,17.444,14.544,17.398,14.322,17.479 L14.295,17.493 C14.051,17.594,13.894,17.816,13.876,18.068 L13.735,20 Z M11.561,18.604 L12.437,18.604 L12.482,17.963 C12.538,17.216,13.011,16.538,13.691,16.232 L13.737,16.211 C14.526,15.883,15.308,16.04,15.886,16.54 L16.358,16.945 L16.98,16.321 L16.574,15.852 C16.064,15.26,15.933,14.446,16.231,13.725 C16.535,12.993,17.206,12.502,17.982,12.446 L18.604,12.399 L18.604,11.524 L17.984,11.477 C17.207,11.421,16.536,10.938,16.239,10.215 C15.935,9.485,16.065,8.664,16.574,8.07 L16.98,7.601 L16.358,6.979 L15.886,7.386 C15.308,7.885,14.472,8.022,13.761,7.729 C13.029,7.428,12.54,6.763,12.482,5.996 L12.438,5.397 L11.558,5.397 L11.514,6 C11.457,6.773,10.973,7.438,10.252,7.733 L10.091,7.789 C9.416,8.008,8.652,7.856,8.112,7.392 L7.639,6.982 L7.02,7.602 L7.426,8.075 C7.936,8.665,8.067,9.479,7.77,10.202 C7.467,10.931,6.796,11.422,6.02,11.477 L5.397,11.524 L5.397,12.399 L6.019,12.446 C6.796,12.503,7.465,12.987,7.762,13.71 C8.066,14.44,7.935,15.261,7.426,15.852 L7.02,16.32 L7.64,16.941 L8.112,16.535 C8.692,16.035,9.528,15.898,10.239,16.194 C10.958,16.488,11.458,17.181,11.515,17.961 L11.561,18.604 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_share.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:pathData=\"M9.866,9.9c-1.158,0-2.1,0.941-2.1,2.1s0.941,2.1,2.1,2.1s2.1-0.941,2.1-2.1S11.024,9.9,9.866,9.9z M9.866,13.1c-0.606,0-1.1-0.493-1.1-1.1s0.493-1.1,1.1-1.1s1.1,0.493,1.1,1.1S10.473,13.1,9.866,13.1z\" />\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:pathData=\"M16.267,16c-0.37,0-0.716,0.091-1.029,0.241l-1.656-1.657c0.513-0.734,0.817-1.623,0.817-2.584 s-0.305-1.85-0.817-2.584l1.656-1.657C15.551,7.909,15.896,8,16.267,8c1.323,0,2.399-1.077,2.399-2.4S17.59,3.2,16.267,3.2 s-2.4,1.076-2.4,2.399c0,0.369,0.091,0.715,0.241,1.028L12.45,8.285c-0.733-0.513-1.622-0.818-2.584-0.818 c-2.5,0-4.533,2.033-4.533,4.533s2.033,4.533,4.533,4.533c0.962,0,1.851-0.306,2.584-0.818l1.657,1.657 c-0.15,0.313-0.241,0.659-0.241,1.028c0,1.323,1.077,2.399,2.4,2.399s2.399-1.076,2.399-2.399S17.59,16,16.267,16z M16.267,4.8 c0.44,0,0.8,0.359,0.8,0.8c0,0.441-0.359,0.801-0.8,0.801c-0.441,0-0.801-0.359-0.801-0.801C15.466,5.159,15.825,4.8,16.267,4.8z M9.866,14.934c-1.617,0-2.934-1.316-2.934-2.934s1.316-2.934,2.934-2.934s2.933,1.316,2.933,2.934S11.483,14.934,9.866,14.934z M16.267,19.2c-0.441,0-0.801-0.359-0.801-0.8c0-0.441,0.359-0.801,0.801-0.801c0.44,0,0.8,0.359,0.8,0.801 C17.066,18.841,16.707,19.2,16.267,19.2z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_skip_next.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_skip_previous.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_stop_black_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M6,6h12v12H6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_swap_outline_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n  <path\n      android:fillColor=\"#595757\"\n      android:pathData=\"M6.842,18.549 L4,15.707 L6.842,12.866 L7.89,13.915 L6.098,15.707 L7.89,17.5 Z\" />\n  <path\n      android:fillColor=\"#595757\"\n      android:pathData=\"M5.049,14.966 L19.414,14.966 L19.414,16.449 L5.049,16.449 L5.049,14.966 Z\" />\n  <path\n      android:fillColor=\"#595757\"\n      android:pathData=\"M17.158,11.134 L16.11,10.085 L17.902,8.292 L16.11,6.5 L17.158,5.451 L20,8.292 Z\" />\n  <path\n      android:fillColor=\"#595757\"\n      android:pathData=\"M4.586,7.551 L18.951,7.551 L18.951,9.034 L4.586,9.034 L4.586,7.551 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12.91,20h-0.919C7.584,20,4,16.411,4,11.999C4,7.588,7.584,4,11.991,4 c2.246,0,4.435,0.975,6.006,2.674c1.43,1.546,2.134,3.457,1.983,5.379c-0.082,1.103-0.854,3.233-3.187,3.385l-0.418,0.027 c-1.888,0.121-3.38,0.216-3.819,0.855l0,0c0.004,0-0.299,0.688,0.681,2.273l0.231,0.323c0.148,0.209,0.168,0.483,0.05,0.712 C13.401,19.857,13.167,20,12.91,20z M11.991,5.371c-3.65,0-6.62,2.974-6.62,6.628c0,3.555,2.807,6.464,6.317,6.622 c-0.948-1.926-0.393-2.894-0.258-3.084c0.812-1.182,2.509-1.291,4.857-1.44l0.416-0.026c1.74-0.114,1.903-2.04,1.909-2.122 c0.12-1.539-0.456-3.081-1.623-4.343C15.677,6.186,13.855,5.371,11.991,5.371z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 12.206 7.02 C 13.01178345 7.02 13.665 7.67321655 13.665 8.479 C 13.665 9.28478345 13.01178345 9.938 12.206 9.938 C 11.40021655 9.938 10.747 9.28478345 10.747 8.479 C 10.747 7.67321655 11.40021655 7.02 12.206 7.02 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 8.828 10.268 C 9.63378345 10.268 10.287 10.92121655 10.287 11.727 C 10.287 12.53278345 9.63378345 13.186 8.828 13.186 C 8.02221655 13.186 7.369 12.53278345 7.369 11.727 C 7.369 10.92121655 8.02221655 10.268 8.828 10.268 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 15.45 10.268 C 16.25578345 10.268 16.909 10.92121655 16.909 11.727 C 16.909 12.53278345 16.25578345 13.186 15.45 13.186 C 14.64421655 13.186 13.991 12.53278345 13.991 11.727 C 13.991 10.92121655 14.64421655 10.268 15.45 10.268 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_time_add_24dp.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dp\"\n    android:height=\"24.0dp\"\n    android:viewportWidth=\"1000.0\"\n    android:viewportHeight=\"1000.0\">\n\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M698.235505586 166.666668C698.235505586 166.666668 869.372215288 309.497277476 869.372215288 309.497277476C869.372215288 309.497277476 821.823923241 366.518082099 821.823923241 366.518082099C821.823923241 366.518082099 650.575796871 223.76176429 650.575796871 223.76176429C650.575796871 223.76176429 698.235505586 166.666668 698.235505586 166.666668M301.727348247 166.666668C301.727348247 166.666668 349.349936128 223.724618456 349.349936128 223.724618456C349.349936128 223.724618456 178.176084759 366.518082099 178.176084759 366.518082099C178.176084759 366.518082099 130.627792712 309.460131642 130.627792712 309.460131642C130.627792712 309.460131642 301.727348247 166.666668 301.727348247 166.666668M499.981420667 248.018822817C315.360335856 248.018822817 165.657509659 397.721628182 165.657509659 582.342754659C165.657509659 766.963839469 315.360335856 916.666674 499.981420667 916.666674C684.602547143 916.666674 834.305381674 766.963839469 834.305381674 582.342754659C834.305381674 397.721628182 684.602547143 248.018822817 499.981420667 248.018822817C499.981420667 248.018822817 499.981420667 248.018822817 499.981420667 248.018822817M499.981420667 842.372465072C356.593619519 842.372465072 239.95171442 725.730547473 239.95171442 582.342754659C239.95171442 438.954920178 356.593619519 322.313027579 499.981420667 322.313027579C643.369255147 322.313027579 760.011172747 438.954920178 760.011172747 582.342754659C760.011172747 725.730547473 643.369255147 842.372465072 499.981420667 842.372465072C499.981420667 842.372465072 499.981420667 842.372465072 499.981420667 842.372465072M537.128545964 433.754336803C537.128545964 433.754336803 462.834337036 433.754336803 462.834337036 433.754336803C462.834337036 433.754336803 462.834337036 545.195629362 462.834337036 545.195629362C462.834337036 545.195629362 351.393023644 545.195629362 351.393023644 545.195629362C351.393023644 545.195629362 351.393023644 619.489838289 351.393023644 619.489838289C351.393023644 619.489838289 462.834337036 619.489838289 462.834337036 619.489838289C462.834337036 619.489838289 462.834337036 730.931172514 462.834337036 730.931172514C462.834337036 730.931172514 537.128545964 730.931172514 537.128545964 730.931172514C537.128545964 730.931172514 537.128545964 619.489838289 537.128545964 619.489838289C537.128545964 619.489838289 648.569838522 619.489838289 648.569838522 619.489838289C648.569838522 619.489838289 648.569838522 545.195629362 648.569838522 545.195629362C648.569838522 545.195629362 537.128545964 545.195629362 537.128545964 545.195629362C537.128545964 545.195629362 537.128545964 433.754336803 537.128545964 433.754336803\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_timer_black_24dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 12.742 9.076 L 11.258 9.076 L 11.258 13.009 L 14.492 13.009 L 14.492 11.522 L 12.742 11.522 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12.094,5.522c-3.719,0-6.742,3.024-6.742,6.742c0,2.38,1.241,4.472,3.107,5.672L7.77,19.016 l1.252,0.801l0.777-1.218c0.717,0.261,1.488,0.41,2.295,0.41c0.918,0,1.792-0.187,2.592-0.52l0.848,1.327l1.252-0.801l-0.803-1.252 c1.725-1.224,2.854-3.229,2.854-5.499C18.836,8.547,15.811,5.522,12.094,5.522z M12.094,17.523c-2.898,0-5.258-2.358-5.258-5.259 c0-2.897,2.359-5.256,5.258-5.256s5.257,2.358,5.257,5.256C17.351,15.165,14.992,17.523,12.094,17.523z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.725,6.557c0-0.336,0.13-0.65,0.367-0.888c0.49-0.49,1.289-0.491,1.779,0.001l1.05-1.053 c-1.069-1.068-2.811-1.068-3.879,0C4.523,5.135,4.238,5.823,4.237,6.556c0,0.733,0.286,1.423,0.805,1.941l1.05-1.05 C5.854,7.209,5.723,6.893,5.725,6.557z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M18.958,4.617c-1.071-1.069-2.812-1.068-3.88,0.002l1.051,1.05c0.489-0.49,1.288-0.49,1.779,0 c0.489,0.489,0.489,1.287-0.002,1.778l1.053,1.05C20.027,7.427,20.027,5.687,18.958,4.617z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_toc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.939,4v0.97H4v13.334C4,19.239,4.762,20,5.697,20h12.606C19.239,20,20,19.239,20,18.304V4H5.939z M5.454,18.304V6.424h0.485v11.88C5.939,18.57,5.454,18.57,5.454,18.304z M18.547,18.304c0,0.133-0.109,0.242-0.243,0.242H7.369 c0.012-0.08,0.024-0.159,0.024-0.242V5.454h11.153V18.304z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.09 11.061 H 16.849 V 11.97 H 9.09 V 11.061 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.09 13 H 16.849 V 13.909 H 9.09 V 13 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.09 7.182 H 16.849 V 8.091 H 9.09 V 7.182 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.09 9.12 H 16.849 V 10.03 H 9.09 V 9.12 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 9.09 14.939 H 16.849 V 15.848 H 9.09 V 14.939 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_top_source.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 4 4.003 H 20 V 5.603 H 4 V 4.003 Z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 5.594 12.821 L 6.742 13.935 L 11.199 9.338 L 11.194 20 L 12.794 20 L 12.799 9.338 L 17.251 13.938 L 18.401 12.825 L 11.999 6.214 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_translate.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M16.424,10.47h-1.258L11.676,20h1.428l0.984-2.77h3.459L18.572,20H20L16.424,10.47z M17.057,15.917 h-2.481l1.241-3.535L17.057,15.917z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M12.361,15.597l0.488-1.354l-0.191-0.082c-0.73-0.313-1.342-0.657-1.824-1.024 c1.232-1.382,2.061-3.222,2.465-5.476h2.135v-1.38h-4.7l0.25-0.146l-0.098-0.201c-0.413-0.843-0.722-1.424-0.946-1.775L9.839,4 L8.656,4.654l0.098,0.204c0.309,0.644,0.543,1.119,0.701,1.423h-5.25v1.38H6.34c0.38,2.138,1.211,3.978,2.475,5.476 c-0.93,0.749-2.461,1.466-4.555,2.133L4,15.352l0.143,0.254c0.153,0.271,0.322,0.595,0.508,0.969l0.085,0.171L4.9,16.678 c2.273-0.928,3.928-1.79,4.916-2.563c0.566,0.456,1.355,0.925,2.353,1.393L12.361,15.597z M11.912,7.661 c-0.287,1.778-0.975,3.294-2.045,4.513c-1.131-1.274-1.877-2.79-2.223-4.513H11.912z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_tune.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M13.592,5.523V4.747c0-0.696-0.565-1.262-1.262-1.262H9.252c-0.694,0-1.26,0.565-1.26,1.262v0.776H4 v1.523h3.992v0.777c0,0.695,0.565,1.261,1.26,1.261h3.078c0.696,0,1.262-0.565,1.262-1.261V7.047H20V5.523H13.592z M11.992,7.485 h-2.4v-2.4h2.4V7.485z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M19.008,10.462c0-0.696-0.565-1.262-1.262-1.262h-3.078c-0.694,0-1.26,0.565-1.26,1.262v0.778H4 v1.521h9.408v0.778c0,0.695,0.565,1.261,1.26,1.261h3.078c0.696,0,1.262-0.565,1.262-1.261v-0.778H20V11.24h-0.992V10.462z M17.408,13.2h-2.4v-2.4h2.4V13.2z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M11.008,16.178c0-0.696-0.565-1.262-1.262-1.262H6.668c-0.694,0-1.26,0.565-1.26,1.262v0.776H4 v1.523h1.408v0.777c0,0.695,0.565,1.261,1.26,1.261h3.078c0.696,0,1.262-0.565,1.262-1.261v-0.777H20v-1.523h-8.992V16.178z M9.408,18.916h-2.4v-2.4h2.4V18.916z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_update.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M15.879,10.302H20V6.182h-1.454V7.43C17.066,5.315,14.646,4,12,4c-4.412,0-8,3.589-8,8s3.588,8,8,8 c3.789,0,7.086-2.691,7.84-6.401l-1.426-0.29C17.798,16.344,15.1,18.546,12,18.546c-3.608,0-6.546-2.937-6.546-6.546 S8.392,5.454,12,5.454c2.398,0,4.576,1.32,5.721,3.394h-1.842V10.302z\" />\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M 12.727 12.105 L 12.727 7.469 L 11.273 7.469 L 11.273 12.708 L 14.014 15.448 L 15.042 14.42 Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_version.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M5.938,4v16h1.414l4.648-2.789L16.649,20h1.411V4H5.938z M16.606,18.278l-4.605-2.763l-4.609,2.764 V5.454h9.215V18.278z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_view_quilt.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#595757\"\n        android:pathData=\"M18,4 L6,4 C4.896,4,4,4.896,4,6 L4,18 C4,19.104,4.896,20,6,20 L18,20 C19.104,20,20,19.104,20,18 L20,6 C20,4.896,19.104,4,18,4 Z M18.568,18.568 L5.432,18.568 L5.432,5.432 L18.568,5.432 L18.568,18.568 Z\" />\n    <path\n        android:strokeColor=\"#595757\"\n        android:strokeWidth=\"1.435\"\n        android:strokeMiterLimit=\"10\"\n        android:pathData=\"M5.234,8.946 L18.734,8.946\" />\n    <path\n        android:strokeColor=\"#595757\"\n        android:strokeWidth=\"1.435\"\n        android:strokeMiterLimit=\"10\"\n        android:pathData=\"M8.559,9.422 L8.559,18.616\" />\n    <path\n        android:strokeColor=\"#595757\"\n        android:strokeWidth=\"1.435\"\n        android:strokeMiterLimit=\"10\"\n        android:pathData=\"M18.734,13.482 L8.969,13.482\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_volume_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"24dp\"\n        android:height=\"24dp\"\n        android:viewportWidth=\"24.0\"\n        android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"#FFFFFFFF\"\n        android:pathData=\"M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_web_outline.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"m12.23,3.5468c4.0323,5.6914 4.6425,11.2543 0,16.6202l0,0m-0.4599,-16.6202c-4.0323,5.6914 -4.6425,11.2543 0,16.6202l0,0m-8.27,-8.167 l17,0 0,0m-0.2485,0a8.2515,8.2515 72.5893,0 1,-8.2515 8.2516,8.2515 8.2515,72.5893 0,1 -8.2515,-8.2516 8.2515,8.2515 72.5893,0 1,8.2515 -8.2516,8.2515 8.2515,72.5893 0,1 8.2515,8.2516z\"\n      android:strokeAlpha=\"1\"\n      android:strokeLineJoin=\"miter\"\n      android:strokeWidth=\"1.5\"\n      android:strokeColor=\"#4A4B4A\"\n      android:strokeLineCap=\"butt\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_web_service_noti.xml",
    "content": "<vector android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportHeight=\"24.0\"\n    android:viewportWidth=\"24.0\"\n    android:width=\"24dp\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path\n        android:fillColor=\"#FF000000\"\n        android:pathData=\"M7,7.07L8.43,8.5c0.91,-0.91 2.18,-1.48 3.57,-1.48s2.66,0.57 3.57,1.48L17,7.07C15.72,5.79 13.95,5 12,5s-3.72,0.79 -5,2.07zM12,1C8.98,1 6.24,2.23 4.25,4.21l1.41,1.41C7.28,4 9.53,3 12,3s4.72,1 6.34,2.62l1.41,-1.41C17.76,2.23 15.02,1 12,1zM14.86,10.01L9.14,10C8.51,10 8,10.51 8,11.14v9.71c0,0.63 0.51,1.14 1.14,1.14h5.71c0.63,0 1.14,-0.51 1.14,-1.14v-9.71c0.01,-0.63 -0.5,-1.13 -1.13,-1.13zM15,20L9,20v-8h6v8z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_web_service_phone.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:pathData=\"M14.604,8.901H9.206c-1.046,0-1.898,0.909-1.898,2.025v8.885c0,1.117,0.853,2.024,1.898,2.024h5.397 c1.048,0,1.898-0.908,1.898-2.024v-8.885C16.502,9.81,15.651,8.901,14.604,8.901z M14.99,19.812c0,0.278-0.176,0.513-0.387,0.513 H9.206c-0.21,0-0.388-0.235-0.388-0.513v-8.885c0-0.278,0.178-0.513,0.388-0.513h5.397c0.211,0,0.387,0.235,0.387,0.513V19.812z\" />\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:pathData=\"M11.997,1.836c-2.017,0-3.953,0.409-5.604,1.181c0,0,0,0-0.001,0C6.186,3.114,5.98,3.216,5.79,3.323 l0.739,1.318c0.158-0.089,0.33-0.174,0.502-0.253L6.93,4.169l0.102,0.218c1.453-0.68,3.17-1.039,4.966-1.04 c1.999,0,3.946,0.461,5.481,1.298l0.723-1.327C16.424,2.349,14.278,1.836,11.997,1.836z\" />\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:pathData=\"M8.621,5.621l0.723,1.328C9.426,6.906,9.508,6.864,9.59,6.824c1.484-0.691,3.654-0.642,5.053,0.123 l0.727-1.327c-1.824-0.996-4.496-1.062-6.418-0.166l0.314,0.674l-0.32-0.671C8.838,5.508,8.729,5.563,8.621,5.621z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/image_welcome.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"680.311dp\"\n    android:height=\"1360.631dp\"\n    android:viewportWidth=\"680.311\"\n    android:viewportHeight=\"1360.631\">\n\n    <path\n        android:strokeColor=\"#82A6F5\"\n        android:strokeWidth=\"10\"\n        android:strokeMiterLimit=\"10\"\n        android:pathData=\"M 225.539 182.693 L 225.539 407.308\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M282,212.126c0.481,0.289,0.674,0.578,0.674,0.771c0,0.191-0.191,0.48-0.576,0.674l-2.407,1.444v45.431 c0,7.315,0.192,15.497,0.385,18.865v0.674c0,1.348-0.385,1.731-2.406,2.599c-0.865,0.386-1.348,0.578-1.829,0.578 c-0.674,0-0.77-0.578-0.77-2.021v-1.06c0.193-5.679,0.289-14.341,0.289-19.828v-38.501c0-3.945-0.193-10.49-0.385-14.053 L282,212.126z M281.229,194.32c9.433,4.235,12.513,7.315,13.09,9.433c0.097,0.289,0.097,0.675,0.097,1.06 c0,2.214-1.443,5.005-3.369,5.005c-1.251,0-1.731-0.771-2.214-3.369c-0.385-2.021-2.502-5.583-8.181-11.261L281.229,194.32z M336.672,251.302c0,8.759,0.48,10.684,2.406,11.646c0.481,0.289,0.674,0.481,0.674,0.962c0,0.192,0,0.385-0.097,0.674 c-1.54,4.429-3.369,5.294-10.105,5.294c-12.031,0-14.149-0.865-14.149-6.256v-19.924h-7.219 c0.097,18.577-8.855,26.277-24.642,31.956l-0.385-1.347c15.785-8.566,20.117-15.979,20.694-30.608h-6.545v3.272 c0,0.962-0.192,1.348-1.637,2.021c-1.059,0.481-1.829,0.771-2.214,0.771c-0.577,0-0.77-0.481-0.77-1.251v-0.866 c0.192-2.6,0.289-4.62,0.289-9.048v-8.663c0-1.829-0.097-5.486-0.289-7.894l5.102,3.946h16.748 c3.272-5.968,5.679-10.78,8.181-17.614l5.68,2.6c0.385,0.192,0.577,0.385,0.577,0.577c0,0.289-0.192,0.481-0.673,0.675 c-2.118,0.962-2.407,1.154-11.743,13.764h9.048l2.502-3.465l5.39,3.85c0.386,0.289,0.578,0.481,0.578,0.771 s-2.214,1.732-2.694,2.021v7.412c0,3.08,0,5.102,0.289,7.796v0.289c0,1.349-3.851,2.695-4.14,2.695 c-0.577,0-0.577-0.385-0.577-3.658h-7.123v20.117c0,1.731,0.771,2.406,8.278,2.406c5.679,0,6.545-0.866,7.123-14.919H336.672z M297.593,209.335c8.566,4.717,10.78,6.738,10.78,9.723c0,2.405-2.021,4.042-3.08,4.042c-1.251,0-1.829-1.06-2.214-4.14 c-0.289-2.118-1.636-4.332-6.16-8.663L297.593,209.335z M326.95,241.58v-13.475h-29.646v13.475H326.95z M352.072,201.731 c0.385,0.385,0.674,0.674,0.674,0.867c0,0.289-0.289,0.48-0.771,0.77l-2.695,1.637v69.977c0,5.583-0.577,7.123-6.545,9.336 c-0.289,0.096-0.48,0.192-0.674,0.192c-0.385,0-0.674-0.289-0.771-0.866c-0.48-2.118-2.021-3.946-11.068-6.545l0.096-1.251 c5.391,1.059,8.855,1.54,10.973,1.54c3.081,0,3.563-0.964,3.563-2.792v-70.938h-35.421c-2.021,0-4.813,0.385-6.449,1.059 l-2.695-3.561c2.406,0.192,4.813,0.385,7.026,0.385h35.517l3.851-4.428L352.072,201.731z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M298.748,378.356c-8.47,7.219-13.283,11.743-16.555,15.688c-0.578,0.675-0.867,0.962-1.251,0.962 c-0.289,0-0.481-0.191-0.771-0.576l-3.85-4.813c2.695-1.155,4.716-3.85,4.716-6.063v-37.25l-6.834,0.385 c-2.599,0.191-3.369,0.385-4.813,0.77l-2.889-2.983c2.889,0.096,4.429,0.096,6.643,0l7.7-0.386l2.888-3.85l5.967,4.813 c0.481,0.385,0.674,0.673,0.674,0.865c0,0.289-0.289,0.481-0.77,0.771l-4.14,2.213v35.902l12.417-7.7L298.748,378.356z M276.417,313.289c5.391,2.984,14.63,9.625,14.63,13.283c0,2.117-1.443,4.62-3.08,4.62c-1.059,0-1.539-0.578-2.31-2.984 c-0.866-2.694-4.235-8.952-9.914-13.957L276.417,313.289z M354.286,372.196c0.48,0.481,0.675,0.77,0.675,1.059 s-0.289,0.386-0.964,0.386h-25.796c-0.674,1.731-1.442,3.465-2.502,5.101c9.818,3.658,18.866,9.145,21.753,12.224 c1.349,1.444,1.828,3.08,1.828,4.524c0,1.925-0.866,3.465-2.021,3.465c-0.867,0-1.638-0.48-4.235-4.813 c-1.733-2.888-8.854-9.625-18.288-13.957c-5.583,8.085-15.4,14.534-30.801,19.442l-0.674-1.251 c16.459-6.737,26.661-15.304,30.512-24.736h-23.967c-2.021,0-4.909,0.385-6.449,0.962l-2.695-3.465 c2.311,0.192,4.813,0.385,7.026,0.385h26.854c2.405-7.315,3.561-14.919,3.85-26.758l7.219,3.368 c0.481,0.289,0.771,0.481,0.771,0.771c0,0.193-0.192,0.48-0.675,0.771l-2.694,1.829c-0.481,5.68-2.021,14.054-4.043,20.021h14.535 l4.428-5.583L354.286,372.196z M356.885,341.78c0.577,0.578,0.962,0.866,0.962,1.059c0,0.097-0.385,0.097-1.154,0.289 c-5.391,1.155-7.797,2.889-14.053,10.203l-1.251-0.77l6.738-11.261h-45.239c-2.021,0-4.909,0.288-6.449,0.865l-2.695-3.369 c2.311,0.289,4.813,0.386,7.026,0.386h20.501v-12.417h-12.417c-2.021,0-4.909,0.385-6.449,0.962l-2.695-3.465 c2.311,0.192,4.813,0.385,7.027,0.385h14.534v-5.486c0-3.754-0.097-6.063-0.289-9.433l6.93,2.983 c0.771,0.289,1.155,0.577,1.155,0.963c0,0.288-0.289,0.577-0.866,1.059l-2.31,1.925v7.989h16.17l4.043-4.909l5.486,5.679 c0.385,0.481,0.576,0.771,0.576,0.962c0,0.289-0.287,0.386-0.865,0.386h-25.411v12.417h21.08l3.562-3.946L356.885,341.78z M299.999,356.314c6.642,2.118,11.936,4.813,13.667,7.219c0.481,0.577,0.674,1.347,0.674,2.021c0,1.828-1.154,3.562-2.405,3.562 c-0.866,0-1.349-0.385-2.117-1.925c-1.54-3.08-5.103-6.737-10.396-9.914L299.999,356.314z M305.87,343.897 c6.064,1.829,11.55,4.909,12.994,7.219c0.385,0.674,0.578,1.444,0.578,2.214c0,1.829-1.06,3.465-2.118,3.465 c-0.866,0-1.347-0.578-2.118-2.021c-1.442-2.695-4.042-6.256-9.816-9.818L305.87,343.897z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M454.938,335.997c0.252,0.216,0.36,0.396,0.36,0.54c0,0.181-0.182,0.288-0.54,0.288h-14.688v5.688 c0,1.943-0.61,2.34-2.987,3.42c-0.071,0.036-0.145,0.071-0.18,0.071c-0.107,0-0.217-0.107-0.252-0.287 c-0.396-1.297-1.62-1.979-4.464-2.771l0.108-0.468c2.123,0.54,3.563,0.756,4.463,0.756c1.368,0,1.691-0.468,1.691-1.188v-5.219 h-12.419c-0.756,0-1.8,0.145-2.412,0.396l-1.008-1.332c0.9,0.071,1.8,0.144,2.628,0.144h13.211c0-1.188-0.036-2.556-0.071-3.204 l2.122,0.973l6.877-3.563h-18.286c-0.757,0-1.801,0.145-2.412,0.396l-1.008-1.332c0.899,0.071,1.8,0.144,2.628,0.144h19.582 l1.8-1.152l1.583,2.232c0.145,0.216,0.217,0.396,0.217,0.468c0,0.108-0.072,0.145-0.217,0.145c-0.107,0-0.539-0.036-0.647-0.036 c-1.728,0-4.787,1.116-10.547,3.671v1.26h10.728l1.619-2.231L454.938,335.997z M454.723,317.063 c0.252,0.216,0.359,0.396,0.359,0.54c0,0.144-0.181,0.252-0.504,0.252H426.43c-0.756,0-1.799,0.144-2.411,0.396l-1.008-1.332 c0.898,0.072,1.8,0.145,2.628,0.145h25.196l1.512-2.087L454.723,317.063z M449.359,320.699c0.216,0.145,0.324,0.216,0.324,0.36 c0,0.071-0.108,0.18-0.288,0.323l-0.937,0.685v1.872c0,1.224,0.035,1.979,0.107,2.879v0.144c0,0.612-1.151,0.9-1.476,0.9 c-0.325,0-0.324-0.108-0.324-1.296h-15.334c0,1.224,0,1.368-0.685,1.62c-0.359,0.144-0.611,0.216-0.792,0.216 c-0.252,0-0.323-0.145-0.323-0.54v-0.288c0.071-0.647,0.107-1.656,0.107-2.879v-2.7c0-0.899-0.036-1.908-0.107-2.592l1.979,1.332 h14.578l0.972-1.44L449.359,320.699z M446.768,325.774v-4.248h-15.334v4.248H446.768z M436.329,311.952 c3.349,1.152,4.355,2.052,4.355,3.384c0,0.9-0.504,1.62-1.08,1.62c-0.432,0-0.612-0.144-0.864-0.936 c-0.322-1.008-0.972-2.16-2.663-3.671L436.329,311.952z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M455.084,386.108c-1.872,0.18-2.593,0.504-3.276,1.439c-0.216,0.288-0.396,0.433-0.611,0.433 c-0.145,0-8.026-2.124-12.419-5.111c-4.031,3.023-9.719,4.896-15.838,5.867l-0.144-0.576c5.687-1.404,11.051-3.383,14.722-6.263 c-2.231-1.872-3.995-4.535-5.399-7.883c-1.332,0-1.69,0-2.734,0.288l-1.368-1.152c0.973,0.072,1.691,0.072,2.412,0.072h12.777 l1.188-1.152l2.268,1.764c0.252,0.181,0.36,0.324,0.36,0.396c0,0.107-0.145,0.18-0.504,0.288c-1.08,0.324-1.296,0.468-2.376,2.124 c-1.729,2.7-2.592,3.888-4.104,5.22c4.284,2.591,9.468,3.527,15.047,3.779V386.108L455.084,386.108z M454.723,368.867 c0.18,0.145,0.252,0.252,0.252,0.324c0,0.107-0.18,0.107-0.576,0.144c-1.655,0.108-3.022,1.584-4.967,3.672l-0.54-0.252 l2.556-3.995h-24.549c-0.252,2.771-0.685,3.995-1.62,4.606c-0.359,0.252-0.72,0.324-1.044,0.324c-0.504,0-1.116-0.252-1.116-0.937 c0-0.396,0.252-0.863,0.9-1.296c0.864-0.576,2.088-2.268,2.447-4.788l0.575,0.108c-0.035,0.432-0.035,0.828-0.071,1.188h15.37 c1.729-2.52,3.42-5.398,4.644-7.774l1.944,1.26c0.216,0.145,0.323,0.216,0.323,0.324s-0.253,0.216-0.827,0.396 c-0.324,0.108-0.828,0.612-1.548,1.513c-0.828,1.008-2.484,2.916-3.744,4.282h8.026l1.225-1.188L454.723,368.867z M450.763,358.32 c0.181,0.216,0.252,0.36,0.252,0.468c0,0.072-0.035,0.108-0.145,0.108c-0.108,0-0.252-0.036-0.468-0.108 c-0.359-0.107-1.044-0.252-1.728-0.252c-0.146,0-0.288,0.036-0.434,0.036c-5.614,0.54-14.47,1.08-22.893,1.332l0.036-0.504 c12.813-1.188,20.661-2.016,23.828-3.061L450.763,358.32z M429.635,361.344c0.646,0.359,4.392,2.411,4.392,4.139 c0,0.828-0.647,1.404-1.152,1.404c-0.322,0-0.72-0.324-0.826-0.937c-0.218-1.224-1.116-2.88-2.7-4.248L429.635,361.344z M432.945,374.014c1.548,2.986,3.601,5.326,5.688,6.946c2.016-1.836,3.384-3.852,4.787-6.946H432.945z M437.229,360.696 c2.736,1.656,3.708,2.988,3.815,3.6c0.035,0.145,0.035,0.252,0.035,0.396c0,1.044-0.898,1.62-1.366,1.62 c-0.434,0-0.576-0.288-0.757-1.548c-0.145-0.973-0.539-2.053-1.979-3.744L437.229,360.696z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M454.866,414.658c0.182,0.18,0.252,0.288,0.252,0.396c0,0.107-0.106,0.144-0.358,0.144h-28.868 c-0.721,0-1.765,0.145-2.304,0.36l-0.974-1.296c0.864,0.107,1.729,0.144,2.521,0.144h12.85v-4.284h-8.025 c-0.721,0-1.765,0.145-2.305,0.36l-0.973-1.295c0.828,0.107,1.729,0.144,2.521,0.144h8.782v-4.643h-9.719 c-0.756,0-1.836,0.144-2.411,0.359l-1.008-1.296c0.863,0.072,1.8,0.145,2.627,0.145h14.398c1.439-2.052,2.52-3.853,3.238-5.255 l1.98,1.296c0.216,0.144,0.324,0.252,0.324,0.324c0,0.107-0.146,0.18-0.396,0.252c-0.898,0.216-2.088,1.044-4.464,3.384h7.271 l1.225-1.765l2.34,2.017c0.18,0.18,0.288,0.288,0.288,0.396c0,0.108-0.145,0.145-0.433,0.145h-13.533v4.643h7.811l1.26-1.764 l2.017,1.979c0.181,0.181,0.288,0.324,0.288,0.396c0,0.144-0.145,0.18-0.396,0.18h-10.979v4.284h11.626l1.439-1.872 L454.866,414.658z M453.103,420.489c0.181,0.18,0.288,0.288,0.288,0.396c0,0.107-0.145,0.144-0.433,0.144h-14.541 c-1.729,5.579-5.651,8.963-15.228,11.123l-0.144-0.432c7.811-2.916,11.878-5.003,13.605-10.69h-8.963 c-0.721,0-1.765,0.144-2.304,0.36l-0.974-1.296c0.828,0.107,1.729,0.144,2.521,0.144h9.935c0.323-1.404,0.575-3.023,0.685-4.896 l2.412,1.224c0.18,0.107,0.286,0.216,0.286,0.324c0,0.071-0.07,0.18-0.252,0.252c-0.756,0.359-0.826,0.504-0.898,1.008 c-0.145,0.72-0.288,1.404-0.468,2.088h11.086l1.225-1.692L453.103,420.489z M431.073,398.784c2.16,0.432,4.032,1.188,4.788,2.196 c0.216,0.288,0.288,0.647,0.288,1.008c0,0.792-0.433,1.583-0.937,1.583c-0.324,0-0.576-0.108-0.863-0.972 c-0.288-0.9-1.692-2.593-3.456-3.492L431.073,398.784z M455.406,429.596c-1.512-0.072-2.556,0.432-3.312,1.548 c-0.107,0.18-0.216,0.252-0.359,0.252c-0.072,0-0.145-0.036-0.217-0.072c-5.327-2.124-9.034-5.076-11.626-10.295h0.684 c4.248,6.191,8.928,7.739,14.83,8.171V429.596L455.406,429.596z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M436.365,450.048c0.216,0.107,0.323,0.216,0.323,0.323c0,0.108-0.107,0.181-0.359,0.324l-1.115,0.576 c-0.359,5.759-1.296,10.475-3.023,14.074c2.88,1.728,4.355,3.06,4.355,4.68c0,1.008-0.612,1.404-0.973,1.404 c-0.288,0-0.54-0.252-0.937-0.937c-0.791-1.403-1.765-2.664-3.131-3.852c-2.053,3.456-4.824,6.119-9.287,8.351l-0.252-0.396 c3.814-2.448,6.623-5.507,8.387-8.927c-1.439-1.08-3.203-2.124-5.399-3.24c0.938-3.744,1.836-7.848,2.521-11.482l-1.835,0.072 c-0.756,0.036-1.8,0.288-2.376,0.576l-1.116-1.26c0.36,0,2.196-0.036,2.628-0.036l2.844-0.108c0.685-3.563,1.151-6.587,1.296-8.314 l2.483,1.512c0.145,0.072,0.216,0.144,0.216,0.252c0,0.072-0.071,0.144-0.18,0.216l-0.9,0.612c-0.396,1.8-0.827,3.672-1.295,5.688 l3.743-0.144l1.224-1.477L436.365,450.048z M429.058,450.911c-0.756,3.349-1.619,7.092-2.663,11.52 c1.655,0.684,3.167,1.403,4.463,2.124c1.477-3.348,2.34-8.027,2.556-13.858L429.058,450.911z M455.118,458.255 c0.36,0.323,0.504,0.54,0.504,0.684c0,0.18-0.216,0.252-0.646,0.252h-8.783v12.203c0,2.124-0.144,2.304-2.699,3.815 c-0.071,0.035-0.145,0.071-0.216,0.071c-0.107,0-0.216-0.107-0.252-0.288c-0.252-1.26-1.044-1.62-4.536-2.664l0.108-0.504 c2.16,0.396,3.563,0.612,4.428,0.612c1.403,0,1.477-0.576,1.477-1.872V459.19h-5.509c-0.756,0-1.8,0.144-2.411,0.396l-1.008-1.332 c0.899,0.071,1.8,0.144,2.628,0.144h6.3v-2.664c0-1.296-0.036-3.131-0.072-3.887l1.764,0.936l4.032-6.371h-9.791 c-0.756,0-1.8,0.145-2.412,0.396l-1.008-1.332c0.899,0.072,1.8,0.144,2.628,0.144h10.511l1.26-1.512l2.195,2.124 c0.181,0.18,0.252,0.288,0.252,0.396c0,0.144-0.144,0.18-0.432,0.216c-1.188,0.107-2.195,0.72-6.769,6.19 c0.182,0.072,0.612,0.324,0.612,0.433c0,0.107-0.108,0.18-1.08,0.684v4.248h5.508l1.513-2.016L455.118,458.255z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M435.645,489.288c0.181,0.145,0.288,0.217,0.288,0.324c0,0.072-0.107,0.181-0.288,0.288l-1.188,0.828 v15.046c0,2.736,0.036,4.067,0.108,5.219v0.216c0,0.757-1.512,1.477-1.548,1.477c-0.146,0-0.181-0.252-0.181-0.72v-1.656h-6.911 v3.06c0,0.396-0.071,0.469-0.756,0.9c-0.359,0.216-0.575,0.36-0.721,0.36c-0.216,0-0.252-0.253-0.252-0.828v-0.36 c0.072-2.232,0.108-4.679,0.108-7.523v-14.325c0-1.8-0.036-2.809-0.108-4.032l2.16,1.584h6.119l0.936-1.296L435.645,489.288z M432.838,498.755v-8.819h-6.911v8.819H432.838z M432.838,509.517v-9.971h-6.911v9.971H432.838z M455.19,494.399 c0.18,0.18,0.288,0.324,0.288,0.396c0,0.107-0.145,0.144-0.468,0.144h-5.508v19.833c0,2.339-0.036,2.483-2.735,3.635 c-0.145,0.036-0.216,0.072-0.324,0.072c-0.144,0-0.216-0.072-0.252-0.288c-0.288-1.332-0.972-1.944-5.219-3.06l0.106-0.576 c2.376,0.504,3.96,0.756,4.969,0.756c1.476,0,1.8-0.468,1.8-1.116v-19.257H439.1c-0.647,0-1.512,0.108-2.124,0.252l-1.296-1.188 c0.899,0.107,1.691,0.144,2.628,0.144h9.539v-3.814c0-2.988,0-3.996-0.072-5.221l2.556,1.296c0.253,0.145,0.396,0.216,0.396,0.324 s-0.146,0.216-0.396,0.36l-0.827,0.468v6.587h2.268l1.296-2.016L455.19,494.399z M438.849,498.646 c2.699,2.087,4.464,3.671,4.464,5.399c0,0.899-0.54,1.764-1.261,1.764c-0.468,0-0.685-0.252-0.973-1.692 c-0.287-1.512-0.972-3.275-2.592-5.184L438.849,498.646z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M453.535,550.342c0,4.176,0.396,5.398,1.477,5.976c0.18,0.108,0.288,0.18,0.288,0.324 c0,0.072-0.036,0.144-0.072,0.252c-1.044,2.16-1.584,2.484-3.779,2.736c-1.439,0.144-3.312,0.252-4.858,0.252 c-5.04,0-5.688-0.721-5.688-3.313V543.61h-5.508c-0.144,9.646-1.26,13.786-12.13,18.034l-0.252-0.468 c8.963-4.716,10.689-7.415,10.689-17.566h-7.307c-0.756,0-1.8,0.145-2.412,0.396l-1.008-1.332c0.898,0.071,1.8,0.144,2.628,0.144 h12.599v-9.395c0-1.477-0.071-3.924-0.145-5.256l2.628,1.477c0.216,0.107,0.324,0.252,0.324,0.432c0,0.108-0.072,0.252-0.218,0.36 l-0.898,0.792v11.59h1.584c1.8-2.412,5.075-7.667,6.659-10.941l2.34,1.764c0.106,0.107,0.18,0.18,0.18,0.252 c0,0.107-0.107,0.18-0.324,0.216c-0.684,0.144-1.188,0.54-1.584,1.008l-6.335,7.703h8.388l1.62-2.196l2.376,2.196 c0.252,0.216,0.358,0.396,0.358,0.54c0,0.145-0.18,0.252-0.575,0.252h-11.986v12.778c0,1.728,1.08,1.872,2.808,1.872 c1.44,0,4.212-0.108,5.185-0.216c1.691-0.217,2.376-0.685,2.376-7.703L453.535,550.342L453.535,550.342z M427.978,532.271 c3.492,2.088,5.939,4.716,5.939,5.651c0,1.403-0.433,2.411-1.225,2.411c-0.359,0-0.612-0.252-0.864-1.188 c-0.396-1.44-1.942-4.68-4.139-6.443L427.978,532.271z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M336.627,1009.641h1.169v0.174h-1.169V1009.641z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M323.57,1077.314l0.672,0.24c-1.651-1.31-5.873-4.357-11.369-6.172c-6.539-2.156-16.283-2.948-25.559,5.512 c-0.105,0.098-0.236,0.131-0.383,0.092c-2.219-0.543-11.82-24.271-20.374-46.504c-0.464,0.313-0.917,0.67-1.389,1.039 c-0.831,0.659-1.731,1.363-2.839,1.929c10.691,26.719,21.417,52.866,22.988,54.386 C295.088,1067.123,308.29,1071.844,323.57,1077.314z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M387.494,1076.688c-0.146,0.031-0.278,0-0.383-0.098c-9.277-8.462-19.022-7.668-25.567-5.515 c-5.47,1.803-9.669,4.826-11.339,6.144l0.064-0.025c14.236-5.228,28.957-10.629,38.836,10.312 c1.525-1.474,11.693-26.187,22.068-52.094c-0.115-0.052-0.228-0.097-0.33-0.126c-1.754-0.471-3.011-1.342-4.072-2.237 C398.51,1054.402,389.617,1076.162,387.494,1076.688z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M335.92,1009.988l-0.053-0.061c-3.911-4.523-39.396-43.937-80.371-34.611 c-0.289,0.447,1.828,4.865,4.064,8.907l7.104-0.201l-3.673-6.462l0.247-0.039c23.855-3.857,63.73,25.469,71.48,31.385l-0.19,0.271 c0.216,0.123,0.427,0.246,0.633,0.37l0.344,0.202l-0.382,0.112c-0.129,0.037-0.264,0.225-0.264,0.494l2.018,66.188 c0,0.305,0.166,0.524,0.315,0.524h0.363c0.146,0,0.314-0.221,0.314-0.52l2.019-66.123c0-0.254-0.137-0.439-0.318-0.439h-0.811 l0.522-0.321c8.939-5.455,27.154-11.722,46.41-15.962c15.455-3.405,33.166-5.711,36.504-1.967l0.066,0.077l-0.031,0.098 c-0.393,1.073-3.553,9.711-7.83,21.094c1.662,0.418,3.478,0.954,5.074,1.563c4.443-11.213,8.469-21.466,11.311-28.748 c-43.979-4.454-80.327,16.482-90.432,23.016l-0.201-0.284c10.863-8.298,48.09-35.085,71.033-31.375l0.246,0.039l-3.674,6.453 l7.104,0.186c2.24-4.024,4.354-8.428,4.051-8.887c-40.827-9.281-76.438,30.404-80.362,34.959l-0.054,0.061h-0.317v-0.174h-0.384 h-1.169h-0.382v0.174H335.92L335.92,1009.988z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M252.195,992.182l-0.036-0.097l0.07-0.077c3.323-3.729,21.045-1.468,36.524,1.891 c18.602,4.035,36.266,9.92,45.519,15.133c-10.467-6.72-46.691-27.325-90.633-22.875c2.65,6.793,6.331,16.165,10.419,26.495 c1.791-0.464,3.706-0.826,5.328-1.065C255.435,1001.046,252.567,993.201,252.195,992.182z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M248.686,1016.834c-0.151,0.89,0.351,1.643,0.664,2.013c0.76,0.898,1.789,1.256,2.124,1.045 c2.864-1.798,7.438-3.373,13.588-4.676c0.74-0.158,2.808-0.822,2.62-1.787c-0.25-1.268-2.175-1.531-3.745-1.531 c-0.503,0-0.939,0.027-1.201,0.044c-0.102,0.007-0.18,0.011-0.23,0.013c-2.323,0.082-8.587,1.109-11.841,2.552 C249.854,1014.862,248.837,1015.934,248.686,1016.834z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M250.244,1023.291c0.381,1.347,1.087,2.028,2.096,2.028c0.705,0,1.309-0.335,1.478-0.437l0.156-0.097 c1.4-0.856,5.668-3.471,12.262-4.232c2.932-0.339,6.361-1.8,6.225-3.106c-0.045-0.441-0.185-1.783-3.015-1.783 c-1.49,0-3.229,0.354-4.731,0.698c-3.586,0.825-9.587,2.663-12.804,4.087C251.168,1020.777,249.874,1021.985,250.244,1023.291z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M268.087,1021.346c-0.074,0.01-0.14,0.016-0.189,0.022c-2.99,0.278-8.495,1.687-13.512,4.119 c-0.778,0.378-1.654,1.358-1.712,2.273c-0.062,1.046,0.34,1.709,0.688,2.078c0.526,0.559,1.183,0.757,1.623,0.757 c0.143,0,0.27-0.021,0.377-0.06c2.138-0.757,3.664-1.619,5.139-2.458c1.357-0.766,2.759-1.563,4.587-2.229 c0.065-0.021,0.231-0.068,0.47-0.131c4.775-1.277,5.376-2.258,5.31-2.729c-0.166-1.162-0.803-1.703-2.009-1.703 C268.558,1021.287,268.285,1021.32,268.087,1021.346z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M264.754,1027.191l-0.093,0.032c-2.167,0.767-3.479,1.604-6.098,3.271c-0.455,0.289-0.945,0.604-1.485,0.944 c-0.376,0.237-0.594,0.497-0.65,0.77c-0.055,0.268,0.036,0.571,0.271,0.916c0.609,0.881,1.08,0.979,1.38,0.979l0,0 c0.313,0,0.633-0.129,0.943-0.252c0.228-0.092,0.463-0.188,0.7-0.234c2.219-0.429,3.641-1.552,5.018-2.64 c0.624-0.493,1.212-0.958,1.862-1.354c0.039-0.021,0.089-0.053,0.15-0.086c0.451-0.26,1.652-0.94,1.813-1.784 c0.064-0.339-0.042-0.677-0.328-1.032c-0.099-0.124-0.344-0.192-0.69-0.192C266.588,1026.529,265.207,1027.029,264.754,1027.191z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M409.591,1012.775c-0.422-0.059-1.201-0.166-2.06-0.166c-1.201,0-2.692,0.225-2.985,1.289 c-0.261,0.949,1.756,1.763,2.483,1.973c6.041,1.744,10.486,3.645,13.217,5.645c0.07,0.056,0.183,0.083,0.324,0.083 c0.492,0,1.32-0.365,1.953-1.063c0.527-0.578,0.779-1.239,0.721-1.865c-0.086-0.908-1.024-2.052-1.805-2.468 c-3.143-1.674-9.311-3.149-11.625-3.397C409.766,1012.798,409.688,1012.788,409.591,1012.775z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M419.768,1022.044c-3.107-1.652-8.957-3.922-12.478-5.003c-1.706-0.522-3.661-1.059-5.237-1.059 c-2.047,0-2.441,0.9-2.566,1.58c-0.227,1.293,3.086,2.999,5.988,3.548c6.516,1.237,10.582,4.149,11.92,5.106l0.15,0.11 c0.182,0.126,0.829,0.545,1.596,0.545c0.93,0,1.631-0.63,2.086-1.879C421.688,1023.721,420.486,1022.425,419.768,1022.044z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M416.934,1026.889c-4.83-2.79-10.217-4.592-13.18-5.086c-0.05-0.01-0.111-0.021-0.185-0.035 c-0.25-0.051-0.623-0.125-1.018-0.125c-1.033,0-1.646,0.512-1.877,1.563c-0.099,0.464,0.428,1.486,5.104,3.104 c0.232,0.082,0.394,0.137,0.453,0.166c1.777,0.799,3.117,1.691,4.416,2.559c1.408,0.941,2.869,1.912,4.947,2.818 c0.137,0.059,0.303,0.09,0.487,0.09c0.98,0,2.369-0.824,2.394-2.662C418.486,1028.359,417.684,1027.32,416.934,1026.889z\" />\n    <path\n        android:fillColor=\"#82A6F5\"\n        android:pathData=\"M406.559,1027.88l-0.092-0.039c-1.291-0.578-2.271-0.869-2.902-0.869c-0.182,0-0.422,0.024-0.535,0.147 c-0.309,0.338-0.44,0.666-0.401,1.008c0.1,0.851,1.25,1.619,1.678,1.908c0.059,0.039,0.11,0.071,0.146,0.099 c0.619,0.44,1.172,0.946,1.761,1.485c1.295,1.183,2.633,2.406,4.813,2.993c0.234,0.065,0.461,0.175,0.682,0.282 c0.324,0.16,0.662,0.328,1,0.328c0.441,0,0.89-0.292,1.369-0.887c0.259-0.324,0.371-0.626,0.338-0.894 c-0.038-0.274-0.235-0.551-0.594-0.811c-0.514-0.381-0.977-0.729-1.406-1.046C409.923,1029.73,408.669,1028.796,406.559,1027.88z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/searchview_line.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    android:shape=\"rectangle\" >\r\n\r\n    <!-- 表示shape的四个角的角度。只适用于矩形shape，这里的角度是指圆角的程度 -->\r\n    <corners android:radius=\"50dp\" />\r\n\r\n    <!-- 这个标签表示纯色填充，通过android:color即可指定shape中填充的颜色 -->\r\n    <solid android:color=\"@color/white\" />\r\n\r\n    <!-- Shape的描边，下面指定了描边的宽度和描边的颜色 -->\r\n    <stroke\r\n        android:width=\"1dp\"\r\n        android:color=\"#d9d9d9\" />\r\n\r\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/selector_common_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:state_pressed=\"true\"\n          android:drawable=\"@color/btn_bg_press\"/>\n    <item android:state_selected=\"true\"\n          android:drawable=\"@color/btn_bg_press\"/>\n\n    <item\n        android:drawable=\"@color/background\"/>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/selector_fillet_btn_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:state_pressed=\"false\" android:drawable=\"@drawable/shape_fillet_btn\" />\n    <item android:state_pressed=\"true\" android:drawable=\"@drawable/shape_fillet_btn_press\" />\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/selector_tv_black.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/tv_btn_normal_black\" android:state_pressed=\"false\" android:state_enabled=\"true\"/>\n    <item android:color=\"@color/tv_btn_press_black\" android:state_pressed=\"true\" android:state_enabled=\"true\"/>\n    <item android:color=\"@color/tv_btn_press_black\" android:state_enabled=\"false\"/>\n</selector>"
  },
  {
    "path": "app/src/main/res/drawable/shape_card_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <corners android:radius=\"3dp\" />\n    <solid android:color=\"@color/btn_bg_press\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/shape_fillet_btn.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <corners android:radius=\"3dp\" />\n    <solid android:color=\"@color/btn_bg_press\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/shape_fillet_btn_press.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <corners android:radius=\"3dp\" />\n    <solid android:color=\"@color/btn_bg_press_2\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/shape_pop_checkaddshelf_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <corners android:radius=\"15dp\"/>\n    <solid android:color=\"#0000\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/shape_radius_1dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners android:radius=\"1dp\"/>\n    <solid android:color=\"#00ffffff\"/>\n    <stroke android:width=\"1dp\"\n        android:color=\"@color/tv_text_secondary\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/shape_space_divider.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <size\n        android:width=\"8dp\"\n        android:height=\"8dp\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/shape_text_cursor.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <size android:width=\"1.5dp\" />\n    <solid android:color=\"#3B96FC\" />\n    <corners android:radius=\"1dp\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable-v21/bg_ib_pre.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/btn_bg_press_2\">\n    <item android:id=\"@android:id/mask\" android:drawable=\"@color/btn_bg_press_2\" />\n</ripple>"
  },
  {
    "path": "app/src/main/res/drawable-v21/bg_ib_pre_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/btn_bg_press\" />"
  },
  {
    "path": "app/src/main/res/layout/activity_about.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@color/colorPrimary\"\n        android:fitsSystemWindows=\"true\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_margin=\"5dp\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n\n            <androidx.cardview.widget.CardView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"6dp\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\"\n                    android:padding=\"10dp\">\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:text=\"@string/app_name\"\n                        android:textSize=\"20sp\"\n                        android:textStyle=\"bold\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_app_summary\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:text=\"@string/about_description\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_version\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_version\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_version\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/version_name\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_share\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_share\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_share\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/share_app\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_update\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_update\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_update\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/check_update\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_update_log\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_list\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_update_log\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/update_log\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_faq\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_faq\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_faq\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/faq\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_qq\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_qq_group\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_qq\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/join_qq_group\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_donate\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_donate\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_donate\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/donate\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_scoring\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_scoring\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_scoring\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/scoring\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_mail\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_mail\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_mail\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/send_mail\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_git\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_launch\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_git\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/git_hub\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_home_page\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_launch\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_home_page\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/home_page\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_disclaimer\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_margin=\"6dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\">\n\n                    <ImageView\n                        android:layout_width=\"24dp\"\n                        android:layout_height=\"24dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:src=\"@drawable/ic_disclaimer\"\n                        app:tint=\"@color/tv_text_secondary\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_disclaimer\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:gravity=\"center_vertical\"\n                        android:text=\"@string/disclaimer\" />\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n        </LinearLayout>\n    </ScrollView>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_book_choice.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            android:transitionName=\"sharedView\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerView\n            android:id=\"@+id/rfRv_search_books\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            app:font_color=\"#767676\"\n            app:second_color=\"#c1c1c1\"\n            app:second_max_progress=\"80dp\"\n            app:speed=\"2dp\" />\n\n        <View\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"20dp\"\n            android:layout_gravity=\"top\"\n            android:background=\"@drawable/bg_shadow_top\" />\n    </FrameLayout>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_book_cover_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:id=\"@+id/ll_content\"\n    android:background=\"@color/background\"\n    android:orientation=\"vertical\"\n    tools:context=\"com.kunfei.bookshelf.view.activity.BookCoverEditActivity\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n        android:id=\"@+id/swipe_refresh_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rf_rv_change_cover\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n\n    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_book_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"#77000000\"\n    android:clickable=\"true\"\n    android:fillViewport=\"true\"\n    android:fitsSystemWindows=\"true\"\n    android:focusable=\"true\">\n\n    <LinearLayout\n        android:id=\"@+id/ifl_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <Space\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\" />\n\n        <androidx.cardview.widget.CardView\n            android:id=\"@+id/book_info_main\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"400dp\"\n            android:layout_gravity=\"center\"\n            android:layout_margin=\"30dp\"\n            app:cardCornerRadius=\"5dp\"\n            app:cardElevation=\"10dp\">\n\n            <com.kunfei.bookshelf.widget.image.FilletImageView\n                android:id=\"@+id/iv_blur_cover\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:scaleType=\"centerCrop\"\n                tools:ignore=\"ContentDescription\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"#88000000\"\n                android:orientation=\"vertical\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:gravity=\"center_vertical\"\n                    android:orientation=\"horizontal\">\n\n                    <com.kunfei.bookshelf.widget.image.CoverImageView\n                        android:id=\"@+id/iv_cover\"\n                        android:layout_width=\"92dp\"\n                        android:layout_height=\"130dp\"\n                        android:layout_margin=\"3dp\"\n                        android:contentDescription=\"@string/img_cover\"\n                        android:scaleType=\"centerCrop\"\n                        android:src=\"@drawable/image_cover_default\" />\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:orientation=\"vertical\"\n                        android:paddingLeft=\"10dp\"\n                        android:paddingTop=\"3dp\"\n                        android:paddingRight=\"10dp\"\n                        android:paddingBottom=\"3dp\">\n\n                        <LinearLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:paddingTop=\"3dp\"\n                            android:paddingBottom=\"3dp\"\n                            tools:ignore=\"UseCompoundDrawables\">\n\n                            <TextView\n                                android:id=\"@+id/tv_name\"\n                                android:layout_width=\"0dp\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_marginRight=\"5dp\"\n                                android:layout_weight=\"1\"\n                                android:gravity=\"center|start\"\n                                android:includeFontPadding=\"false\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/book_name\"\n                                android:textColor=\"@color/white\"\n                                android:textSize=\"16sp\"\n                                tools:ignore=\"RtlHardcoded\" />\n\n                            <ImageView\n                                android:id=\"@+id/iv_menu\"\n                                android:layout_width=\"20dp\"\n                                android:layout_height=\"20dp\"\n                                android:contentDescription=\"@string/menu\"\n                                android:src=\"@drawable/ic_more_vert\"\n                                app:tint=\"@android:color/white\" />\n\n                        </LinearLayout>\n\n                        <LinearLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:gravity=\"center_vertical\"\n                            android:orientation=\"horizontal\"\n                            android:paddingTop=\"3dp\"\n                            android:paddingBottom=\"3dp\">\n\n                            <ImageView\n                                android:layout_width=\"18sp\"\n                                android:layout_height=\"18sp\"\n                                android:contentDescription=\"@string/author\"\n                                android:paddingRight=\"2dp\"\n                                android:src=\"@drawable/ic_author\"\n                                app:tint=\"@color/tv_text_book_detail\"\n                                tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n                            <TextView\n                                android:id=\"@+id/tv_author\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"1\"\n                                android:ellipsize=\"end\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingRight=\"6dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/author\"\n                                android:textColor=\"@color/tv_text_book_detail\"\n                                android:textSize=\"13sp\"\n                                tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n                            <View\n                                android:layout_width=\"0dp\"\n                                android:layout_height=\"match_parent\"\n                                android:layout_weight=\"1\"\n                                android:background=\"@color/transparent\" />\n\n                        </LinearLayout>\n\n                        <LinearLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:gravity=\"center_vertical\"\n                            android:orientation=\"horizontal\"\n                            android:paddingTop=\"3dp\"\n                            android:paddingBottom=\"3dp\">\n\n                            <ImageView\n                                android:id=\"@+id/iv_web\"\n                                android:layout_width=\"18sp\"\n                                android:layout_height=\"18sp\"\n                                android:contentDescription=\"@string/origin\"\n                                android:paddingRight=\"2dp\"\n                                android:src=\"@drawable/ic_web_outline\"\n                                app:tint=\"@color/tv_text_book_detail\"\n                                tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n                            <TextView\n                                android:id=\"@+id/tv_origin\"\n                                android:layout_width=\"0dp\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"1\"\n                                android:ellipsize=\"end\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingRight=\"6dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/origin\"\n                                android:textColor=\"@color/tv_text_book_detail\"\n                                android:textSize=\"13sp\"\n                                tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n                            <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                                android:id=\"@+id/tv_change_origin\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingStart=\"4dp\"\n                                android:paddingTop=\"1dp\"\n                                android:paddingEnd=\"4dp\"\n                                android:paddingBottom=\"1dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/change_origin\"\n                                android:textSize=\"13sp\" />\n\n                        </LinearLayout>\n\n                        <LinearLayout\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:gravity=\"center_vertical\"\n                            android:orientation=\"horizontal\"\n                            android:paddingTop=\"3dp\"\n                            android:paddingBottom=\"3dp\">\n\n                            <ImageView\n                                android:id=\"@+id/iv_last\"\n                                android:layout_width=\"18sp\"\n                                android:layout_height=\"18sp\"\n                                android:contentDescription=\"@string/last_read\"\n                                android:paddingRight=\"2dp\"\n                                android:src=\"@drawable/ic_book_last\"\n                                app:tint=\"@color/tv_text_book_detail\"\n                                tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n                            <TextView\n                                android:id=\"@+id/tv_chapter\"\n                                android:layout_width=\"0dp\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_weight=\"1\"\n                                android:ellipsize=\"end\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingTop=\"3dp\"\n                                android:paddingBottom=\"3dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/read_dur_progress\"\n                                android:textColor=\"@color/tv_text_book_detail\"\n                                android:textSize=\"13sp\" />\n\n                            <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                                android:id=\"@+id/tv_toc\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingStart=\"4dp\"\n                                android:paddingTop=\"1dp\"\n                                android:paddingEnd=\"4dp\"\n                                android:paddingBottom=\"1dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/chapter_list\"\n                                android:textSize=\"13sp\" />\n                        </LinearLayout>\n\n                        <RadioGroup\n                            android:id=\"@+id/rg_book_group\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:orientation=\"horizontal\"\n                            android:paddingTop=\"3dp\"\n                            android:paddingBottom=\"3dp\">\n\n                            <com.kunfei.bookshelf.widget.views.ATERadioNoButton\n                                android:id=\"@+id/rb_zg\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_marginRight=\"4dp\"\n                                android:button=\"@null\"\n                                android:checked=\"true\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingStart=\"4dp\"\n                                android:paddingTop=\"1dp\"\n                                android:paddingEnd=\"4dp\"\n                                android:paddingBottom=\"1dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/pursue_more\"\n                                android:textColor=\"@color/white\"\n                                android:textSize=\"13sp\"\n                                tools:ignore=\"RtlHardcoded\" />\n\n                            <com.kunfei.bookshelf.widget.views.ATERadioNoButton\n                                android:id=\"@+id/rb_yf\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_marginRight=\"4dp\"\n                                android:button=\"@null\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingStart=\"4dp\"\n                                android:paddingTop=\"1dp\"\n                                android:paddingEnd=\"4dp\"\n                                android:paddingBottom=\"1dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/fattening\"\n                                android:textColor=\"@color/white\"\n                                android:textSize=\"13sp\"\n                                tools:ignore=\"RtlHardcoded\" />\n\n                            <com.kunfei.bookshelf.widget.views.ATERadioNoButton\n                                android:id=\"@+id/rb_wj\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:layout_marginRight=\"4dp\"\n                                android:button=\"@null\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingStart=\"4dp\"\n                                android:paddingTop=\"1dp\"\n                                android:paddingEnd=\"4dp\"\n                                android:paddingBottom=\"1dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/finish\"\n                                android:textColor=\"@color/white\"\n                                android:textSize=\"13sp\"\n                                tools:ignore=\"RtlHardcoded\" />\n\n                            <com.kunfei.bookshelf.widget.views.ATERadioNoButton\n                                android:id=\"@+id/rb_bd\"\n                                android:layout_width=\"wrap_content\"\n                                android:layout_height=\"wrap_content\"\n                                android:button=\"@null\"\n                                android:includeFontPadding=\"false\"\n                                android:paddingStart=\"4dp\"\n                                android:paddingTop=\"1dp\"\n                                android:paddingEnd=\"4dp\"\n                                android:paddingBottom=\"1dp\"\n                                android:singleLine=\"true\"\n                                android:text=\"@string/local\"\n                                android:textColor=\"@color/white\"\n                                android:textSize=\"13sp\" />\n\n                        </RadioGroup>\n\n                    </LinearLayout>\n                </LinearLayout>\n\n                <View\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"0.5dp\"\n                    android:background=\"@color/btn_bg_press\" />\n\n                <com.kunfei.bookshelf.widget.ScrollTextView\n                    android:id=\"@+id/tv_intro\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"0dp\"\n                    android:layout_weight=\"1\"\n                    android:background=\"@color/background_menu\"\n                    android:clickable=\"true\"\n                    android:focusable=\"true\"\n                    android:paddingLeft=\"10dp\"\n                    android:paddingTop=\"5dp\"\n                    android:paddingRight=\"10dp\"\n                    android:paddingBottom=\"5dp\"\n                    android:scrollbarStyle=\"outsideInset\"\n                    android:scrollbars=\"vertical\"\n                    android:text=\"@string/book_intro\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"15sp\"\n                    android:visibility=\"invisible\" />\n\n                <View\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"0.5dp\"\n                    android:background=\"@color/btn_bg_press\" />\n\n                <FrameLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"50dp\">\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:background=\"@color/background_menu\"\n                        android:orientation=\"horizontal\"\n                        android:padding=\"3dp\">\n\n                        <TextView\n                            android:id=\"@+id/tv_shelf\"\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_weight=\"1\"\n                            android:background=\"@drawable/selector_fillet_btn_bg\"\n                            android:clickable=\"true\"\n                            android:focusable=\"true\"\n                            android:gravity=\"center\"\n                            android:includeFontPadding=\"false\"\n                            android:text=\"@string/remove_from_bookshelf\"\n                            android:textColor=\"@color/tv_text_default\"\n                            android:textSize=\"15sp\" />\n\n                        <Space\n                            android:layout_width=\"3dp\"\n                            android:layout_height=\"match_parent\" />\n\n                        <TextView\n                            android:id=\"@+id/tv_read\"\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_weight=\"1\"\n                            android:background=\"@drawable/selector_fillet_btn_bg\"\n                            android:clickable=\"true\"\n                            android:focusable=\"true\"\n                            android:gravity=\"center\"\n                            android:includeFontPadding=\"false\"\n                            android:text=\"@string/start_read\"\n                            android:textColor=\"@color/tv_text_default\"\n                            android:textSize=\"15sp\" />\n\n                    </LinearLayout>\n\n                    <TextView\n                        android:id=\"@+id/tv_loading\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:background=\"@color/tv_text_button_nor\"\n                        android:clickable=\"true\"\n                        android:focusable=\"true\"\n                        android:gravity=\"center\"\n                        android:text=\"@string/data_loading\"\n                        android:textColor=\"#767676\"\n                        android:textSize=\"16sp\"\n                        android:visibility=\"gone\" />\n                </FrameLayout>\n            </LinearLayout>\n\n        </androidx.cardview.widget.CardView>\n\n        <Space\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\" />\n    </LinearLayout>\n\n</ScrollView>"
  },
  {
    "path": "app/src/main/res/layout/activity_book_info_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:padding=\"5dp\">\n\n                <com.kunfei.bookshelf.widget.image.CoverImageView\n                    android:id=\"@+id/iv_cover\"\n                    android:layout_width=\"90dp\"\n                    android:layout_height=\"126dp\"\n                    android:contentDescription=\"@string/img_cover\"\n                    android:scaleType=\"centerCrop\"\n                    android:src=\"@drawable/image_cover_default\" />\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:orientation=\"vertical\"\n                    android:padding=\"5dp\">\n\n                    <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n                        android:id=\"@+id/til_book_name\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/book_name\">\n\n                        <com.kunfei.bookshelf.widget.views.ATEEditText\n                            android:id=\"@+id/tie_book_name\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:singleLine=\"true\" />\n                    </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n                    <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n                        android:id=\"@+id/til_book_author\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:hint=\"@string/author\">\n\n                        <com.kunfei.bookshelf.widget.views.ATEEditText\n                            android:id=\"@+id/tie_book_author\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"wrap_content\"\n                            android:singleLine=\"true\" />\n                    </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n                </LinearLayout>\n            </LinearLayout>\n\n            <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n                android:id=\"@+id/til_cover_url\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"5dp\"\n                android:hint=\"@string/cover_path\">\n\n                <com.kunfei.bookshelf.widget.views.ATEEditText\n                    android:id=\"@+id/tie_cover_url\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\" />\n            </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\"\n                android:paddingStart=\"5dp\"\n                android:paddingEnd=\"5dp\">\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tv_select_cover\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"5dp\"\n                    android:text=\"@string/select_local_image\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tv_change_cover\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginLeft=\"10dp\"\n                    android:padding=\"5dp\"\n                    android:text=\"@string/cover_change_source\"\n                    tools:ignore=\"RtlHardcoded\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tv_refresh_cover\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginLeft=\"10dp\"\n                    android:padding=\"5dp\"\n                    android:text=\"@string/refresh_cover\"\n                    tools:ignore=\"RtlHardcoded\" />\n\n            </LinearLayout>\n\n            <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n                android:id=\"@+id/til_book_jj\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"10dp\"\n                android:hint=\"@string/book_intro\">\n\n                <com.kunfei.bookshelf.widget.views.ATEEditText\n                    android:id=\"@+id/tie_book_jj\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\" />\n            </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n        </LinearLayout>\n    </ScrollView>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_book_read.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/fl_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <!--阅读页面-->\n    <com.kunfei.bookshelf.widget.page.PageView\n        android:id=\"@+id/pageView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        tools:visibility=\"gone\" />\n\n    <com.kunfei.bookshelf.widget.seekbar.VerticalSeekBarWrapper\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_marginTop=\"30dp\"\n        android:layout_marginBottom=\"20dp\"\n        tools:ignore=\"RtlHardcoded\">\n\n        <com.kunfei.bookshelf.widget.seekbar.VerticalSeekBar\n            android:id=\"@+id/pb_nextPage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:visibility=\"invisible\"\n            app:seekBarRotation=\"CW270\" />\n\n    </com.kunfei.bookshelf.widget.seekbar.VerticalSeekBarWrapper>\n\n    <ImageView\n        android:id=\"@+id/cursor_left\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        android:src=\"@drawable/ic_cursor_left\"\n        android:visibility=\"invisible\" />\n\n    <ImageView\n        android:id=\"@+id/cursor_right\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:contentDescription=\"@null\"\n        android:src=\"@drawable/ic_cursor_right\"\n        android:visibility=\"invisible\" />\n\n    <com.kunfei.bookshelf.view.popupwindow.ReadLongPressPop\n        android:id=\"@+id/readLongPress\"\n        android:layout_width=\"190dp\"\n        android:layout_height=\"30dp\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"invisible\" />\n\n    <com.kunfei.bookshelf.view.popupwindow.MediaPlayerPop\n        android:id=\"@+id/mediaPlayerPop\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"gone\" />\n\n    <FrameLayout\n        android:id=\"@+id/fl_menu\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:visibility=\"invisible\">\n\n        <View\n            android:id=\"@+id/v_menu_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"#00000000\"\n            android:clickable=\"true\"\n            android:focusable=\"true\" />\n\n        <LinearLayout\n            android:id=\"@+id/ll_menu_top\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            android:visibility=\"invisible\">\n\n            <LinearLayout\n                android:id=\"@+id/ll_ISB\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/colorPrimary\"\n                android:orientation=\"vertical\">\n\n                <com.google.android.material.appbar.AppBarLayout\n                    android:id=\"@+id/appBar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:theme=\"?attr/actionBarStyle\">\n\n                    <androidx.appcompat.widget.Toolbar\n                        android:id=\"@+id/toolbar\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"?attr/actionBarSize\"\n                        android:background=\"?attr/colorPrimary\"\n                        app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"1dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                    <androidx.constraintlayout.widget.ConstraintLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\">\n\n                        <TextView\n                            android:id=\"@+id/tv_chapter_name\"\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"wrap_content\"\n                            android:background=\"@color/colorPrimary\"\n                            android:includeFontPadding=\"false\"\n                            android:paddingStart=\"10dp\"\n                            android:paddingTop=\"2dp\"\n                            android:paddingEnd=\"10dp\"\n                            android:paddingBottom=\"2dp\"\n                            android:singleLine=\"true\"\n                            android:textColor=\"@color/tv_text_default\"\n                            app:layout_constraintTop_toTopOf=\"parent\"\n                            app:layout_constraintLeft_toLeftOf=\"parent\"\n                            app:layout_constraintRight_toLeftOf=\"@+id/login\" />\n\n                        <TextView\n                            android:id=\"@+id/tv_chapter_url\"\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"wrap_content\"\n                            android:background=\"@color/colorPrimary\"\n                            android:includeFontPadding=\"false\"\n                            android:paddingStart=\"10dp\"\n                            android:paddingTop=\"2dp\"\n                            android:paddingEnd=\"10dp\"\n                            android:paddingBottom=\"2dp\"\n                            android:singleLine=\"true\"\n                            android:textColor=\"@color/tv_text_default\"\n                            app:layout_constraintTop_toBottomOf=\"@+id/tv_chapter_name\"\n                            app:layout_constraintLeft_toLeftOf=\"parent\"\n                            app:layout_constraintRight_toLeftOf=\"@+id/login\" />\n\n                        <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                            android:id=\"@+id/login\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"0dp\"\n                            android:padding=\"6dp\"\n                            android:visibility=\"gone\"\n                            android:text=\"@string/login\"\n                            android:gravity=\"center\"\n                            app:layout_constraintRight_toLeftOf=\"@+id/pay\"\n                            app:layout_constraintTop_toTopOf=\"parent\"\n                            app:layout_constraintBottom_toBottomOf=\"parent\" />\n\n                        <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                            android:id=\"@+id/pay\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"0dp\"\n                            android:padding=\"6dp\"\n                            android:visibility=\"gone\"\n                            android:text=\"@string/chapter_pay\"\n                            android:gravity=\"center\"\n                            app:layout_constraintRight_toRightOf=\"parent\"\n                            app:layout_constraintTop_toTopOf=\"parent\"\n                            app:layout_constraintBottom_toBottomOf=\"parent\" />\n\n                    </androidx.constraintlayout.widget.ConstraintLayout>\n\n                    <View\n                        android:id=\"@+id/atv_line\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"1dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </com.google.android.material.appbar.AppBarLayout>\n\n            </LinearLayout>\n\n            <View style=\"@style/Style.Shadow.Top\" />\n        </LinearLayout>\n\n        <com.kunfei.bookshelf.view.popupwindow.ReadBottomMenu\n            android:id=\"@+id/read_menu_bottom\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:visibility=\"invisible\" />\n\n        <com.kunfei.bookshelf.view.popupwindow.ReadAdjustPop\n            android:id=\"@+id/readAdjustPop\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:visibility=\"invisible\" />\n\n        <com.kunfei.bookshelf.view.popupwindow.ReadInterfacePop\n            android:id=\"@+id/readInterfacePop\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:visibility=\"invisible\" />\n\n        <com.kunfei.bookshelf.view.popupwindow.MoreSettingPop\n            android:id=\"@+id/moreSettingPop\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:visibility=\"invisible\" />\n\n        <com.kunfei.bookshelf.view.popupwindow.ReadAdjustMarginPop\n            android:id=\"@+id/readAdjustMarginPop\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:visibility=\"invisible\" />\n\n    </FrameLayout>\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_book_source.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\">\n\n            <androidx.appcompat.widget.SearchView\n                android:id=\"@+id/searchView\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:imeOptions=\"actionSearch\"\n                app:iconifiedByDefault=\"false\"\n                app:searchIcon=\"@null\"\n                app:defaultQueryHint=\"搜索\" />\n\n        </androidx.appcompat.widget.Toolbar>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.kunfei.bookshelf.widget.recycler.scroller.FastScrollRecyclerView\n            android:id=\"@+id/recycler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n    </RelativeLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_chapterlist.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:clickable=\"true\"\n    android:fitsSystemWindows=\"true\"\n    android:focusable=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"@color/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\">\n\n            <com.google.android.material.tabs.TabLayout\n                android:id=\"@+id/tab_tl_indicator\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:layout_gravity=\"bottom\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginRight=\"10dp\" />\n\n        </androidx.appcompat.widget.Toolbar>\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.viewpager.widget.ViewPager\n        android:id=\"@+id/tab_vp\"\n        android:layout_below=\"@id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <View\n        style=\"@style/Style.Shadow.Top\"\n        android:layout_below=\"@id/action_bar\" />\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_donate.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_margin=\"5dp\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n\n            <androidx.cardview.widget.CardView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_margin=\"10dp\"\n                        android:text=\"你的支持是我更新的动力\" />\n\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/cv_wx_gzh\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"vertical\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_margin=\"10dp\"\n                        android:text=\"请关注微信公众号 开源阅读 点击复制\" />\n                </LinearLayout>\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_zfb_hb_ssm\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"5dp\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:text=\"支付宝红包搜索码 537954522 点击复制\" />\n\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_zfb_tz\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"5dp\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:text=\"支付宝收款,支持红包,点击直接跳转支付宝\" />\n\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_zfb_hb\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"5dp\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:text=\"支付宝红包二维码\" />\n\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_zfb_rwm\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"5dp\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:text=\"支付宝收款二维码\" />\n\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_wx_rwm\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"5dp\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:text=\"微信赞赏码\" />\n\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/vw_qq_rwm\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"5dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:foreground=\"?attr/selectableItemBackground\"\n                app:cardBackgroundColor=\"@color/background_card\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"5dp\">\n\n                    <TextView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_margin=\"5dp\"\n                        android:text=\"QQ收款码\" />\n\n                </LinearLayout>\n\n            </androidx.cardview.widget.CardView>\n\n        </LinearLayout>\n    </ScrollView>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_import_book.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"@color/background\"\n            android:theme=\"?attr/actionBarStyle\" />\n\n        <com.google.android.material.tabs.TabLayout\n            android:id=\"@+id/tab_tl_indicator\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"32dp\"\n            android:background=\"@color/background\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <!--文件点击按钮-->\n    <RelativeLayout\n        android:id=\"@+id/rl_bottom\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"52dp\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentBottom=\"true\"\n        android:gravity=\"center_vertical\"\n        android:paddingRight=\"10dp\"\n        tools:ignore=\"RtlHardcoded,RtlSymmetry\">\n\n        <com.kunfei.bookshelf.widget.views.ATECheckBox\n            android:id=\"@+id/file_system_cb_selected_all\"\n            android:layout_width=\"120dp\"\n            android:layout_height=\"40dp\"\n            android:layout_centerVertical=\"true\"\n            android:layout_marginStart=\"10dp\"\n            android:text=\"@string/select_all\"\n            android:textColor=\"@color/tv_text_default\" />\n\n        <com.kunfei.bookshelf.widget.views.ATEAccentStrokeTextView\n            android:id=\"@+id/file_system_btn_add_book\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"35dp\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:gravity=\"center\"\n            android:minWidth=\"110dp\"\n            android:text=\"加入书架\" />\n\n        <com.kunfei.bookshelf.widget.views.ATEAccentStrokeTextView\n            android:id=\"@+id/file_system_btn_delete\"\n            android:layout_width=\"70dp\"\n            android:layout_height=\"35dp\"\n            android:layout_centerVertical=\"true\"\n            android:gravity=\"center\"\n            android:layout_marginRight=\"15dp\"\n            android:layout_toLeftOf=\"@id/file_system_btn_add_book\"\n            android:text=\"删除\" />\n    </RelativeLayout>\n\n    <androidx.viewpager.widget.ViewPager\n        android:id=\"@+id/tab_vp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_above=\"@+id/rl_bottom\"\n        android:layout_below=\"@+id/action_bar\" />\n\n    <View\n        android:id=\"@+id/v_shadow\"\n        style=\"@style/Style.Shadow.Bottom\"\n        android:layout_above=\"@+id/rl_bottom\" />\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/ll_content\"\n    android:fitsSystemWindows=\"true\"\n    android:layout_height=\"match_parent\"\n    android:layout_width=\"match_parent\">\n\n    <androidx.drawerlayout.widget.DrawerLayout\n        android:id=\"@+id/drawer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <include\n            android:id=\"@+id/main_view\"\n            layout=\"@layout/content_main\" />\n\n        <com.google.android.material.navigation.NavigationView\n            android:id=\"@+id/navigation_view\"\n            android:layout_width=\"230dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"start\"\n            android:background=\"@color/background_menu\"\n            app:menu=\"@menu/menu_main_drawer\" />\n\n    </androidx.drawerlayout.widget.DrawerLayout>\n</LinearLayout>\n\n"
  },
  {
    "path": "app/src/main/res/layout/activity_qrcode_capture.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\">\n\n    <cn.bingoogolapple.qrcode.zxing.ZXingView\n        android:id=\"@+id/zxingview\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:visibility=\"gone\"\n        app:qrcv_animTime=\"1000\"\n        app:qrcv_barCodeTipText=\"将条码放入框内，即可自动扫描\"\n        app:qrcv_barcodeRectHeight=\"120dp\"\n        app:qrcv_borderColor=\"@android:color/white\"\n        app:qrcv_borderSize=\"1dp\"\n        app:qrcv_cornerColor=\"@color/colorPrimaryDark\"\n        app:qrcv_cornerDisplayType=\"center\"\n        app:qrcv_cornerLength=\"20dp\"\n        app:qrcv_cornerSize=\"3dp\"\n        app:qrcv_isAutoZoom=\"true\"\n        app:qrcv_isBarcode=\"false\"\n        app:qrcv_isOnlyDecodeScanBoxArea=\"false\"\n        app:qrcv_isScanLineReverse=\"true\"\n        app:qrcv_isShowDefaultGridScanLineDrawable=\"false\"\n        app:qrcv_isShowDefaultScanLineDrawable=\"true\"\n        app:qrcv_isShowLocationPoint=\"true\"\n        app:qrcv_isShowTipBackground=\"true\"\n        app:qrcv_isShowTipTextAsSingleLine=\"false\"\n        app:qrcv_isTipTextBelowRect=\"false\"\n        app:qrcv_maskColor=\"#33FFFFFF\"\n        app:qrcv_qrCodeTipText=\"将二维码放入框内，即可自动扫描\"\n        app:qrcv_rectWidth=\"300dp\"\n        app:qrcv_scanLineColor=\"@color/colorPrimaryDark\"\n        app:qrcv_scanLineMargin=\"0dp\"\n        app:qrcv_scanLineSize=\"0.5dp\"\n        app:qrcv_tipTextColor=\"@android:color/white\"\n        app:qrcv_tipTextSize=\"12sp\"\n        app:qrcv_toolbarHeight=\"?attr/actionBarSize\"\n        app:qrcv_topOffset=\"65dp\"\n        app:qrcv_verticalBias=\"-1\" />\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@color/colorPrimary\"\n        android:fitsSystemWindows=\"true\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentStart=\"true\"\n        android:layout_alignParentBottom=\"true\"\n        android:padding=\"16dp\">\n\n        <Space\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fab_flashlight\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"16dp\"\n            android:contentDescription=\"@string/read_aloud\"\n            android:src=\"@drawable/ic_daytime\"\n            android:tint=\"@color/tv_text_default\"\n            app:backgroundTint=\"@color/background_menu\"\n            app:elevation=\"2dp\"\n            app:fabSize=\"mini\"\n            app:pressedTranslationZ=\"2dp\" />\n\n        <Space\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\" />\n\n    </LinearLayout>\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_read_style.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"com.kunfei.bookshelf.view.activity.ReadStyleActivity\">\n\n    <LinearLayout\n        android:id=\"@+id/ll_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/tv_content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:includeFontPadding=\"false\"\n            android:lineSpacingExtra=\"5dp\"\n            android:layout_margin=\"16dp\"\n            android:lineSpacingMultiplier=\"1\"\n            android:text=\"@string/content_sl\"\n            android:textSize=\"19sp\" />\n\n    </LinearLayout>\n\n    <LinearLayout\n        android:id=\"@+id/ll_bottom\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@color/background_menu\"\n        android:layout_gravity=\"bottom\"\n        android:orientation=\"vertical\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:padding=\"6dp\">\n\n            <com.kunfei.bookshelf.widget.views.ATESwitch\n                android:id=\"@+id/sw_darkStatusIcon\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"6dp\"\n                android:layout_marginRight=\"6dp\"\n                android:text=\"@string/darkStatusIcon\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:padding=\"6dp\">\n\n            <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                android:id=\"@+id/tvSelectTextColor\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:layout_margin=\"6dp\"\n                android:layout_weight=\"1\"\n                android:singleLine=\"true\"\n                android:gravity=\"center\"\n                android:padding=\"6dp\"\n                android:text=\"文字颜色\"\n                android:textSize=\"14sp\"\n                tools:ignore=\"HardcodedText\" />\n\n            <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                android:id=\"@+id/tvSelectBgColor\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:layout_margin=\"6dp\"\n                android:layout_weight=\"1\"\n                android:singleLine=\"true\"\n                android:gravity=\"center\"\n                android:padding=\"6dp\"\n                android:text=\"背景颜色\"\n                android:textSize=\"14sp\"\n                tools:ignore=\"HardcodedText\" />\n\n            <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                android:id=\"@+id/tvSelectBgImage\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:layout_margin=\"6dp\"\n                android:layout_weight=\"1\"\n                android:singleLine=\"true\"\n                android:gravity=\"center\"\n                android:padding=\"6dp\"\n                android:text=\"背景图片\"\n                android:visibility=\"gone\"\n                android:textSize=\"14sp\"\n                tools:ignore=\"HardcodedText\" />\n\n            <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                android:id=\"@+id/tvDefault\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:layout_margin=\"6dp\"\n                android:layout_weight=\"1\"\n                android:singleLine=\"true\"\n                android:gravity=\"center\"\n                android:padding=\"6dp\"\n                android:text=\"恢复默认\"\n                android:textSize=\"14sp\"\n                tools:ignore=\"HardcodedText\" />\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            android:clickable=\"true\"\n            android:padding=\"6dp\">\n\n            <TextView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:paddingStart=\"10dp\"\n                android:paddingEnd=\"10dp\"\n                android:text=\"背景图片\" />\n\n            <com.kunfei.bookshelf.widget.HorizontalListView\n                android:id=\"@+id/bgImgList\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"100dp\" />\n\n        </LinearLayout>\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:theme=\"?attr/actionBarStyle\">\n\n            <androidx.appcompat.widget.Toolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                android:background=\"@color/background_menu\"\n                app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n        </com.google.android.material.appbar.AppBarLayout>\n    </LinearLayout>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_recycler_vew.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.kunfei.bookshelf.widget.recycler.scroller.FastScrollRecyclerView\n            android:id=\"@+id/recycler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n    </RelativeLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_search_book.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            style=\"@style/NoPaddingToolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            android:transitionName=\"sharedView\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\">\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/card_search\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"30dp\"\n                app:cardBackgroundColor=\"@color/background_card\"\n                app:cardElevation=\"0dp\">\n\n                <androidx.appcompat.widget.SearchView\n                    android:id=\"@+id/searchView\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginLeft=\"-5dp\"\n                    android:imeOptions=\"actionSearch\"\n                    app:iconifiedByDefault=\"false\"\n                    app:queryBackground=\"@null\"\n                    app:searchIcon=\"@null\"\n                    app:submitBackground=\"@null\"\n                    tools:ignore=\"RtlHardcoded,UnusedAttribute\" />\n            </androidx.cardview.widget.CardView>\n\n        </androidx.appcompat.widget.Toolbar>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerView\n            android:id=\"@+id/rfRv_search_books\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@color/background\"\n            app:bg_color=\"#00000000\"\n            app:font_color=\"#767676\"\n            app:second_color=\"#c1c1c1\"\n            app:second_max_progress=\"80dp\"\n            app:speed=\"2dp\" />\n\n        <LinearLayout\n            android:id=\"@+id/ll_search_history\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@color/background\"\n            android:orientation=\"vertical\"\n            android:visibility=\"invisible\">\n\n            <TextView\n                android:id=\"@+id/tv_bookshelf\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_marginLeft=\"18dp\"\n                android:layout_marginTop=\"10dp\"\n                android:layout_marginBottom=\"5dp\"\n                android:text=\"@string/bookshelf\"\n                android:textColor=\"#767676\"\n                android:textSize=\"16sp\"\n                android:visibility=\"gone\"\n                tools:ignore=\"RtlHardcoded\" />\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/rv_bookshelf\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:paddingLeft=\"18dp\"\n                android:paddingRight=\"18dp\"\n                android:visibility=\"gone\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginTop=\"15dp\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:layout_marginLeft=\"18dp\"\n                    android:layout_marginTop=\"5dp\"\n                    android:layout_marginBottom=\"5dp\"\n                    android:text=\"@string/searchHistory\"\n                    android:textColor=\"#767676\"\n                    android:textSize=\"16sp\"\n                    tools:ignore=\"RtlHardcoded\" />\n\n                <View\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"0dp\"\n                    android:layout_weight=\"1\" />\n\n                <TextView\n                    android:id=\"@+id/tv_search_history_clean\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:clickable=\"true\"\n                    android:focusable=\"true\"\n                    android:gravity=\"center\"\n                    android:paddingLeft=\"15dp\"\n                    android:paddingTop=\"5dp\"\n                    android:paddingRight=\"15dp\"\n                    android:paddingBottom=\"5dp\"\n                    android:text=\"@string/clear\"\n                    android:textColor=\"@drawable/selector_tv_black\"\n                    android:textSize=\"16sp\" />\n            </LinearLayout>\n\n            <ScrollView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <com.google.android.flexbox.FlexboxLayout\n                    android:id=\"@+id/tfl_search_history\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:clipToPadding=\"false\"\n                    android:overScrollMode=\"never\"\n                    android:padding=\"15dp\"\n                    app:dividerDrawable=\"@drawable/shape_space_divider\"\n                    app:flexDirection=\"row\"\n                    app:flexWrap=\"wrap\"\n                    app:showDivider=\"middle\" />\n            </ScrollView>\n\n        </LinearLayout>\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fabSearchStop\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_alignParentBottom=\"true\"\n            android:layout_marginEnd=\"30dp\"\n            android:layout_marginBottom=\"30dp\"\n            android:contentDescription=\"@string/stop\"\n            android:src=\"@drawable/ic_stop_black_24dp\"\n            app:elevation=\"5dp\"\n            app:fabSize=\"mini\"\n            app:layout_anchorGravity=\"right|bottom\" />\n\n    </RelativeLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:id=\"@+id/ll_content\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:id=\"@+id/settingsFrameLayout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_source_debug.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            android:transitionName=\"sharedView\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\">\n\n            <androidx.appcompat.widget.SearchView\n                android:id=\"@+id/searchView\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:imeOptions=\"actionSearch\"\n                tools:ignore=\"RtlHardcoded,UnusedAttribute\" />\n\n        </androidx.appcompat.widget.Toolbar>\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/recycler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:divider=\"@null\" />\n\n        <com.kunfei.bookshelf.widget.RotateLoading\n            android:id=\"@+id/loading\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_margin=\"6dp\"\n            app:loading_color=\"@color/colorAccent\"\n            app:loading_width=\"2dp\"\n            tools:ignore=\"RtlHardcoded\" />\n    </FrameLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_source_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:fitsSystemWindows=\"true\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <com.kunfei.bookshelf.widget.views.ATECheckBox\n            android:id=\"@+id/cb_is_enable\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:checked=\"true\"\n            android:text=\"@string/is_enable\" />\n\n        <com.kunfei.bookshelf.widget.views.ATECheckBox\n            android:id=\"@+id/cb_is_audio\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:checked=\"false\"\n            android:text=\"@string/audio\" />\n\n        <View\n            android:layout_width=\"0dp\"\n            android:layout_height=\"1dp\"\n            android:layout_weight=\"1\" />\n\n        <TextView\n            android:id=\"@+id/tv_edit_find\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:padding=\"3dp\"\n            android:layout_marginLeft=\"5dp\"\n            android:layout_marginRight=\"5dp\"\n            android:textColor=\"@color/tv_text_default\"\n            android:background=\"@drawable/selector_fillet_btn_bg\"\n            android:text=\"@string/edit_find\" />\n\n    </LinearLayout>\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_source_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <WebView\n        android:id=\"@+id/web_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_update.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <LinearLayout\n        android:id=\"@+id/ll_download\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:orientation=\"horizontal\"\n        android:padding=\"5dp\">\n\n        <TextView\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_weight=\"1\"\n            android:text=\"@string/download_update\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"14sp\" />\n\n        <TextView\n            android:id=\"@+id/tv_download_progress\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:text=\"0/100\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"14sp\"\n            tools:ignore=\"HardcodedText\" />\n\n    </LinearLayout>\n\n    <ProgressBar\n        android:id=\"@+id/pb_download\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"3dp\"\n        android:visibility=\"invisible\"\n        android:layout_margin=\"5dp\"\n        android:max=\"100\"\n        style=\"@android:style/Widget.ProgressBar.Horizontal\" />\n\n    <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n        android:id=\"@+id/tv_install_update\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:padding=\"5dp\"\n        android:layout_margin=\"5dp\"\n        android:gravity=\"center\"\n        android:text=\"安装更新\"\n        android:textSize=\"14sp\" />\n\n    <TextView\n        android:id=\"@+id/tv_markdown\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"10dp\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_web_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:transitionName=\"sharedView\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <WebView\n        android:id=\"@+id/web_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/white\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_welcome.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:fitsSystemWindows=\"true\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_gravity=\"center\"\n    android:background=\"@color/background\">\n\n    <ImageView\n        android:id=\"@+id/iv_bg\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:src=\"@drawable/image_welcome\"\n        android:scaleType=\"fitCenter\"\n        android:contentDescription=\"@string/welcome\" />\n\n    <TextView\n        android:id=\"@+id/tv_gzh\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"关注公众号[开源阅读]\\n看文章点广告支持作者\"\n        android:layout_marginBottom=\"32dp\"\n        android:gravity=\"center_horizontal\"\n        android:layout_gravity=\"bottom|center_horizontal\"\n        tools:ignore=\"HardcodedText\" />\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/content_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/main_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    app:layout_behavior=\"@string/appbar_scrolling_view_behavior\"\n    tools:showIn=\"@layout/activity_main\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            style=\"@style/NoPaddingToolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:background=\"?attr/colorPrimary\"\n            android:gravity=\"center_vertical\"\n            android:theme=\"?attr/actionBarStyle\"\n            android:transitionName=\"sharedView\"\n            app:layout_scrollFlags=\"scroll|enterAlways\"\n            app:popupTheme=\"@style/AppTheme.PopupOverlay\">\n\n            <androidx.cardview.widget.CardView\n                android:id=\"@+id/card_search\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"30dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                app:cardBackgroundColor=\"@color/background_card\"\n                app:cardElevation=\"0dp\">\n\n                <TextView\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginLeft=\"11dp\"\n                    android:gravity=\"center_vertical\"\n                    android:text=\"@string/search_book_key\"\n                    tools:ignore=\"RtlHardcoded\" />\n            </androidx.cardview.widget.CardView>\n        </androidx.appcompat.widget.Toolbar>\n\n        <com.google.android.material.tabs.TabLayout\n            android:id=\"@+id/tab_tl_indicator\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"36dp\"\n            android:layout_gravity=\"bottom\"\n            app:tabGravity=\"fill\"\n            app:tabIndicatorFullWidth=\"false\"\n            app:tabIndicatorGravity=\"bottom\"\n            app:tabMode=\"fixed\" />\n\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.viewpager.widget.ViewPager\n        android:id=\"@+id/tab_vp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:layout_behavior=\"@string/appbar_scrolling_view_behavior\" />\n\n</androidx.coordinatorlayout.widget.CoordinatorLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_bookmark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\"\n    android:orientation=\"vertical\"\n    android:padding=\"10dp\">\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:padding=\"10dp\"\n        android:text=\"@string/bookmark\"\n        android:textSize=\"20sp\" />\n\n    <TextView\n        android:id=\"@+id/tvChapterName\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"10dp\"\n        android:text=\"@string/bookmark\" />\n\n    <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n        android:id=\"@+id/til_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:hint=\"@string/content\"\n        android:padding=\"10dp\">\n\n        <com.kunfei.bookshelf.widget.views.ATEEditText\n            android:id=\"@+id/tie_content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:maxLines=\"3\" />\n    </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n    <TextView\n        android:id=\"@+id/tv_ok\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"10dp\"\n        android:background=\"@drawable/selector_fillet_btn_bg\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:gravity=\"center\"\n        android:padding=\"5dp\"\n        android:text=\"@string/ok\"\n        android:textColor=\"@color/tv_text_default\" />\n\n    <LinearLayout\n        android:id=\"@+id/llEdit\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <TextView\n            android:id=\"@+id/tv_save\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:layout_weight=\"1\"\n            android:background=\"@drawable/selector_fillet_btn_bg\"\n            android:clickable=\"true\"\n            android:focusable=\"true\"\n            android:gravity=\"center\"\n            android:padding=\"5dp\"\n            android:text=\"@string/action_save\"\n            android:textColor=\"@color/tv_text_default\" />\n\n        <TextView\n            android:id=\"@+id/tv_del\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:layout_weight=\"1\"\n            android:background=\"@drawable/selector_fillet_btn_bg\"\n            android:clickable=\"true\"\n            android:focusable=\"true\"\n            android:gravity=\"center\"\n            android:padding=\"5dp\"\n            android:text=\"@string/action_del\"\n            android:textColor=\"@color/tv_text_default\" />\n\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_change_source.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\"\n    android:orientation=\"vertical\"\n    android:padding=\"10dp\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"10dp\"\n        android:orientation=\"horizontal\">\n\n        <TextView\n            android:id=\"@+id/atv_title\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_marginLeft=\"5dp\"\n            android:layout_marginRight=\"5dp\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center|start\"\n            android:singleLine=\"true\"\n            android:textSize=\"16sp\" />\n\n        <androidx.appcompat.widget.AppCompatImageButton\n            android:id=\"@+id/ibt_stop\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:background=\"@drawable/bg_ib_pre_round\"\n            android:src=\"@drawable/ic_stop_black_24dp\"\n            android:tint=\"@color/tv_text_default\" />\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:background=\"@color/btn_bg_press\" />\n\n    <androidx.appcompat.widget.SearchView\n        android:id=\"@+id/searchView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:queryHint=\"@string/search\" />\n\n    <com.kunfei.bookshelf.widget.recycler.refresh.RefreshRecyclerView\n        android:id=\"@+id/rf_rv_change_source\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:bg_color=\"#00000000\"\n        app:font_color=\"#767676\"\n        app:second_color=\"#c1c1c1\"\n        app:second_max_progress=\"80dp\"\n        app:speed=\"2dp\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_download_choice.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\"\n    android:gravity=\"center_horizontal\"\n    android:orientation=\"vertical\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\"\n        android:orientation=\"vertical\"\n        android:paddingTop=\"15dp\"\n        android:paddingBottom=\"10dp\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"40dp\"\n            android:layout_gravity=\"center_horizontal\"\n            android:text=\"@string/download_offline\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"16sp\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"40dp\"\n            android:orientation=\"horizontal\">\n\n            <View\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_marginRight=\"5dp\"\n                android:text=\"@string/chapter_s\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"16sp\"\n                tools:ignore=\"RtlHardcoded\" />\n\n            <EditText\n                android:id=\"@+id/edt_start\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:background=\"@drawable/bg_edit\"\n                android:inputType=\"number\"\n                android:lines=\"1\"\n                android:maxLength=\"5\"\n                android:minWidth=\"60dp\"\n                android:paddingLeft=\"5dp\"\n                android:paddingTop=\"4dp\"\n                android:paddingRight=\"5dp\"\n                android:paddingBottom=\"4dp\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textCursorDrawable=\"@drawable/shape_text_cursor\"\n                android:textSize=\"14sp\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_marginLeft=\"5dp\"\n                android:layout_marginRight=\"5dp\"\n                android:text=\"@string/to\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"16sp\" />\n\n            <EditText\n                android:id=\"@+id/edt_end\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:background=\"@drawable/bg_edit\"\n                android:inputType=\"number\"\n                android:lines=\"1\"\n                android:maxLength=\"5\"\n                android:minWidth=\"60dp\"\n                android:paddingLeft=\"5dp\"\n                android:paddingTop=\"4dp\"\n                android:paddingRight=\"5dp\"\n                android:paddingBottom=\"4dp\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textCursorDrawable=\"@drawable/shape_text_cursor\"\n                android:textSize=\"14sp\" />\n\n            <View\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\" />\n        </LinearLayout>\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:background=\"#c1c1c1\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:orientation=\"horizontal\"\n        android:padding=\"3dp\">\n\n        <TextView\n            android:id=\"@+id/tv_cancel\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:background=\"@drawable/selector_fillet_btn_bg\"\n            android:clickable=\"true\"\n            android:gravity=\"center\"\n            android:text=\"@string/cancel\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"15sp\"\n            tools:ignore=\"KeyboardInaccessibleWidget\" />\n\n        <Space\n            android:layout_width=\"3dp\"\n            android:layout_height=\"match_parent\" />\n\n        <TextView\n            android:id=\"@+id/tv_download\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:background=\"@drawable/selector_fillet_btn_bg\"\n            android:clickable=\"true\"\n            android:focusable=\"true\"\n            android:gravity=\"center\"\n            android:text=\"@string/ok\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"15sp\" />\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_file_chooser.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\">\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/tool_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:theme=\"?attr/actionBarStyle\"\n        app:titleTextAppearance=\"@style/ToolbarTitle\"\n        app:popupTheme=\"@style/AppTheme.PopupOverlay\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/rv_path\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"24dp\"\n        android:paddingLeft=\"10dp\"\n        android:paddingRight=\"10dp\"\n        android:background=\"@color/background_card\"\n        android:elevation=\"5dp\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/background_card\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_file\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n        <TextView\n            android:id=\"@+id/tv_empty\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:gravity=\"center\"\n            android:visibility=\"gone\" />\n\n    </FrameLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_input.xml",
    "content": "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@color/background\"\n    android:orientation=\"vertical\"\n    android:padding=\"10dp\">\n\n    <TextView\n        android:id=\"@+id/tv_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\" />\n\n    <com.kunfei.bookshelf.widget.views.ATEAutoCompleteTextView\n        android:id=\"@+id/et_input\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\"\n        android:completionThreshold=\"0\"\n        android:maxLines=\"5\"\n        tools:ignore=\"LabelFor\" />\n\n    <TextView\n        android:id=\"@+id/tv_ok\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\"\n        android:background=\"@drawable/selector_fillet_btn_bg\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:gravity=\"center\"\n        android:paddingTop=\"5dp\"\n        android:paddingBottom=\"5dp\"\n        android:text=\"@string/ok\"\n        android:textColor=\"@color/tv_text_default\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@color/background\"\n    android:gravity=\"center\"\n    android:orientation=\"vertical\">\n\n    <androidx.appcompat.widget.Toolbar\n        android:id=\"@+id/tool_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@color/background_menu\"\n        android:elevation=\"5dp\"\n        android:theme=\"?attr/actionBarStyle\"\n        android:title=\"@string/login\"\n        app:popupTheme=\"@style/AppTheme.PopupOverlay\"\n        tools:ignore=\"UnusedAttribute\" />\n\n    <LinearLayout\n        android:id=\"@+id/list_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_number_picker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:gravity=\"center\"\n        tools:ignore=\"UselessParent\">\n\n        <NumberPicker\n            android:id=\"@+id/number_picker\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\">\n\n        </NumberPicker>\n\n    </LinearLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_page_key.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"16dp\">\n\n    <TextView\n        android:id=\"@+id/tv_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\"\n        android:textSize=\"18sp\"\n        android:textStyle=\"bold\"\n        android:text=\"@string/custom_page_key\" />\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\"\n        android:hint=\"@string/prev_page_key\">\n\n        <EditText\n            android:id=\"@+id/et_prev\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:inputType=\"number\"\n            android:singleLine=\"true\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\"\n        android:hint=\"@string/next_page_key\">\n\n        <EditText\n            android:id=\"@+id/et_next\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:inputType=\"number\"\n            android:singleLine=\"true\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <TextView\n        android:id=\"@+id/tv_ok\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\"\n        android:background=\"@drawable/selector_fillet_btn_bg\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:gravity=\"center\"\n        android:paddingTop=\"5dp\"\n        android:paddingBottom=\"5dp\"\n        android:text=\"@string/ok\"\n        android:textColor=\"@color/tv_text_default\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/dialog_replace_rule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\">\n\n    <LinearLayout\n        android:id=\"@+id/ll_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\"\n        android:padding=\"10dp\">\n\n        <TextView\n            android:id=\"@+id/title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_margin=\"10dp\"\n            android:text=\"@string/replace_rule_edit\"\n            android:textSize=\"20sp\" />\n\n        <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n            android:id=\"@+id/til_replace_summary\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:hint=\"@string/replace_rule_summary\">\n\n            <com.kunfei.bookshelf.widget.views.ATEEditText\n                android:id=\"@+id/tie_replace_summary\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:maxLines=\"1\" />\n        </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n        <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n            android:id=\"@+id/til_replace_rule\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:hint=\"@string/replace_rule\">\n\n            <com.kunfei.bookshelf.widget.views.ATEEditText\n                android:id=\"@+id/tie_replace_rule\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:maxLines=\"3\" />\n        </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n        <com.kunfei.bookshelf.widget.views.ATECheckBox\n            android:id=\"@+id/cb_use_regex\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/use_regex\" />\n\n        <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n            android:id=\"@+id/til_replace_to\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:hint=\"@string/replace_to\">\n\n            <com.kunfei.bookshelf.widget.views.ATEEditText\n                android:id=\"@+id/tie_replace_to\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:maxLines=\"3\" />\n        </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n        <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n            android:id=\"@+id/til_use_to\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:hint=\"@string/use_to\">\n\n            <com.kunfei.bookshelf.widget.views.ATEEditText\n                android:id=\"@+id/tie_use_to\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:maxLines=\"3\" />\n        </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n        <TextView\n            android:id=\"@+id/replace_ad_intro\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"left\"\n            android:layout_margin=\"10dp\"\n            android:text=\"@string/replace_ad_intro\"\n            android:textSize=\"16sp\"\n            android:visibility=\"gone\" />\n\n        <TextView\n            android:id=\"@+id/tv_ok\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"5dp\"\n            android:background=\"@drawable/selector_fillet_btn_bg\"\n            android:clickable=\"true\"\n            android:focusable=\"true\"\n            android:gravity=\"center\"\n            android:paddingTop=\"5dp\"\n            android:paddingBottom=\"5dp\"\n            android:text=\"@string/ok\"\n            android:textColor=\"@color/tv_text_default\" />\n    </LinearLayout>\n</ScrollView>"
  },
  {
    "path": "app/src/main/res/layout/dialog_txt_chpater_rule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\"\n    android:orientation=\"vertical\"\n    android:padding=\"10dp\">\n\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_margin=\"10dp\"\n        android:text=\"@string/txt_chapter_regex\"\n        android:textSize=\"20sp\" />\n\n    <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n        android:id=\"@+id/til_rule_name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:hint=\"名称\"\n        android:padding=\"10dp\">\n\n        <com.kunfei.bookshelf.widget.views.ATEEditText\n            android:id=\"@+id/tie_rule_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:maxLines=\"3\" />\n    </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n    <com.kunfei.bookshelf.widget.views.ATETextInputLayout\n        android:id=\"@+id/til_rule_regex\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:hint=\"正则\"\n        android:padding=\"10dp\">\n\n        <com.kunfei.bookshelf.widget.views.ATEEditText\n            android:id=\"@+id/tie_rule_regex\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:maxLines=\"3\" />\n    </com.kunfei.bookshelf.widget.views.ATETextInputLayout>\n\n    <TextView\n        android:id=\"@+id/tv_ok\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_margin=\"5dp\"\n        android:background=\"@drawable/selector_fillet_btn_bg\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:gravity=\"center\"\n        android:paddingTop=\"5dp\"\n        android:paddingBottom=\"5dp\"\n        android:text=\"@string/ok\"\n        android:textColor=\"@color/tv_text_default\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_book_find.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\"\n    android:orientation=\"vertical\">\n\n    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n        android:id=\"@+id/refresh_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <FrameLayout\n            android:padding=\"6dp\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <androidx.recyclerview.widget.RecyclerView\n                    android:id=\"@+id/rv_find_left\"\n                    android:layout_width=\"90dp\"\n                    android:layout_height=\"match_parent\" />\n\n                <View\n                    android:id=\"@+id/vw_divider\"\n                    android:layout_width=\"0.8dp\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/btn_bg_press\" />\n\n                <androidx.recyclerview.widget.RecyclerView\n                    android:id=\"@+id/rv_find_right\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"1\" />\n\n            </LinearLayout>\n\n            <include\n                android:id=\"@+id/empty_view\"\n                layout=\"@layout/view_empty\" />\n\n        </FrameLayout>\n\n    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_book_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout\n        android:id=\"@+id/refresh_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\">\n\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"6dp\">\n\n            <androidx.recyclerview.widget.RecyclerView\n                android:id=\"@+id/rv_bookshelf\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                app:bg_color=\"@color/transparent\"\n                app:font_color=\"@color/tv_text_default\"\n                app:second_color=\"#c1c1c1\"\n                app:second_max_progress=\"80dp\"\n                app:speed=\"2dp\" />\n\n            <include\n                android:id=\"@+id/viewEmpty\"\n                layout=\"@layout/view_empty\" />\n        </FrameLayout>\n\n    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n    <LinearLayout\n        android:id=\"@+id/action_bar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"40dp\"\n        android:background=\"@color/background_card\"\n        android:gravity=\"center_vertical\"\n        android:orientation=\"horizontal\"\n        android:paddingLeft=\"10dp\"\n        android:paddingRight=\"10dp\"\n        android:visibility=\"gone\">\n\n        <ImageView\n            android:id=\"@+id/iv_back\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:background=\"@drawable/bg_ib_pre_round\"\n            android:contentDescription=\"@string/back\"\n            android:src=\"@drawable/ic_arrow_back\"\n            android:tint=\"@color/tv_text_default\" />\n\n        <TextView\n            android:id=\"@+id/tv_select_count\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingLeft=\"10dp\"\n            android:paddingRight=\"10dp\"\n            android:textColor=\"@color/tv_text_default\" />\n\n        <View\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\" />\n\n        <ImageView\n            android:id=\"@+id/iv_select_all\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:layout_marginRight=\"10dp\"\n            android:background=\"@drawable/bg_ib_pre_round\"\n            android:contentDescription=\"@string/del_select\"\n            android:src=\"@drawable/ic_select_all\"\n            android:tint=\"@color/tv_text_default\"\n            tools:ignore=\"RtlHardcoded\" />\n\n        <ImageView\n            android:id=\"@+id/iv_del\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"30dp\"\n            android:layout_marginRight=\"10dp\"\n            android:background=\"@drawable/bg_ib_pre_round\"\n            android:contentDescription=\"@string/del_select\"\n            android:src=\"@drawable/ic_clear_all\"\n            android:tint=\"@color/tv_text_default\"\n            tools:ignore=\"RtlHardcoded\" />\n\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_bookmark_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_below=\"@id/action_bar\"\n    android:layout_alignParentStart=\"true\"\n    android:descendantFocusability=\"blocksDescendants\">\n\n    <com.kunfei.bookshelf.widget.recycler.scroller.FastScrollRecyclerView\n        android:id=\"@+id/rv_list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:overScrollMode=\"never\" />\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_chapter_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:descendantFocusability=\"blocksDescendants\">\n\n    <com.kunfei.bookshelf.widget.recycler.scroller.FastScrollRecyclerView\n        android:id=\"@+id/rv_list\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_above=\"@id/ll_chapter_base_info\"\n        android:overScrollMode=\"never\" />\n\n    <LinearLayout\n        android:id=\"@+id/ll_chapter_base_info\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"42dp\"\n        android:paddingLeft=\"10dp\"\n        android:paddingRight=\"10dp\"\n        android:layout_alignParentBottom=\"true\"\n        android:gravity=\"center_vertical\"\n        android:orientation=\"horizontal\">\n\n        <TextView\n            android:id=\"@+id/tv_current_chapter_info\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:ellipsize=\"middle\"\n            android:paddingLeft=\"10dp\"\n            android:paddingRight=\"10dp\"\n            android:singleLine=\"true\"\n            android:gravity=\"center_vertical\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"12sp\" />\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/iv_chapter_top\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@drawable/bg_ib_pre_round\"\n            android:contentDescription=\"@string/go_to_top\"\n            android:src=\"@drawable/ic_arrow_drop_up\"\n            app:tint=\"@color/tv_text_default\" />\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/iv_chapter_bottom\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"match_parent\"\n            android:background=\"@drawable/bg_ib_pre_round\"\n            android:contentDescription=\"@string/go_to_bottom\"\n            android:src=\"@drawable/ic_arrow_drop_down\"\n            app:tint=\"@color/tv_text_default\" />\n    </LinearLayout>\n\n    <View\n        android:id=\"@+id/v_shadow\"\n        android:layout_height=\"1px\"\n        android:layout_width=\"match_parent\"\n        android:background=\"@color/bg_divider_line\"\n        android:layout_above=\"@id/ll_chapter_base_info\" />\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_file_category.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:orientation=\"vertical\">\n    <!--path-->\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:minHeight=\"36dp\"\n        android:layout_marginTop=\"8dp\"\n        android:gravity=\"center_vertical\"\n        android:orientation=\"horizontal\">\n\n        <TextView\n            android:id=\"@+id/tv_sd\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:padding=\"8dp\"\n            android:layout_marginLeft=\"8dp\"\n            android:singleLine=\"true\"\n            android:textSize=\"13sp\"\n            android:text=\"@string/nb_file_path\"\n            android:textColor=\"@color/success\"\n            tools:ignore=\"RtlHardcoded,UnusedAttribute\" />\n\n        <TextView\n            android:id=\"@+id/file_category_tv_path\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center_vertical\"\n            android:singleLine=\"true\"\n            android:textColor=\"@color/tv_text_secondary\"\n            android:textSize=\"13sp\"\n            android:focusable=\"true\"\n            tools:text=\"/\" />\n\n        <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n            android:id=\"@+id/file_category_tv_back_last\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:gravity=\"center\"\n            android:paddingLeft=\"16dp\"\n            android:paddingRight=\"16dp\"\n            android:paddingTop=\"8dp\"\n            android:paddingBottom=\"8dp\"\n            android:layout_margin=\"4dp\"\n            app:cornerRadius=\"5dp\"\n            android:text=\"上级\"\n            android:textFontWeight=\"800\"\n            android:textSize=\"14sp\"\n            tools:ignore=\"UnusedAttribute\" />\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1px\"\n        android:layout_marginBottom=\"4dp\"\n        android:background=\"@color/btn_bg_press\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/file_category_rv_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_local_book.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.kunfei.bookshelf.widget.recycler.refresh.RefreshLayout\n\txmlns:android=\"http://schemas.android.com/apk/res/android\"\n\tandroid:id=\"@+id/refresh_layout\"\n\tandroid:layout_width=\"match_parent\"\n\tandroid:layout_height=\"match_parent\">\n\n\t<androidx.recyclerview.widget.RecyclerView\n\t\tandroid:id=\"@+id/local_book_rv_content\"\n\t\tandroid:layout_width=\"match_parent\"\n\t\tandroid:layout_height=\"match_parent\" />\n</com.kunfei.bookshelf.widget.recycler.refresh.RefreshLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_1line_text_and_del.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\"\n    android:padding=\"6dp\"\n    tools:ignore=\"UseCompoundDrawables\">\n\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\" />\n\n    <ImageView\n        android:id=\"@+id/iv_del\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        android:src=\"@drawable/ic_clear_all\"\n        android:visibility=\"gone\"\n        android:contentDescription=\"@string/delete\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_book_source.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"horizontal\"\n    android:padding=\"16dp\">\n\n    <com.kunfei.bookshelf.widget.views.ATECheckBox\n        android:id=\"@+id/cb_book_source\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_weight=\"1\"\n        android:singleLine=\"true\"\n        android:text=\"\"\n        android:textColor=\"@color/tv_text_default\" />\n\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_edit_source\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_gravity=\"center\"\n        android:background=\"@drawable/bg_ib_pre_round\"\n        android:contentDescription=\"@string/edit\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/ic_edit\"\n        app:tint=\"@color/tv_text_default\" />\n\n    <ImageView\n        android:id=\"@+id/iv_top_source\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_gravity=\"center\"\n        android:background=\"@drawable/bg_ib_pre_round\"\n        android:contentDescription=\"@string/to_top\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/ic_top_source\"\n        app:tint=\"@color/tv_text_default\" />\n\n    <ImageView\n        android:id=\"@+id/iv_del_source\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_gravity=\"center\"\n        android:background=\"@drawable/bg_ib_pre_round\"\n        android:contentDescription=\"@string/delete\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/ic_clear_all\"\n        app:tint=\"@color/tv_text_default\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_bookshelf_grid.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingLeft=\"20dp\"\n    android:paddingRight=\"20dp\"\n    android:paddingTop=\"10dp\"\n    android:paddingBottom=\"10dp\">\n\n    <com.kunfei.bookshelf.widget.image.CoverImageView\n        android:id=\"@+id/iv_cover\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:scaleType=\"centerCrop\"\n        android:src=\"@drawable/image_cover_default\"\n        android:transitionName=\"img_cover\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:ignore=\"UnusedAttribute\" />\n\n    <com.kunfei.bookshelf.widget.BadgeView\n        android:id=\"@+id/bv_unread\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"right\"\n        android:layout_margin=\"5dp\"\n        android:includeFontPadding=\"false\"\n        app:layout_constraintTop_toTopOf=\"@+id/iv_cover\"\n        app:layout_constraintRight_toRightOf=\"@+id/iv_cover\"\n        tools:ignore=\"RtlHardcoded\" />\n\n    <com.kunfei.bookshelf.widget.RotateLoading\n        android:id=\"@+id/rl_loading\"\n        android:layout_width=\"26dp\"\n        android:layout_height=\"26dp\"\n        android:layout_gravity=\"right\"\n        android:visibility=\"invisible\"\n        app:layout_constraintTop_toTopOf=\"@+id/iv_cover\"\n        app:layout_constraintRight_toRightOf=\"@+id/iv_cover\"\n        app:loading_color=\"@color/colorAccent\"\n        app:loading_width=\"2dp\"\n        tools:ignore=\"RtlHardcoded\" />\n\n    <TextView\n        android:id=\"@+id/tv_name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom\"\n        android:gravity=\"center\"\n        android:singleLine=\"true\"\n        android:text=\"@string/app_name\"\n        android:textColor=\"@color/tv_text_default\"\n        android:textSize=\"15sp\"\n        app:layout_constraintTop_toBottomOf=\"@+id/iv_cover\" />\n\n    <View\n        android:id=\"@+id/vw_select\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"0dp\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        android:visibility=\"invisible\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_bookshelf_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/cv_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:background=\"?android:attr/selectableItemBackground\"\n    tools:ignore=\"UnusedAttribute\">\n\n    <com.kunfei.bookshelf.widget.image.CoverImageView\n        android:id=\"@+id/iv_cover\"\n        android:layout_width=\"60dp\"\n        android:layout_height=\"80dp\"\n        android:layout_margin=\"8dp\"\n        android:contentDescription=\"@string/img_cover\"\n        android:scaleType=\"centerCrop\"\n        android:src=\"@drawable/image_cover_default\"\n        android:transitionName=\"img_cover\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintLeft_toLeftOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:ignore=\"UnusedAttribute\" />\n\n    <FrameLayout\n        android:id=\"@+id/fl_has_new\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        app:layout_constraintRight_toRightOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"@id/tv_name\">\n\n        <com.kunfei.bookshelf.widget.BadgeView\n            android:id=\"@+id/bv_unread\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"right\"\n            android:layout_margin=\"5dp\"\n            android:includeFontPadding=\"false\"\n            tools:ignore=\"RtlHardcoded\" />\n\n        <com.kunfei.bookshelf.widget.RotateLoading\n            android:id=\"@+id/rl_loading\"\n            android:layout_width=\"26dp\"\n            android:layout_height=\"26dp\"\n            android:layout_gravity=\"right\"\n            android:visibility=\"invisible\"\n            app:loading_color=\"@color/colorAccent\"\n            app:loading_width=\"2dp\"\n            tools:ignore=\"RtlHardcoded\" />\n    </FrameLayout>\n\n    <TextView\n        android:id=\"@+id/tv_name\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"8dp\"\n        android:layout_marginTop=\"8dp\"\n        android:includeFontPadding=\"false\"\n        android:paddingLeft=\"4dp\"\n        android:singleLine=\"true\"\n        android:text=\"@string/book_name\"\n        android:textColor=\"@color/tv_text_default\"\n        android:textSize=\"16sp\"\n        app:layout_constraintBottom_toTopOf=\"@+id/tv_author\"\n        app:layout_constraintLeft_toRightOf=\"@+id/iv_cover\"\n        app:layout_constraintRight_toLeftOf=\"@id/fl_has_new\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_author\"\n        android:layout_width=\"@dimen/desc_icon_size\"\n        android:layout_height=\"@dimen/desc_icon_size\"\n        android:layout_marginLeft=\"8dp\"\n        android:contentDescription=\"@string/author\"\n        android:paddingStart=\"2dp\"\n        android:paddingEnd=\"2dp\"\n        android:src=\"@drawable/ic_author\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/tv_author\"\n        app:layout_constraintLeft_toRightOf=\"@+id/iv_cover\"\n        app:layout_constraintTop_toTopOf=\"@+id/tv_author\"\n        app:tint=\"@color/tv_text_secondary\"\n        tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n    <TextView\n        android:id=\"@+id/tv_author\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:includeFontPadding=\"false\"\n        android:maxLines=\"1\"\n        android:paddingEnd=\"6dp\"\n        android:text=\"@string/author\"\n        android:textColor=\"@color/tv_text_secondary\"\n        android:textSize=\"13sp\"\n        app:layout_constraintBottom_toTopOf=\"@+id/tv_read\"\n        app:layout_constraintLeft_toRightOf=\"@+id/iv_author\"\n        app:layout_constraintRight_toLeftOf=\"@id/fl_has_new\"\n        app:layout_constraintTop_toBottomOf=\"@+id/tv_name\"\n        tools:ignore=\"RtlSymmetry\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_read\"\n        android:layout_width=\"@dimen/desc_icon_size\"\n        android:layout_height=\"@dimen/desc_icon_size\"\n        android:layout_marginLeft=\"8dp\"\n        android:contentDescription=\"@string/read_dur_progress\"\n        android:paddingStart=\"2dp\"\n        android:paddingEnd=\"2dp\"\n        android:src=\"@drawable/ic_history\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/tv_read\"\n        app:layout_constraintLeft_toRightOf=\"@+id/iv_cover\"\n        app:layout_constraintTop_toTopOf=\"@+id/tv_read\"\n        app:tint=\"@color/tv_text_secondary\"\n        tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n    <TextView\n        android:id=\"@+id/tv_read\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:includeFontPadding=\"false\"\n        android:singleLine=\"true\"\n        android:text=\"@string/read_dur_progress\"\n        android:textColor=\"@color/tv_text_secondary\"\n        android:textSize=\"13sp\"\n        app:layout_constraintBottom_toTopOf=\"@id/tv_last\"\n        app:layout_constraintLeft_toRightOf=\"@+id/iv_read\"\n        app:layout_constraintRight_toLeftOf=\"@id/fl_has_new\"\n        app:layout_constraintTop_toBottomOf=\"@+id/tv_author\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_last\"\n        android:layout_width=\"@dimen/desc_icon_size\"\n        android:layout_height=\"@dimen/desc_icon_size\"\n        android:layout_marginLeft=\"8dp\"\n        android:contentDescription=\"@string/book_search_last\"\n        android:paddingStart=\"2dp\"\n        android:paddingEnd=\"2dp\"\n        android:src=\"@drawable/ic_book_last\"\n        app:layout_constraintBottom_toBottomOf=\"@+id/tv_last\"\n        app:layout_constraintLeft_toRightOf=\"@+id/iv_cover\"\n        app:layout_constraintTop_toTopOf=\"@+id/tv_last\"\n        app:tint=\"@color/tv_text_secondary\"\n        tools:ignore=\"RtlHardcoded,RtlSymmetry\" />\n\n    <TextView\n        android:id=\"@+id/tv_last\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"8dp\"\n        android:singleLine=\"true\"\n        android:text=\"@string/book_search_last\"\n        android:textColor=\"@color/tv_text_secondary\"\n        android:textSize=\"13sp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintLeft_toRightOf=\"@+id/iv_last\"\n        app:layout_constraintRight_toLeftOf=\"@id/fl_has_new\"\n        app:layout_constraintTop_toBottomOf=\"@+id/tv_read\" />\n\n    <View\n        android:id=\"@+id/vw_select\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:visibility=\"gone\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:layout_constraintBottom_toBottomOf=\"parent\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:background=\"@color/btn_bg_press\"\n        app:layout_constraintBottom_toBottomOf=\"parent\" />\n\n</androidx.constraintlayout.widget.ConstraintLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_change_cover.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:background=\"?android:attr/selectableItemBackground\"\n    android:orientation=\"vertical\"\n    android:gravity=\"center_horizontal\"\n    tools:ignore=\"UnusedAttribute\"\n    android:layout_margin=\"10dp\">\n\n    <com.kunfei.bookshelf.widget.image.CoverImageView\n        android:id=\"@+id/iv_cover\"\n        android:layout_width=\"80dp\"\n        android:layout_height=\"100dp\"\n        android:scaleType=\"centerCrop\"\n        android:contentDescription=\"@string/img_cover\"/>\n\n    <TextView\n        android:id=\"@+id/tv_source_name\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:singleLine=\"true\"/>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_change_source.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:background=\"?android:attr/selectableItemBackground\"\n    android:orientation=\"horizontal\"\n    android:gravity=\"center_vertical\"\n    tools:ignore=\"UnusedAttribute\">\n\n    <LinearLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/tv_source_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_margin=\"2dp\"\n            android:singleLine=\"true\"\n            android:text=\"@string/app_name\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"16sp\" />\n\n        <TextView\n            android:id=\"@+id/tv_lastChapter\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_margin=\"2dp\"\n            android:singleLine=\"true\"\n            android:text=\"@string/app_name\"\n            android:textColor=\"@color/tv_text_secondary\"\n            android:textSize=\"12sp\" />\n    </LinearLayout>\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_checked\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        android:layout_margin=\"16dp\"\n        android:src=\"@drawable/ic_check\"\n        android:visibility=\"invisible\"\n        app:tint=\"@color/tv_text_default\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_chapter_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/fl_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <LinearLayout\n        android:id=\"@+id/ll_name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:background=\"?attr/selectableItemBackground\"\n        android:paddingLeft=\"15dp\"\n        android:paddingTop=\"12dp\"\n        android:paddingRight=\"15dp\"\n        android:paddingBottom=\"12dp\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <TextView\n            android:id=\"@+id/tv_name\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:ellipsize=\"end\"\n            android:gravity=\"center_vertical\"\n            android:singleLine=\"true\"\n            android:text=\"@string/app_name\"\n            android:textSize=\"15sp\" />\n\n    </LinearLayout>\n\n\n    <View\n        android:id=\"@+id/v_line\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1.5dp\"\n        android:layout_gravity=\"bottom\"\n        android:layout_marginLeft=\"15dp\"\n        android:layout_marginRight=\"15dp\"\n        android:background=\"@drawable/bg_chapter_item_divider\"\n        android:layerType=\"software\"\n        android:paddingBottom=\"1dp\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_download.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\"\n        android:padding=\"16dp\">\n\n        <com.kunfei.bookshelf.widget.image.CoverImageView\n            android:id=\"@+id/iv_cover\"\n            android:layout_width=\"45dp\"\n            android:layout_height=\"65dp\"\n            android:contentDescription=\"@string/img_cover\"\n            android:src=\"@drawable/image_cover_default\" />\n\n        <LinearLayout\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:orientation=\"vertical\"\n            android:paddingLeft=\"10dp\"\n            android:paddingTop=\"5dp\"\n            android:paddingRight=\"10dp\"\n            tools:ignore=\"RtlHardcoded,RtlSymmetry\">\n\n            <TextView\n                android:id=\"@+id/tv_name\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"26dp\"\n                android:layout_gravity=\"center_horizontal|bottom\"\n                android:singleLine=\"true\"\n                android:text=\"@string/book_name\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"16sp\" />\n\n            <TextView\n                android:id=\"@+id/tv_download\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"26dp\"\n                android:layout_gravity=\"center_horizontal|bottom\"\n                android:singleLine=\"true\"\n                android:text=\"@string/download_offline\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"14sp\" />\n\n        </LinearLayout>\n\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/iv_delete\"\n            android:layout_width=\"24dp\"\n            android:layout_height=\"24dp\"\n            android:layout_gravity=\"center\"\n            android:layout_margin=\"16dp\"\n            android:src=\"@drawable/ic_clear_all\"\n            app:tint=\"@color/tv_text_default\" />\n\n\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:background=\"@color/btn_bg_press\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_file.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"60dp\"\n    android:background=\"@drawable/selector_common_bg\"\n    android:orientation=\"horizontal\">\n\n    <FrameLayout\n        android:id=\"@+id/file_fl_icon\"\n        android:layout_width=\"60dp\"\n        android:layout_height=\"match_parent\">\n        <!--文件夹标识或已选文件标识-->\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/file_iv_icon\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:scaleType=\"center\"\n            android:visibility=\"gone\"\n            app:tint=\"@color/tv_text_default\"\n            tools:src=\"@drawable/ic_folder\" />\n        <!--选择是否添加文件-->\n        <com.kunfei.bookshelf.widget.views.ATECheckBox\n            android:id=\"@+id/file_cb_select\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:clickable=\"true\"\n            android:enabled=\"true\"\n            android:focusable=\"true\" />\n    </FrameLayout>\n\n    <RelativeLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\"\n        android:paddingTop=\"8dp\"\n        android:paddingBottom=\"8dp\">\n\n        <TextView\n            android:id=\"@+id/file_tv_name\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:singleLine=\"true\"\n            android:text=\"@string/app_name\"\n            android:textSize=\"16sp\" />\n\n        <LinearLayout\n            android:id=\"@+id/file_ll_brief\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentBottom=\"true\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                android:id=\"@+id/file_tv_tag\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginRight=\"15dp\"\n                android:paddingStart=\"5dp\"\n                android:paddingEnd=\"5dp\"\n                android:text=\"TXT\"\n                tools:ignore=\"HardcodedText,RtlHardcoded\" />\n\n            <TextView\n                android:id=\"@+id/file_tv_size\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginRight=\"15dp\"\n                tools:ignore=\"RtlHardcoded\"\n                tools:text=\"324kb\" />\n\n            <TextView\n                android:id=\"@+id/file_tv_date\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                tools:text=\"2017-05-22\" />\n        </LinearLayout>\n\n        <TextView\n            android:id=\"@+id/file_tv_sub_count\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentBottom=\"true\"\n            android:visibility=\"gone\"\n            tools:text=\"0 项\" />\n    </RelativeLayout>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_file_filepicker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:orientation=\"horizontal\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:padding=\"5dp\">\n\n    <ImageView\n        android:id=\"@+id/image_view\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        tools:ignore=\"ContentDescription\" />\n\n    <TextView\n        android:id=\"@+id/text_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_vertical\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_find1_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\">\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_group\"\n        android:layout_width=\"18dp\"\n        android:layout_height=\"18dp\"\n        android:layout_margin=\"5dp\"\n        android:alpha=\"0.5\"\n        android:src=\"@drawable/ic_expand_more_24dp\"\n        app:tint=\"@color/tv_text_default\" />\n\n    <TextView\n        android:id=\"@+id/tv_kind_name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"5dp\"\n        android:singleLine=\"true\"\n        android:text=\"@string/app_name\"\n        android:textColor=\"@color/tv_text_default\"\n        android:textSize=\"18sp\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_find1_kind.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:padding=\"5dp\">\n\n    <TextView\n        android:id=\"@+id/tv_kind_name\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginStart=\"50dp\"\n        android:singleLine=\"true\"\n        android:text=\"@string/app_name\"\n        android:textColor=\"@color/tv_text_secondary\"\n        android:textSize=\"16sp\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_find2_childer_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tv_item\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@drawable/shape_card_view\"\n    android:clickable=\"true\"\n    android:ellipsize=\"end\"\n    android:focusable=\"true\"\n    android:gravity=\"center\"\n    android:padding=\"5dp\"\n    android:singleLine=\"true\"\n    android:textColor=\"@color/tv_text_default\"\n    android:textSize=\"14sp\" />\n"
  },
  {
    "path": "app/src/main/res/layout/item_find2_header_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tv_source_name\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:padding=\"9dp\"\n    android:singleLine=\"true\"\n    android:text=\"@string/app_name\"\n    android:textColor=\"@color/tv_text_secondary\"\n    android:textSize=\"16sp\"\n    android:textStyle=\"bold\" />"
  },
  {
    "path": "app/src/main/res/layout/item_find_left.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tv_source_name\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"6dp\"\n    android:singleLine=\"true\"\n    android:text=\"@string/app_name\"\n    android:textColor=\"@color/tv_text_default\" />\n"
  },
  {
    "path": "app/src/main/res/layout/item_font.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center_vertical\"\n    android:orientation=\"horizontal\"\n    android:padding=\"5dp\">\n\n    <TextView\n        android:id=\"@+id/tv_font\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:padding=\"5dp\"\n        android:textColor=\"@color/tv_text_default\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_checked\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        android:src=\"@drawable/ic_check\"\n        android:visibility=\"invisible\"\n        app:tint=\"@color/tv_text_default\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_icon_preference.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"?android:attr/listPreferredItemHeight\"\n    android:padding=\"6dip\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:adjustViewBounds=\"true\"\n        android:padding=\"6dip\" />\n\n    <CheckedTextView\n        android:id=\"@+id/label\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentEnd=\"true\"\n        android:layout_toRightOf=\"@+id/icon\"\n        android:checkMark=\"?android:attr/listChoiceIndicatorSingle\"\n        android:ellipsize=\"marquee\"\n        android:gravity=\"center_vertical\"\n        android:minHeight=\"?android:attr/listPreferredItemHeight\"\n        android:singleLine=\"true\"\n        android:textAppearance=\"?android:attr/textAppearanceMedium\"\n        android:textColor=\"?android:attr/textColorAlertDialogListItem\"\n        android:textIsSelectable=\"false\"\n        tools:ignore=\"RtlHardcoded\" />\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_path_filepicker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:orientation=\"horizontal\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"match_parent\"\n    android:clickable=\"true\"\n    android:focusable=\"true\">\n\n    <TextView\n        android:id=\"@+id/text_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_vertical\" />\n\n    <ImageView\n        android:id=\"@+id/image_view\"\n        android:layout_width=\"20dp\"\n        android:layout_height=\"match_parent\"\n        tools:ignore=\"ContentDescription\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_read_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"80dp\"\n    android:layout_height=\"wrap_content\"\n    android:gravity=\"center\"\n    android:orientation=\"vertical\">\n\n    <ImageView\n        android:id=\"@+id/iv_bg\"\n        android:layout_width=\"80dp\"\n        android:layout_height=\"80dp\"\n        android:scaleType=\"fitXY\"\n        android:padding=\"2dp\"\n        android:background=\"@color/colorPrimary\"\n        android:src=\"@drawable/icon_image\"\n        tools:ignore=\"UnusedAttribute\" />\n\n    <TextView\n        android:id=\"@+id/tv_desc\"\n        android:layout_width=\"80dp\"\n        android:layout_height=\"wrap_content\"\n        android:singleLine=\"true\"\n        android:text=\"@string/app_name\"\n        android:textAlignment=\"center\"\n        android:layout_marginTop=\"2dp\"\n        android:layout_marginBottom=\"4dp\"\n        android:textColor=\"@color/tv_text_secondary\"\n        android:textSize=\"10sp\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_replace_rule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"horizontal\"\n    android:padding=\"16dp\">\n\n    <com.kunfei.bookshelf.widget.views.ATECheckBox\n        android:id=\"@+id/cb_replace_rule\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_weight=\"1\"\n        android:singleLine=\"true\"\n        android:text=\"\"\n        android:textColor=\"@color/tv_text_default\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_edit\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_gravity=\"center\"\n        android:background=\"@drawable/bg_ib_pre_round\"\n        android:contentDescription=\"@string/edit\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/ic_edit\"\n        app:tint=\"@color/tv_text_default\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_del\"\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_gravity=\"center\"\n        android:background=\"@drawable/bg_ib_pre_round\"\n        android:contentDescription=\"@string/delete\"\n        android:padding=\"8dp\"\n        android:src=\"@drawable/ic_clear_all\"\n        app:tint=\"@color/tv_text_default\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_search_book.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/fl_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:paddingLeft=\"6dp\"\n    android:paddingRight=\"6dp\"\n    android:paddingTop=\"5dp\"\n    android:paddingBottom=\"3dp\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:background=\"?android:attr/selectableItemBackground\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"100dp\"\n        android:orientation=\"horizontal\">\n\n        <com.kunfei.bookshelf.widget.image.CoverImageView\n            android:id=\"@+id/iv_cover\"\n            android:layout_width=\"70dp\"\n            android:layout_height=\"100dp\"\n            android:contentDescription=\"@string/book\"\n            android:scaleType=\"centerCrop\"\n            android:src=\"@drawable/image_cover_default\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:paddingLeft=\"6dp\"\n            android:orientation=\"vertical\"\n            tools:ignore=\"RtlHardcoded,RtlSymmetry\">\n\n            <TextView\n                android:id=\"@+id/tv_name\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\"\n                android:layout_marginBottom=\"2dp\"\n                android:ellipsize=\"end\"\n                android:lines=\"1\"\n                android:text=\"@string/app_name\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"16sp\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/tv_origin\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:ellipsize=\"end\"\n                    android:lines=\"1\"\n                    android:text=\"来源\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"12sp\" />\n\n                <TextView\n                    android:id=\"@+id/tv_origin_num\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:layout_marginLeft=\"5dp\"\n                    android:ellipsize=\"end\"\n                    android:lines=\"1\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"12sp\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\"\n                android:orientation=\"horizontal\"\n                tools:ignore=\"NestedWeights\">\n\n                <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                    android:id=\"@+id/tv_state\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:layout_marginRight=\"5dp\"\n                    android:paddingLeft=\"2dp\"\n                    android:paddingRight=\"2dp\"\n                    android:text=\"连载\"\n                    android:textSize=\"12sp\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                    android:id=\"@+id/tv_kind\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:layout_marginRight=\"5dp\"\n                    android:lines=\"1\"\n                    android:paddingLeft=\"2dp\"\n                    android:paddingRight=\"2dp\"\n                    android:text=\"现代都市\"\n                    android:textSize=\"12sp\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                    android:id=\"@+id/tv_words\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:lines=\"1\"\n                    android:paddingLeft=\"2dp\"\n                    android:paddingRight=\"2dp\"\n                    android:text=\"1.2万字\"\n                    android:textSize=\"12sp\" />\n            </LinearLayout>\n\n            <TextView\n                android:id=\"@+id/tv_lasted\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\"\n                android:ellipsize=\"end\"\n                android:lines=\"1\"\n                android:text=\"最新章节\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"12sp\" />\n\n            <TextView\n                android:id=\"@+id/tv_introduce\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\"\n                android:ellipsize=\"end\"\n                android:lines=\"1\"\n                android:text=\"简介\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"12sp\" />\n        </LinearLayout>\n    </LinearLayout>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.5dp\"\n        android:layout_gravity=\"bottom\"\n        android:background=\"@color/bg_divider_line\" />\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_search_history.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tv\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"3dp\"\n    android:background=\"@drawable/shape_card_view\"\n    android:clickable=\"true\"\n    android:ellipsize=\"middle\"\n    android:focusable=\"true\"\n    android:padding=\"6dp\"\n    android:singleLine=\"true\"\n    android:text=\"@string/app_name\"\n    android:textColor=\"@color/tv_text_default\" />"
  },
  {
    "path": "app/src/main/res/layout/item_source_debug.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tv\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:textIsSelectable=\"true\"\n    android:autoLink=\"web\" />"
  },
  {
    "path": "app/src/main/res/layout/item_source_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.kunfei.bookshelf.widget.views.ATETextInputLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/textInputLayout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\">\n\n    <com.kunfei.bookshelf.widget.views.ATEEditText\n        android:id=\"@+id/editText\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\" />\n</com.kunfei.bookshelf.widget.views.ATETextInputLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_text.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/text_view\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"3dp\"\n    android:background=\"@drawable/selector_fillet_btn_bg\"\n    android:clickable=\"true\"\n    android:ellipsize=\"end\"\n    android:focusable=\"true\"\n    android:gravity=\"center\"\n    android:padding=\"5dp\"\n    android:singleLine=\"true\"\n    android:textColor=\"@color/tv_text_default\"\n    android:textSize=\"14sp\"\n    tools:ignore=\"UnusedAttribute\" />\n"
  },
  {
    "path": "app/src/main/res/layout/mo_dialog_image_text.xml",
    "content": "<androidx.cardview.widget.CardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/cv_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"30dp\"\n    app:cardBackgroundColor=\"@color/background_card\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:padding=\"10dp\"\n        android:gravity=\"center_horizontal\"\n        android:orientation=\"vertical\">\n        <Space\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\"/>\n\n        <androidx.appcompat.widget.AppCompatImageView\n            android:id=\"@+id/image_view\"\n            android:layout_width=\"300dp\"\n            android:layout_height=\"300dp\"\n            android:scaleType=\"centerCrop\"/>\n\n        <Space\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\"/>\n        <TextView\n            android:id=\"@+id/tv_can_copy\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:lineSpacingExtra=\"2dp\"\n            android:padding=\"10dp\"\n            android:text=\"@string/app_name\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textIsSelectable=\"true\"\n            android:textSize=\"16sp\" />\n        <Space\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\"/>\n    </LinearLayout>\n\n</androidx.cardview.widget.CardView>"
  },
  {
    "path": "app/src/main/res/layout/mo_dialog_infor.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.cardview.widget.CardView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"260dp\"\n    android:layout_height=\"wrap_content\"\n    app:cardBackgroundColor=\"@color/background_card\">\n\n    <LinearLayout\n        android:id=\"@+id/ll_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_horizontal\"\n        android:orientation=\"vertical\"\n        android:padding=\"16dp\">\n\n            <TextView\n                android:id=\"@+id/msg_tv\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"20dp\"\n                android:layout_marginTop=\"10dp\"\n                android:gravity=\"left\"\n                android:minHeight=\"30dp\"\n                android:textColor=\"#767676\"\n                android:textSize=\"16sp\"\n                tools:ignore=\"RtlHardcoded\" />\n\n        <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n                android:id=\"@+id/tv_close\"\n                android:layout_width=\"70dp\"\n                android:layout_height=\"30dp\"\n                android:layout_gravity=\"center\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginRight=\"10dp\"\n                android:gravity=\"center\"\n                android:lines=\"1\"\n                android:text=\"@string/ok\"\n                android:textSize=\"14sp\" />\n\n    </LinearLayout>\n\n</androidx.cardview.widget.CardView>"
  },
  {
    "path": "app/src/main/res/layout/mo_dialog_loading.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.cardview.widget.CardView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"260dp\"\n    android:layout_height=\"140dp\"\n    app:cardBackgroundColor=\"@color/background_card\">\n\n    <LinearLayout\n        android:id=\"@+id/ll_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\"\n        android:paddingTop=\"3.5dp\"\n        android:paddingLeft=\"10.5dp\"\n        android:paddingRight=\"10.5dp\"\n        android:paddingBottom=\"16.5dp\"\n        android:gravity=\"center_horizontal\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\"\n            tools:ignore=\"UselessParent\">\n\n            <FrameLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\"\n                android:layout_gravity=\"center\">\n\n                <com.kunfei.bookshelf.widget.RotateLoading\n                    android:id=\"@+id/rl_loading\"\n                    android:layout_width=\"48dp\"\n                    android:layout_height=\"48dp\"\n                    app:loading_width=\"2dp\"\n                    app:loading_color=\"#767676\"\n                    android:layout_gravity=\"center\"\n                    android:layout_marginTop=\"20dp\"\n                    android:layout_marginBottom=\"20dp\" />\n            </FrameLayout>\n\n            <TextView\n                android:id=\"@+id/msg_tv\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/loading\"\n                android:layout_gravity=\"center\"\n                android:gravity=\"center\"\n                android:textSize=\"16sp\"\n                android:textColor=\"@color/tv_text_default\"\n                android:layout_marginBottom=\"20dp\" />\n        </LinearLayout>\n    </LinearLayout>\n\n</androidx.cardview.widget.CardView>"
  },
  {
    "path": "app/src/main/res/layout/mo_dialog_markdown.xml",
    "content": "<androidx.cardview.widget.CardView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_margin=\"30dp\"\n    app:cardBackgroundColor=\"@color/background_card\">\n\n\n    <ScrollView\n        android:id=\"@+id/md_scroll\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\"\n        android:scrollbars=\"vertical\"\n        android:fillViewport=\"true\">\n        <TextView\n            android:id=\"@+id/tv_markdown\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:lineSpacingMultiplier=\"1.2\"\n            android:padding=\"10dp\"/>\n    </ScrollView>\n\n</androidx.cardview.widget.CardView>"
  },
  {
    "path": "app/src/main/res/layout/mo_dialog_text_large.xml",
    "content": "<androidx.cardview.widget.CardView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_margin=\"30dp\"\n    app:cardBackgroundColor=\"@color/background_card\">\n\n    <ScrollView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n        <TextView\n            android:id=\"@+id/tv_can_copy\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"16sp\"\n            android:padding=\"10dp\"\n            android:text=\"@string/app_name\"\n            android:textColor=\"@color/tv_text_default\"\n            android:lineSpacingExtra=\"2dp\"\n            android:textIsSelectable=\"true\"/>\n    </ScrollView>\n\n</androidx.cardview.widget.CardView>"
  },
  {
    "path": "app/src/main/res/layout/mo_dialog_two.xml",
    "content": "<androidx.cardview.widget.CardView\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"300dp\"\n    android:layout_height=\"wrap_content\"\n    app:cardBackgroundColor=\"@color/background_card\">\n\n    <LinearLayout\n        android:id=\"@+id/ll_content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center_horizontal\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/tv_msg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center\"\n            android:padding=\"20sp\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"16sp\" />\n\n        <View\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0.5dp\"\n            android:background=\"#c1c1c1\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"50dp\"\n            android:padding=\"3dp\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:id=\"@+id/tv_cancel\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_weight=\"1\"\n                android:background=\"@drawable/selector_fillet_btn_bg\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:gravity=\"center\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"15sp\" />\n\n            <Space\n                android:layout_width=\"3dp\"\n                android:layout_height=\"match_parent\" />\n\n            <TextView\n                android:id=\"@+id/tv_done\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"match_parent\"\n                android:layout_weight=\"1\"\n                android:background=\"@drawable/selector_fillet_btn_bg\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:gravity=\"center\"\n                android:textColor=\"@color/tv_text_default\"\n                android:textSize=\"15sp\" />\n        </LinearLayout>\n    </LinearLayout>\n</androidx.cardview.widget.CardView>"
  },
  {
    "path": "app/src/main/res/layout/navigation_header.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/na_top\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_read\"\n        android:layout_width=\"160dp\"\n        android:layout_height=\"80dp\"\n        android:layout_marginLeft=\"16dp\"\n        android:layout_marginRight=\"16dp\"\n        android:src=\"@drawable/ic_read\"\n        app:tint=\"@color/colorAccent\"\n        android:contentDescription=\"@string/app_name\" />\n\n    <TextView\n        android:id=\"@+id/tv_user\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"16dp\"\n        android:layout_marginRight=\"16dp\"\n        android:layout_marginBottom=\"10dp\"\n        android:singleLine=\"true\"\n        android:text=\"@string/read_summary\"\n        android:textColor=\"@color/tv_text_default\"\n        android:textSize=\"14sp\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.9dp\"\n        android:background=\"@color/btn_bg_press\" />\n\n</LinearLayout>\n\n\n"
  },
  {
    "path": "app/src/main/res/layout/pop_media_player.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center\">\n\n    <View\n        android:id=\"@+id/vw_bg\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center\"\n        android:orientation=\"vertical\">\n\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\">\n\n            <ImageView\n                android:id=\"@+id/iv_cover_bg\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:scaleType=\"centerCrop\"\n                android:contentDescription=\"@string/img_cover\" />\n\n            <de.hdodenhof.circleimageview.CircleImageView\n                android:id=\"@+id/iv_cover\"\n                android:layout_width=\"260dp\"\n                android:layout_height=\"260dp\"\n                android:layout_gravity=\"center\"\n                android:contentDescription=\"@string/img_cover\"\n                android:src=\"@drawable/image_cover_default\"\n                app:civ_border_color=\"@color/tv_text_default\"\n                app:civ_border_width=\"1dp\"\n                tools:ignore=\"HardcodedText\" />\n\n        </FrameLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\"\n            android:paddingStart=\"5dp\"\n            android:paddingEnd=\"5dp\">\n\n            <TextView\n                android:id=\"@+id/tv_dur_time\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                tools:ignore=\"RtlSymmetry\" />\n\n            <com.kunfei.bookshelf.widget.views.ATESeekBar\n                android:id=\"@+id/seek_bar\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"25dp\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"1\" />\n\n            <TextView\n                android:id=\"@+id/tv_all_time\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:paddingLeft=\"15dp\"\n            android:paddingRight=\"15dp\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\">\n\n            <ImageView\n                android:id=\"@+id/iv_timer\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:background=\"@drawable/bg_ib_pre_round\"\n                android:contentDescription=\"@string/set_timer\"\n                android:src=\"@drawable/ic_timer_black_24dp\" />\n\n            <View\n                android:layout_width=\"0dp\"\n                android:layout_height=\"1dp\"\n                android:layout_weight=\"1\" />\n\n            <ImageView\n                android:id=\"@+id/iv_skip_previous\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:background=\"@drawable/bg_ib_pre_round\"\n                android:contentDescription=\"@string/skip_previous\"\n                android:src=\"@drawable/ic_skip_previous\" />\n\n            <com.google.android.material.floatingactionbutton.FloatingActionButton\n                android:id=\"@+id/fab_play_stop\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_margin=\"10dp\"\n                android:contentDescription=\"@string/read_aloud\"\n                android:src=\"@drawable/ic_play_24dp\"\n                android:tint=\"@color/tv_text_default\"\n                app:backgroundTint=\"@color/background_menu\"\n                app:elevation=\"2dp\"\n                app:fabSize=\"normal\"\n                app:pressedTranslationZ=\"2dp\" />\n\n            <ImageView\n                android:id=\"@+id/iv_skip_next\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:background=\"@drawable/bg_ib_pre_round\"\n                android:contentDescription=\"@string/skip_next\"\n                android:src=\"@drawable/ic_skip_next\" />\n\n            <View\n                android:layout_width=\"0dp\"\n                android:layout_height=\"1dp\"\n                android:layout_weight=\"1\" />\n\n            <ImageView\n                android:id=\"@+id/iv_chapter\"\n                android:layout_width=\"36dp\"\n                android:layout_height=\"36dp\"\n                android:background=\"@drawable/bg_ib_pre_round\"\n                android:src=\"@drawable/ic_chapter_list\"\n                android:contentDescription=\"@string/chapter_list\" />\n\n        </LinearLayout>\n\n    </LinearLayout>\n\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/pop_more_setting.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <View style=\"@style/Style.Shadow.Bottom\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"310dp\"\n        android:background=\"@color/background_menu\">\n\n        <View\n            android:id=\"@+id/vw_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n        <ScrollView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"vertical\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <LinearLayout\n                        android:id=\"@+id/ll_screen_direction\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"48dp\"\n                        android:background=\"@color/background_menu\"\n                        android:orientation=\"horizontal\"\n                        android:paddingLeft=\"15dp\"\n                        android:paddingRight=\"15dp\">\n\n                        <TextView\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:layout_weight=\"1\"\n                            android:textColor=\"@color/menu_color_default\"\n                            android:text=\"@string/screen_direction\" />\n\n                        <TextView\n                            android:id=\"@+id/tv_screen_direction\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:textColor=\"@color/menu_color_default\"\n                            android:text=\"@string/screen_unspecified\" />\n\n                    </LinearLayout>\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <LinearLayout\n                        android:id=\"@+id/llScreenTimeOut\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"48dp\"\n                        android:background=\"@color/background_menu\"\n                        android:orientation=\"horizontal\"\n                        android:paddingLeft=\"15dp\"\n                        android:paddingRight=\"15dp\">\n\n                        <TextView\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:layout_weight=\"1\"\n                            android:textColor=\"@color/menu_color_default\"\n                            android:text=\"@string/keep_light\" />\n\n                        <TextView\n                            android:id=\"@+id/tv_screen_time_out\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:textColor=\"@color/menu_color_default\"\n                            android:text=\"@string/keep_light\" />\n\n                    </LinearLayout>\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <LinearLayout\n                        android:id=\"@+id/llJFConvert\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"48dp\"\n                        android:background=\"@color/background_menu\"\n                        android:orientation=\"horizontal\"\n                        android:paddingLeft=\"15dp\"\n                        android:paddingRight=\"15dp\">\n\n                        <TextView\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:layout_weight=\"1\"\n                            android:textColor=\"@color/menu_color_default\"\n                            android:text=\"@string/jf_convert\" />\n\n                        <TextView\n                            android:id=\"@+id/tvJFConvert\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:textColor=\"@color/menu_color_default\"\n                            android:text=\"@string/jf_convert_o\" />\n\n                    </LinearLayout>\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_light_novel_paragraph\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/light_novel_paragraph\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/llImmersionStatusBar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sbImmersionStatusBar\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/immersion_status_bar\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_hideStatusBar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_hideStatusBar\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/pt_hide_status_bar\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_to_lh\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_to_lh\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/to_lh\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_hideNavigationBar\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_hideNavigationBar\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/pt_hide_navigation_bar\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/llNavigationBarColor\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <LinearLayout\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"48dp\"\n                        android:background=\"@color/background_menu\"\n                        android:orientation=\"horizontal\"\n                        android:paddingLeft=\"15dp\"\n                        android:paddingRight=\"15dp\">\n\n                        <TextView\n                            android:id=\"@+id/reNavBarColor\"\n                            android:layout_width=\"0dp\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:layout_weight=\"1\"\n                            android:textColor=\"@color/menu_color_default\"\n                            android:text=\"@string/re_navigation_bar_color\" />\n\n                        <TextView\n                            android:id=\"@+id/reNavBarColor_val\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:layout_gravity=\"center_vertical\"\n                            android:textColor=\"@color/selector_menu_text\"\n                            android:text=\"@string/black\" />\n\n                    </LinearLayout>\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_show_title\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/showTitle\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sw_volume_next_page\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/volume_open_page\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_read_aloud_key\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sw_read_aloud_key\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/aloud_volume_page\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_click\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/click_open_page\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_click_all_next\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_click_all_next\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/click_all_next_page\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_click_key_code\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <TextView\n                        android:id=\"@+id/sb_click_key_code\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/c_page_key\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_showLine\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/showLine\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_showTimeBattery\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_showTimeBattery\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/showTimeBattery\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n                <LinearLayout\n                    android:id=\"@+id/ll_select_text\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:background=\"@color/background_menu\"\n                    android:orientation=\"vertical\">\n\n                    <com.kunfei.bookshelf.widget.views.ATESwitch\n                        android:id=\"@+id/sb_select_text\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"wrap_content\"\n                        android:padding=\"15dp\"\n                        android:textColor=\"@color/menu_color_default\"\n                        android:text=\"@string/select_text\" />\n\n                    <View\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0.8dp\"\n                        android:background=\"@color/btn_bg_press\" />\n\n                </LinearLayout>\n\n            </LinearLayout>\n\n        </ScrollView>\n\n    </FrameLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/pop_read_adjust.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <View style=\"@style/Style.Shadow.Bottom\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"220dp\"\n        android:background=\"@color/background_menu\">\n\n        <View\n            android:id=\"@+id/vw_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"72dp\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"vertical\"\n                android:paddingLeft=\"10dp\"\n                android:paddingRight=\"10dp\">\n\n                <LinearLayout\n                    android:id=\"@+id/ll_follow_sys\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"8dp\">\n\n                    <TextView\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_weight=\"1\"\n                        android:text=\"@string/brightness\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"14sp\" />\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:layout_marginLeft=\"7dp\"\n                        android:layout_marginRight=\"7dp\"\n                        android:text=\"@string/flow_sys\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"14sp\" />\n\n                    <com.kunfei.bookshelf.widget.check_box.SmoothCheckBox\n                        android:id=\"@+id/scb_follow_sys\"\n                        android:layout_width=\"16dp\"\n                        android:layout_height=\"16dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:clickable=\"false\"\n                        android:contentDescription=\"@string/flow_sys\"\n                        app:duration=\"150\" />\n                </LinearLayout>\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_light\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"30dp\"\n                    android:layout_marginStart=\"15dp\"\n                    android:layout_marginEnd=\"15dp\"\n                    android:max=\"255\" />\n\n            </LinearLayout>\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0.8dp\"\n                android:background=\"@color/btn_bg_press\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"72dp\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"vertical\"\n                android:paddingLeft=\"10dp\"\n                android:paddingRight=\"10dp\">\n\n                <LinearLayout\n                    android:id=\"@+id/ll_tts_SpeechRate\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"8dp\">\n\n                    <TextView\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_weight=\"1\"\n                        android:text=\"@string/read_aloud_speed\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"14sp\" />\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:layout_marginLeft=\"7dp\"\n                        android:layout_marginRight=\"7dp\"\n                        android:text=\"@string/flow_sys\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"14sp\" />\n\n                    <com.kunfei.bookshelf.widget.check_box.SmoothCheckBox\n                        android:id=\"@+id/scb_tts_follow_sys\"\n                        android:layout_width=\"16dp\"\n                        android:layout_height=\"16dp\"\n                        android:layout_gravity=\"center_vertical\"\n                        android:clickable=\"false\"\n                        android:contentDescription=\"@string/flow_sys\"\n                        app:duration=\"150\" />\n                </LinearLayout>\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_tts_SpeechRate\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"30dp\"\n                    android:layout_marginStart=\"15dp\"\n                    android:layout_marginEnd=\"15dp\"\n                    android:max=\"95\" />\n\n            </LinearLayout>\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0.8dp\"\n                android:background=\"@color/btn_bg_press\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"vertical\"\n                android:paddingLeft=\"10dp\"\n                android:paddingRight=\"10dp\">\n\n                <LinearLayout\n                    android:id=\"@+id/ll_click\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:orientation=\"horizontal\"\n                    android:padding=\"8dp\">\n\n                    <TextView\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:layout_weight=\"1\"\n                        android:text=\"@string/auto_next_page_speed\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"14sp\" />\n\n                    <TextView\n                        android:id=\"@+id/tv_auto_page\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center\"\n                        android:text=\"%dS\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"14sp\"\n                        tools:ignore=\"HardcodedText\" />\n\n                </LinearLayout>\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_click\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"30dp\"\n                    android:layout_marginStart=\"15dp\"\n                    android:layout_marginEnd=\"15dp\"\n                    android:max=\"180\" />\n\n            </LinearLayout>\n        </LinearLayout>\n\n    </FrameLayout>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/pop_read_adjust_margin.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <View style=\"@style/Style.Shadow.Bottom\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"345dp\"\n        android:background=\"@color/background_menu\">\n\n        <View\n            android:id=\"@+id/vw_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:paddingLeft=\"12dp\"\n            android:paddingRight=\"8dp\"\n            android:orientation=\"vertical\">\n\n            <TextView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:text=\"@string/custom_mr\"\n                android:paddingBottom=\"8dp\"\n                android:paddingTop=\"8dp\"\n                android:textFontWeight=\"600\"\n                android:textSize=\"18sp\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_rm\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_rm_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_rm\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_rm_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_rm\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_dm\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_dm_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_dm\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_dm_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_dm\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_f\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_f_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_f\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_f_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_f\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_z_t\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_t_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_z_t\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_t_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_z_t\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_z_l\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_l_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_z_l\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_l_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_z_l\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_z_r\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_r_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_z_r\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_r_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_z_r\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_z_b\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_b_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_z_b\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_z_b_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_z_b\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_t_t\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_t_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_t_t\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_t_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_t_t\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_t_l\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_l_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_t_l\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_l_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_t_l\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_t_r\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_r_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_t_r\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_r_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_t_r\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"horizontal\"\n                android:gravity=\"center\"\n                android:paddingBottom=\"2dp\">\n\n                <TextView\n                    android:layout_width=\"@dimen/adjust_margin_left\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"@string/custom_mr_t_b\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"@dimen/adjust_margin_titfs\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_b_remove\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_remove\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_mr_t_b\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"@dimen/adjust_margin_barH\"\n                    android:layout_weight=\"1\"\n                    android:max=\"255\" />\n\n                <androidx.appcompat.widget.AppCompatImageView\n                    android:id=\"@+id/iv_mr_t_b_add\"\n                    android:layout_width=\"24dp\"\n                    android:layout_height=\"24dp\"\n                    android:background=\"?android:attr/selectableItemBackground\"\n                    android:src=\"@drawable/ic_add\"\n                    android:tint=\"@color/tv_text_default\" />\n\n                <TextView\n                    android:id=\"@+id/tv_hpb_mr_t_b\"\n                    android:layout_width=\"@dimen/adjust_margin_right\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:text=\"%dS\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"13sp\"\n                    android:gravity=\"end\"\n                    tools:ignore=\"HardcodedText\" />\n\n            </LinearLayout>\n\n        </LinearLayout>\n\n    </FrameLayout>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/pop_read_interface.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <View style=\"@style/Style.Shadow.Bottom\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"205dp\"\n        android:paddingTop=\"8dp\"\n        android:background=\"@color/background_menu\">\n\n        <View\n            android:id=\"@+id/vw_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:orientation=\"vertical\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"56dp\"\n                android:layout_gravity=\"center\"\n                android:orientation=\"horizontal\"\n                android:paddingLeft=\"15dp\"\n                android:paddingTop=\"5dp\"\n                android:paddingRight=\"15dp\"\n                android:paddingBottom=\"10dp\">\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/nbTextSizeDec\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"3.5\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"A-\"\n                    android:textFontWeight=\"800\"\n                    android:textSize=\"14sp\" />\n\n                <TextView\n                    android:id=\"@+id/nbTextSize\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"36dp\"\n                    android:gravity=\"center_vertical\"\n                    android:paddingLeft=\"4dp\"\n                    android:paddingRight=\"4dp\"\n                    android:text=\"20\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/nbTextSizeAdd\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"3.5\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"A+\"\n                    android:textFontWeight=\"800\"\n                    android:textSize=\"14sp\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"0.5\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/fl_text_Bold\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"4\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"@string/text_bold\"\n                    android:textSize=\"14sp\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"0.5\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/fl_text_font\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"4\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"@string/custom_font\"\n                    android:textSize=\"14sp\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"0.5\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/fl_indent\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"4\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"@string/indent\"\n                    android:textSize=\"14sp\" />\n\n            </LinearLayout>\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0.8dp\"\n                android:background=\"@color/btn_bg_press\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"60dp\"\n                android:layout_gravity=\"center\"\n                android:orientation=\"horizontal\"\n                android:paddingLeft=\"15dp\"\n                android:paddingTop=\"12dp\"\n                android:paddingRight=\"15dp\"\n                android:paddingBottom=\"10dp\">\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tvPageMode\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"5\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"@string/page_mode\"\n                    android:textSize=\"14sp\"\n                    tools:ignore=\"HardcodedText\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"1.0\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tvRowDef0\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"4\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:textSize=\"14sp\"\n                    android:text=\"@string/row_0\"\n                    tools:ignore=\"HardcodedText\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"0.5\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tvRowDef1\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"4\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"@string/row_1\"\n                    android:textSize=\"14sp\"\n                    tools:ignore=\"HardcodedText\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"0.5\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tvRowDef2\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"4\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"@string/row_2\"\n                    android:textSize=\"14sp\"\n                    tools:ignore=\"HardcodedText\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"0.5\" />\n\n                <com.kunfei.bookshelf.widget.views.ATEStrokeTextView\n                    android:id=\"@+id/tvRowDef\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_gravity=\"center\"\n                    android:layout_weight=\"4\"\n                    android:gravity=\"center\"\n                    app:cornerRadius=\"16dp\"\n                    android:text=\"@string/row_def\"\n                    android:textSize=\"14sp\"\n                    tools:ignore=\"HardcodedText\" />\n\n                <Space\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"0.5\" />\n\n                <FrameLayout\n                    android:layout_width=\"40dp\"\n                    android:layout_height=\"36dp\"\n                    android:layout_weight=\"1\">\n\n                    <de.hdodenhof.circleimageview.CircleImageView\n                        android:id=\"@+id/tvOther\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:layout_gravity=\"center\"\n                        android:contentDescription=\"@string/custom_mr\"\n                        android:tooltipText=\"@string/custom_mr\"\n                        android:src=\"@color/background_menu\"\n                        app:civ_border_color=\"@color/tv_text_default\"\n                        app:civ_border_width=\"1dp\"\n                        tools:ignore=\"HardcodedText\" />\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:layout_weight=\"1\"\n                        android:padding=\"5dp\"\n                        android:src=\"@drawable/ic_add\"\n                        app:tint=\"@color/tv_text_default\"\n                        tools:ignore=\"NestedWeights\" />\n\n                </FrameLayout>\n\n            </LinearLayout>\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"0.8dp\"\n                android:background=\"@color/btn_bg_press\" />\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"@color/background_menu\"\n                android:orientation=\"vertical\"\n                android:paddingTop=\"12dp\">\n\n                <LinearLayout\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_gravity=\"center\"\n                    android:baselineAligned=\"false\"\n                    android:orientation=\"horizontal\"\n                    android:paddingTop=\"5dp\"\n                    android:paddingBottom=\"9dp\">\n\n                    <Space\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"1dp\"\n                        android:layout_weight=\"1\" />\n\n                    <FrameLayout\n                        android:layout_width=\"40dp\"\n                        android:layout_height=\"40dp\"\n                        android:layout_weight=\"1\">\n\n                        <de.hdodenhof.circleimageview.CircleImageView\n                            android:id=\"@+id/civ_bg_white\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_gravity=\"center\"\n                            android:contentDescription=\"背景1\"\n                            android:src=\"@color/background_menu\"\n                            app:civ_border_color=\"@color/tv_text_default\"\n                            app:civ_border_width=\"1dp\"\n                            tools:ignore=\"HardcodedText\" />\n\n                        <TextView\n                            android:id=\"@+id/tv0\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:gravity=\"center\"\n                            android:text=\"@string/text\"\n                            android:textColor=\"@color/tv_text_default\"\n                            android:textSize=\"12sp\" />\n\n                    </FrameLayout>\n\n                    <Space\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"1dp\"\n                        android:layout_weight=\"2\" />\n\n                    <FrameLayout\n                        android:layout_width=\"40dp\"\n                        android:layout_height=\"40dp\"\n                        android:layout_weight=\"1\">\n\n                        <de.hdodenhof.circleimageview.CircleImageView\n                            android:id=\"@+id/civ_bg_yellow\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_gravity=\"center\"\n                            android:contentDescription=\"背景2\"\n                            android:src=\"@color/background_menu\"\n                            app:civ_border_color=\"@color/tv_text_default\"\n                            app:civ_border_width=\"1dp\"\n                            tools:ignore=\"HardcodedText\" />\n\n                        <TextView\n                            android:id=\"@+id/tv1\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:gravity=\"center\"\n                            android:text=\"@string/text\"\n                            android:textColor=\"@color/tv_text_default\"\n                            android:textSize=\"12sp\" />\n\n                    </FrameLayout>\n\n                    <Space\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"1dp\"\n                        android:layout_weight=\"2\" />\n\n                    <FrameLayout\n                        android:layout_width=\"40dp\"\n                        android:layout_height=\"40dp\"\n                        android:layout_weight=\"1\">\n\n                        <de.hdodenhof.circleimageview.CircleImageView\n                            android:id=\"@+id/civ_bg_green\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_gravity=\"center\"\n                            android:contentDescription=\"背景3\"\n                            android:src=\"@color/background_menu\"\n                            app:civ_border_color=\"@color/tv_text_default\"\n                            app:civ_border_width=\"1dp\"\n                            tools:ignore=\"HardcodedText\" />\n\n                        <TextView\n                            android:id=\"@+id/tv2\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:gravity=\"center\"\n                            android:text=\"@string/text\"\n                            android:textColor=\"@color/tv_text_default\"\n                            android:textSize=\"12sp\" />\n\n                    </FrameLayout>\n\n                    <Space\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"1dp\"\n                        android:layout_weight=\"2\" />\n\n                    <FrameLayout\n                        android:layout_width=\"40dp\"\n                        android:layout_height=\"40dp\"\n                        android:layout_weight=\"1\">\n\n                        <de.hdodenhof.circleimageview.CircleImageView\n                            android:id=\"@+id/civ_bg_blue\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_gravity=\"center\"\n                            android:contentDescription=\"背景4\"\n                            android:src=\"@color/background_menu\"\n                            app:civ_border_color=\"@color/tv_text_default\"\n                            app:civ_border_width=\"1dp\"\n                            tools:ignore=\"HardcodedText\" />\n\n                        <TextView\n                            android:id=\"@+id/tv3\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:gravity=\"center\"\n                            android:text=\"@string/text\"\n                            android:textColor=\"@color/tv_text_default\"\n                            android:textSize=\"12sp\" />\n\n                    </FrameLayout>\n\n                    <Space\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"1dp\"\n                        android:layout_weight=\"2\" />\n\n                    <FrameLayout\n                        android:layout_width=\"40dp\"\n                        android:layout_height=\"40dp\"\n                        android:layout_weight=\"1\">\n\n                        <de.hdodenhof.circleimageview.CircleImageView\n                            android:id=\"@+id/civ_bg_black\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:layout_gravity=\"center\"\n                            android:contentDescription=\"背景5\"\n                            android:src=\"@color/background_menu\"\n                            app:civ_border_color=\"@color/tv_text_default\"\n                            app:civ_border_width=\"1dp\"\n                            tools:ignore=\"HardcodedText\" />\n\n                        <TextView\n                            android:id=\"@+id/tv4\"\n                            android:layout_width=\"match_parent\"\n                            android:layout_height=\"match_parent\"\n                            android:gravity=\"center\"\n                            android:text=\"@string/text\"\n                            android:textColor=\"@color/tv_text_default\"\n                            android:textSize=\"12sp\" />\n\n                    </FrameLayout>\n\n                    <Space\n                        android:layout_width=\"0dp\"\n                        android:layout_height=\"1dp\"\n                        android:layout_weight=\"1\" />\n\n                </LinearLayout>\n\n            </LinearLayout>\n        </LinearLayout>\n\n    </FrameLayout>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/pop_read_long_press.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"190dp\"\n    android:layout_height=\"30dp\"\n    android:layout_gravity=\"center_horizontal\"\n    android:alpha=\"0.9\"\n    android:background=\"#000000\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:orientation=\"vertical\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"30dp\"\n        android:layout_gravity=\"center_horizontal\"\n        android:layout_marginTop=\"0.0dip\"\n        android:layout_marginBottom=\"0.0dip\"\n        android:paddingLeft=\"12.0dip\"\n        android:paddingRight=\"12.0dip\">\n\n\n        <FrameLayout\n            android:id=\"@+id/fl_cp\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:paddingLeft=\"4.0dip\"\n            android:paddingRight=\"4.0dip\">\n\n            <TextView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:gravity=\"center\"\n                android:text=\"复制\"\n                android:textColor=\"#ffbbbbbb\"\n                android:textSize=\"14sp\" />\n\n        </FrameLayout>\n\n\n        <FrameLayout\n            android:id=\"@+id/fl_replace\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:paddingLeft=\"4.0dip\"\n            android:paddingRight=\"4.0dip\">\n\n            <TextView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:gravity=\"center\"\n                android:text=\"替换\"\n                android:textColor=\"#ffbbbbbb\"\n                android:textSize=\"14sp\" />\n\n        </FrameLayout>\n\n        <FrameLayout\n            android:id=\"@+id/fl_replace_ad\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:paddingLeft=\"4.0dip\"\n            android:paddingRight=\"4.0dip\">\n\n            <TextView\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:gravity=\"center\"\n                android:text=\"广告\"\n                android:textColor=\"#ffbbbbbb\"\n                android:textSize=\"14sp\" />\n\n        </FrameLayout>\n\n    </LinearLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/pop_read_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <LinearLayout\n        android:id=\"@+id/ll_read_aloud_timer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"60dp\"\n        android:orientation=\"horizontal\"\n        android:paddingStart=\"32dp\"\n        android:paddingEnd=\"32dp\"\n        android:visibility=\"gone\">\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fab_read_aloud_timer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:contentDescription=\"@string/set_timer\"\n            android:src=\"@drawable/ic_timer_black_24dp\"\n            android:tint=\"@color/tv_text_default\"\n            app:backgroundTint=\"@color/background_menu\"\n            app:elevation=\"2dp\"\n            app:fabSize=\"mini\"\n            app:pressedTranslationZ=\"2dp\" />\n\n        <androidx.cardview.widget.CardView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            app:cardBackgroundColor=\"@color/background_card\">\n\n            <TextView\n                android:id=\"@+id/tv_read_aloud_timer\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"10dp\"\n                android:text=\"@string/read_aloud_timer\" />\n        </androidx.cardview.widget.CardView>\n\n    </LinearLayout>\n\n    <LinearLayout\n        android:id=\"@+id/ll_floating_button\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"60dp\"\n        android:orientation=\"horizontal\"\n        android:paddingStart=\"32dp\"\n        android:paddingEnd=\"32dp\">\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fabReadAloud\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:contentDescription=\"@string/read_aloud\"\n            android:src=\"@drawable/ic_read_aloud\"\n            android:tint=\"@color/tv_text_default\"\n            app:backgroundTint=\"@color/background_menu\"\n            app:elevation=\"2dp\"\n            app:fabSize=\"mini\"\n            app:pressedTranslationZ=\"2dp\" />\n\n        <Space\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fabAutoPage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:contentDescription=\"@string/auto_next_page\"\n            android:src=\"@drawable/ic_auto_page\"\n            android:tint=\"@color/tv_text_default\"\n            app:backgroundTint=\"@color/background_menu\"\n            app:elevation=\"2dp\"\n            app:fabSize=\"mini\"\n            app:pressedTranslationZ=\"2dp\" />\n\n        <Space\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fabReplaceRule\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:contentDescription=\"@string/replace_rule_title\"\n            android:src=\"@drawable/ic_find_replace\"\n            android:tint=\"@color/tv_text_default\"\n            app:backgroundTint=\"@color/background_menu\"\n            app:elevation=\"2dp\"\n            app:fabSize=\"mini\"\n            app:pressedTranslationZ=\"2dp\" />\n\n        <Space\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fabNightTheme\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_margin=\"10dp\"\n            android:contentDescription=\"@string/night_theme\"\n            android:src=\"@drawable/ic_brightness\"\n            android:tint=\"@color/tv_text_default\"\n            app:backgroundTint=\"@color/background_menu\"\n            app:elevation=\"2dp\"\n            app:fabSize=\"mini\"\n            app:pressedTranslationZ=\"2dp\" />\n\n    </LinearLayout>\n\n    <View style=\"@style/Style.Shadow.Bottom\" />\n\n    <!--底部设置栏-->\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"100dp\"\n        android:background=\"@color/background_menu\">\n\n        <View\n            android:id=\"@+id/vw_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n        <LinearLayout\n            android:id=\"@+id/llNavigationBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\">\n            <!--章节设置-->\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginLeft=\"20dp\"\n                android:layout_marginTop=\"5dp\"\n                android:layout_marginRight=\"20dp\"\n                android:layout_marginBottom=\"5dp\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/tv_pre\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginStart=\"10dp\"\n                    android:layout_marginEnd=\"10dp\"\n                    android:background=\"@drawable/bg_ib_pre_round\"\n                    android:clickable=\"true\"\n                    android:enabled=\"false\"\n                    android:focusable=\"true\"\n                    android:paddingTop=\"10dp\"\n                    android:paddingBottom=\"10dp\"\n                    android:text=\"@string/previous_chapter\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"14sp\" />\n\n                <com.kunfei.bookshelf.widget.views.ATESeekBar\n                    android:id=\"@+id/hpb_read_progress\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"25dp\"\n                    android:layout_gravity=\"center_vertical\"\n                    android:layout_weight=\"1\" />\n\n                <TextView\n                    android:id=\"@+id/tv_next\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginStart=\"10dp\"\n                    android:layout_marginEnd=\"10dp\"\n                    android:background=\"@drawable/bg_ib_pre_round\"\n                    android:clickable=\"true\"\n                    android:enabled=\"false\"\n                    android:focusable=\"true\"\n                    android:paddingTop=\"10dp\"\n                    android:paddingBottom=\"10dp\"\n                    android:text=\"@string/next_chapter\"\n                    android:textColor=\"@color/tv_text_default\"\n                    android:textSize=\"14sp\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"50dp\"\n                android:baselineAligned=\"false\"\n                android:orientation=\"horizontal\">\n\n                <View\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"1\" />\n\n                <!--目录按钮-->\n                <LinearLayout\n                    android:id=\"@+id/ll_catalog\"\n                    android:layout_width=\"50dp\"\n                    android:layout_height=\"50dp\"\n                    android:background=\"@drawable/bg_ib_pre_round\"\n                    android:clickable=\"true\"\n                    android:contentDescription=\"@string/chapter_list\"\n                    android:focusable=\"true\"\n                    android:orientation=\"vertical\"\n                    android:paddingBottom=\"7dp\">\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0dp\"\n                        android:layout_weight=\"1\"\n                        android:contentDescription=\"@string/chapter_list\"\n                        android:src=\"@drawable/ic_chapter_list\"\n                        app:tint=\"@color/tv_text_default\"\n                        tools:ignore=\"NestedWeights\" />\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_horizontal\"\n                        android:layout_marginTop=\"3dp\"\n                        android:text=\"@string/chapter_list\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"12sp\" />\n                </LinearLayout>\n\n                <View\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"2\" />\n                <!--调节按钮-->\n                <LinearLayout\n                    android:id=\"@+id/ll_adjust\"\n                    android:layout_width=\"50dp\"\n                    android:layout_height=\"50dp\"\n                    android:background=\"@drawable/bg_ib_pre_round\"\n                    android:clickable=\"true\"\n                    android:contentDescription=\"@string/adjust\"\n                    android:focusable=\"true\"\n                    android:orientation=\"vertical\"\n                    android:paddingBottom=\"7dp\">\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0dp\"\n                        android:layout_weight=\"1\"\n                        android:contentDescription=\"@string/adjust\"\n                        android:src=\"@drawable/ic_tune\"\n                        app:tint=\"@color/tv_text_default\"\n                        tools:ignore=\"NestedWeights\" />\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_horizontal\"\n                        android:layout_marginTop=\"3dp\"\n                        android:text=\"@string/adjust\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"12sp\" />\n                </LinearLayout>\n\n                <View\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"2\" />\n                <!--界面按钮-->\n                <LinearLayout\n                    android:id=\"@+id/ll_font\"\n                    android:layout_width=\"50dp\"\n                    android:layout_height=\"50dp\"\n                    android:background=\"@drawable/bg_ib_pre_round\"\n                    android:clickable=\"true\"\n                    android:contentDescription=\"@string/interface_setting\"\n                    android:focusable=\"true\"\n                    android:orientation=\"vertical\"\n                    android:paddingBottom=\"7dp\">\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0dp\"\n                        android:layout_weight=\"1\"\n                        android:contentDescription=\"@string/interface_setting\"\n                        android:src=\"@drawable/ic_interface_setting\"\n                        app:tint=\"@color/tv_text_default\"\n                        tools:ignore=\"NestedWeights\" />\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_horizontal\"\n                        android:layout_marginTop=\"3dp\"\n                        android:text=\"@string/interface_setting\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"12sp\" />\n                </LinearLayout>\n\n                <View\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"2\" />\n                <!--设置按钮-->\n                <LinearLayout\n                    android:id=\"@+id/ll_setting\"\n                    android:layout_width=\"50dp\"\n                    android:layout_height=\"50dp\"\n                    android:background=\"@drawable/bg_ib_pre_round\"\n                    android:clickable=\"true\"\n                    android:contentDescription=\"@string/setting\"\n                    android:focusable=\"true\"\n                    android:orientation=\"vertical\"\n                    android:paddingBottom=\"7dp\">\n\n                    <androidx.appcompat.widget.AppCompatImageView\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"0dp\"\n                        android:layout_weight=\"1\"\n                        android:contentDescription=\"@string/setting\"\n                        android:src=\"@drawable/ic_settings\"\n                        app:tint=\"@color/tv_text_default\"\n                        tools:ignore=\"NestedWeights\" />\n\n                    <TextView\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_horizontal\"\n                        android:layout_marginTop=\"3dp\"\n                        android:text=\"@string/setting\"\n                        android:textColor=\"@color/tv_text_default\"\n                        android:textSize=\"12sp\" />\n                </LinearLayout>\n\n                <View\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_weight=\"1\" />\n            </LinearLayout>\n\n            <View\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\" />\n        </LinearLayout>\n    </FrameLayout>\n\n    <View\n        android:id=\"@+id/vwNavigationBar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:background=\"@color/background_menu\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/popup_keyboard_tool.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.recyclerview.widget.RecyclerView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/recycler_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center\"\n    android:background=\"@color/background_card\"\n    android:padding=\"5dp\" />\n"
  },
  {
    "path": "app/src/main/res/layout/tab_view_icon_right.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"48dp\"\n    android:gravity=\"center\"\n    android:orientation=\"horizontal\">\n\n    <TextView\n        android:id=\"@+id/tabtext\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center\"\n        android:text=\"TAB\"\n        android:textColor=\"@color/tv_text_default\"\n        android:textSize=\"14sp\" />\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/tabicon\"\n        android:layout_width=\"24dp\"\n        android:layout_height=\"24dp\"\n        android:layout_gravity=\"center\"\n        android:src=\"@mipmap/ic_launcher\"\n        app:tint=\"@color/tv_text_default\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_empty.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/rl_empty_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_centerInParent=\"true\"\n    android:orientation=\"vertical\"\n    android:visibility=\"gone\">\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerInParent=\"true\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/tv_empty\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"10dp\"\n            android:text=\"暂无数据\"\n            android:textColor=\"@color/tv_text_default\" />\n    </LinearLayout>\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_fastscroller.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<merge\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <TextView\n        android:id=\"@+id/fastscroll_bubble\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"end\"\n        android:gravity=\"center\"\n        android:maxLines=\"1\"\n        android:textSize=\"@dimen/fastscroll_bubble_textsize\"\n        android:visibility=\"gone\"\n        tools:background=\"@drawable/fastscroll_bubble\"\n        tools:text=\"A\"\n        tools:textColor=\"#ffffff\"\n        tools:visibility=\"visible\" />\n\n    <FrameLayout\n        android:id=\"@+id/fastscroll_scrollbar\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:paddingEnd=\"@dimen/fastscroll_scrollbar_padding_end\"\n        android:paddingLeft=\"@dimen/fastscroll_scrollbar_padding_start\"\n        android:paddingRight=\"@dimen/fastscroll_scrollbar_padding_end\"\n        android:paddingStart=\"@dimen/fastscroll_scrollbar_padding_start\"\n        android:visibility=\"gone\"\n        tools:visibility=\"visible\">\n\n        <ImageView\n            android:id=\"@+id/fastscroll_track\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"center_horizontal\"\n            tools:ignore=\"ContentDescription\"\n            tools:src=\"@drawable/fastscroll_track\" />\n\n        <ImageView\n            android:id=\"@+id/fastscroll_handle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            tools:ignore=\"ContentDescription\"\n            tools:src=\"@drawable/fastscroll_handle\" />\n\n    </FrameLayout>\n\n</merge>"
  },
  {
    "path": "app/src/main/res/layout/view_file_picker.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/rv_path\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"30dp\" />\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0.2dp\"\n        android:background=\"@color/btn_bg_press_2\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.recyclerview.widget.RecyclerView\n            android:id=\"@+id/rv_file\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\" />\n\n        <TextView\n            android:id=\"@+id/tv_empty\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:gravity=\"center\"\n            android:textColor=\"@color/tv_text_secondary\"\n            android:visibility=\"gone\" />\n\n    </FrameLayout>\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ImageView android:id=\"@+id/preview\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"48dp\"\n    android:layout_height=\"48dp\" />"
  },
  {
    "path": "app/src/main/res/layout/view_loading.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n              android:layout_width=\"match_parent\"\n              android:layout_height=\"match_parent\">\n\n    <ProgressBar\n        android:id=\"@+id/loadding_pb\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerInParent=\"true\"/>\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_net_error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n                android:id=\"@+id/rl_empty_view\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:layout_centerInParent=\"true\"\n                android:orientation=\"vertical\">\n\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerInParent=\"true\"\n        android:background=\"@color/background\"\n        android:orientation=\"vertical\">\n\n        <TextView\n            android:id=\"@+id/tvEmptyView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"10dp\"\n            android:text=\"网络连接失败...\"\n            android:textColor=\"@color/tv_text_default\" />\n    </LinearLayout>\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_night_theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <androidx.appcompat.widget.AppCompatImageView\n        android:id=\"@+id/iv_theme_day_night\"\n        android:layout_width=\"36dp\"\n        android:layout_height=\"36dp\"\n        android:layout_centerHorizontal=\"true\"\n        android:layout_centerVertical=\"true\" />\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_number_buttom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:layout_marginLeft=\"3dp\"\n    android:background=\"@drawable/shape_radius_1dp\"\n    android:orientation=\"horizontal\"\n    tools:ignore=\"RtlHardcoded\">\n\n    <TextView\n        android:id=\"@+id/button_sub\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center\"\n        android:text=\"-\"\n        tools:ignore=\"HardcodedText\" />\n\n    <TextView\n        android:id=\"@+id/tv_number\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center\"\n        android:text=\"1\"\n        tools:ignore=\"HardcodedText\" />\n\n    <TextView\n        android:id=\"@+id/button_add\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center\"\n        android:text=\"+\"\n        tools:ignore=\"HardcodedText\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_recycler_font.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingStart=\"30dp\"\n    android:paddingEnd=\"30dp\">\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_refresh_error.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@color/background\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:gravity=\"center\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:id=\"@+id/tv_error_msg\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/get_data_error\"\n        android:textColor=\"@color/tv_text_default\"\n        android:textSize=\"16sp\" />\n\n    <com.kunfei.bookshelf.widget.views.ATEAccentBgTextView\n        android:id=\"@+id/tv_refresh_again\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_marginTop=\"20dp\"\n        android:gravity=\"center\"\n        android:lines=\"1\"\n        android:paddingLeft=\"18dp\"\n        android:paddingTop=\"7dp\"\n        android:paddingRight=\"18dp\"\n        android:paddingBottom=\"7dp\"\n        android:text=\"@string/retry\"\n        android:textSize=\"16sp\" />\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/view_refresh_load_more.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clickable=\"true\"\n    android:id=\"@+id/ll_loadmore\">\n    <TextView\n        android:id=\"@+id/tv_loadmore\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:text=\"正在加载...\"\n        android:padding=\"10dp\"\n        android:layout_gravity=\"center\"\n        android:textSize=\"14sp\"\n        android:textColor=\"#A0ABAE\"/>\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_refresh_no_data.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:orientation=\"vertical\" android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:background=\"@color/background\"\n        android:clickable=\"true\"\n        android:focusable=\"true\">\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:text=\"未搜索到相关书籍~\"\n            android:textColor=\"@color/tv_text_default\"\n            android:textSize=\"22sp\"/>\n    </FrameLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/view_refresh_recycler.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/ll_content\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.kunfei.bookshelf.widget.recycler.refresh.RefreshProgressBar\n        android:id=\"@+id/rpb\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"3dp\"\n        android:visibility=\"visible\" />\n\n    <androidx.recyclerview.widget.RecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:scrollbarStyle=\"outsideOverlay\"\n        android:scrollbars=\"vertical\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/menu/menu_book_download.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.BookSourceActivity\">\n\n    <item\n        android:id=\"@+id/action_cancel\"\n        android:icon=\"@drawable/ic_stop_black_24dp\"\n        android:title=\"@string/cancel\"\n        app:showAsAction=\"ifRoom\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_book_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/action_save\"\n        android:icon=\"@drawable/ic_save\"\n        android:title=\"@string/action_save\"\n        app:showAsAction=\"always\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/menu_book_read_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.MainActivity\">\n\n    <group android:id=\"@+id/menuOnLine\">\n        <item\n            android:id=\"@+id/action_change_source\"\n            android:icon=\"@drawable/ic_exchange\"\n            android:title=\"@string/change_origin\"\n            app:showAsAction=\"always\" />\n\n        <item\n            android:id=\"@+id/action_refresh\"\n            android:icon=\"@drawable/ic_refresh_black_24dp\"\n            android:title=\"@string/refresh\"\n            app:showAsAction=\"ifRoom\" />\n\n        <item\n            android:id=\"@+id/action_download\"\n            android:icon=\"@drawable/ic_download_line\"\n            android:title=\"@string/download_offline\"\n            app:showAsAction=\"ifRoom\" />\n\n        <item\n            android:id=\"@+id/disable_book_source\"\n            android:icon=\"@drawable/ic_cancel\"\n            android:title=\"@string/disable_book_source\"\n            app:showAsAction=\"never\" />\n\n    </group>\n\n    <group android:id=\"@+id/menu_text\">\n        <item\n            android:id=\"@+id/action_set_regex\"\n            android:icon=\"@drawable/ic_exchange\"\n            android:title=\"@string/txt_chapter_regex\"\n            app:showAsAction=\"always\" />\n    </group>\n\n    <group android:id=\"@+id/menuLocal\">\n\n        <item\n            android:id=\"@+id/action_set_charset\"\n            android:icon=\"@drawable/ic_translate\"\n            android:title=\"@string/set_charset\"\n            app:showAsAction=\"ifRoom\" />\n\n    </group>\n\n    <group android:id=\"@+id/menuShow\">\n        <item\n            android:id=\"@+id/add_bookmark\"\n            android:icon=\"@drawable/ic_bookmark\"\n            android:title=\"@string/bookmark_add\"\n            app:showAsAction=\"ifRoom\" />\n\n        <item\n            android:id=\"@+id/action_copy_text\"\n            android:icon=\"@drawable/ic_copy\"\n            android:title=\"@string/copy_text\"\n            app:showAsAction=\"ifRoom\" />\n\n        <item\n            android:id=\"@+id/enable_replace\"\n            android:icon=\"@drawable/ic_find_replace\"\n            android:title=\"@string/replace_rule_title\"\n            android:checkable=\"true\"\n            android:checked=\"true\"\n            app:showAsAction=\"never\" />\n\n        <item\n            android:id=\"@+id/update_chapter_list\"\n            android:icon=\"@drawable/ic_update\"\n            android:title=\"@string/update_chapter\"\n            app:showAsAction=\"never\" />\n\n        <item\n            android:id=\"@+id/action_book_info\"\n            android:icon=\"@drawable/ic_toc\"\n            android:title=\"@string/book_info\"\n            app:showAsAction=\"never\" />\n\n    </group>\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_book_search_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.MainActivity\">\n\n    <item\n        android:id=\"@+id/action_book_source_manage\"\n        android:title=\"@string/book_source_manage\"\n        app:showAsAction=\"never\" />\n\n    <group android:id=\"@+id/source_group\">\n\n    </group>\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_book_source_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.BookSourceActivity\">\n\n    <item\n        android:id=\"@+id/action_select_all\"\n        android:icon=\"@drawable/ic_select_all\"\n        android:title=\"@string/select_all\"\n        app:showAsAction=\"always\"\n        tools:ignore=\"AlwaysShowAction\" />\n\n    <item\n        android:id=\"@+id/action_group\"\n        android:icon=\"@drawable/ic_groups\"\n        android:title=\"@string/menu_action_group\"\n        app:showAsAction=\"always\"\n        tools:ignore=\"AlwaysShowAction\">\n\n        <menu>\n            <group android:id=\"@+id/sort\">\n                <item\n                    android:id=\"@+id/sort_manual\"\n                    android:checkable=\"true\"\n                    android:title=\"@string/sort_manual\" />\n\n                <item\n                    android:id=\"@+id/sort_auto\"\n                    android:checkable=\"true\"\n                    android:title=\"@string/sort_auto\" />\n\n                <item\n                    android:id=\"@+id/sort_pin_yin\"\n                    android:checkable=\"true\"\n                    android:title=\"@string/sort_pin_yin\" />\n\n                <item\n                    android:id=\"@+id/show_enabled\"\n                    android:title=\"@string/show_enabled\" />\n\n            </group>\n\n            <group android:id=\"@+id/source_group\">\n\n            </group>\n\n        </menu>\n\n    </item>\n\n    <item\n        android:id=\"@+id/action_add_book_source\"\n        android:icon=\"@drawable/ic_add\"\n        android:title=\"@string/add_book_source\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_import_book_source_local\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_book_source_local\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_import_book_source_onLine\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_book_source_on_line\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_import_book_source_rwm\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_by_qr_code\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_import_book_source_default\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_by_default\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_revert_selection\"\n        android:icon=\"@drawable/ic_swap_outline_24dp\"\n        android:title=\"@string/revert_selection\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_del_select\"\n        android:icon=\"@drawable/ic_clear_all\"\n        android:title=\"@string/del_select\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_check_book_source\"\n        android:icon=\"@drawable/ic_check_source\"\n        android:title=\"@string/check_select_source\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/action_check_find_source\"\n        android:icon=\"@drawable/ic_baseline_label\"\n        android:title=\"@string/check_find_source\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_share_wifi\"\n        android:icon=\"@drawable/ic_share\"\n        android:title=\"@string/wifi_share\"\n        app:showAsAction=\"never\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_book_source_edit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\r\n\r\n    <item\r\n        android:id=\"@+id/action_save\"\r\n        android:icon=\"@drawable/ic_save\"\r\n        android:title=\"@string/action_save\"\r\n        app:showAsAction=\"ifRoom\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_debug_source\"\r\n        android:icon=\"@drawable/ic_bug_report_black_24dp\"\r\n        android:title=\"@string/debug_source\"\r\n        app:showAsAction=\"ifRoom\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_login\"\r\n        android:title=\"@string/login\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_copy_source\"\r\n        android:title=\"@string/copy_source\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_copy_source_no_find\"\r\n        android:title=\"@string/copy_source_no_find\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_paste_source\"\r\n        android:title=\"@string/paste_source\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_qr_code_camera\"\r\n        android:title=\"@string/import_by_qr_code\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_share_it\"\r\n        android:title=\"@string/qr_share\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_share_str\"\r\n        android:title=\"@string/str_share\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_share_wifi\"\r\n        android:title=\"@string/wifi_share\"\r\n        app:showAsAction=\"never\" />\r\n\r\n    <item\r\n        android:id=\"@+id/action_rule_summary\"\r\n        android:title=\"@string/rule_summary\"\r\n        app:showAsAction=\"never\" />\r\n\r\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/menu_debug_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/action_scan\"\n        android:title=\"@string/camera_scan\"\n        android:icon=\"@drawable/ic_scan\"\n        app:showAsAction=\"always\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/menu_main_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.MainActivity\">\n\n    <item\n        android:id=\"@+id/action_add_local\"\n        android:icon=\"@drawable/ic_add\"\n        android:title=\"@string/book_local\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_add_url\"\n        android:icon=\"@drawable/ic_add_online\"\n        android:title=\"@string/add_url\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_add_qrcode\"\n        android:icon=\"@drawable/ic_scan\"\n        android:title=\"@string/add_qrcode\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_download_all\"\n        android:icon=\"@drawable/ic_download_line\"\n        android:title=\"@string/download_all\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/menu_bookshelf_layout\"\n        android:icon=\"@drawable/ic_view_quilt\"\n        android:title=\"@string/bookshelf_layout\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_arrange_bookshelf\"\n        android:icon=\"@drawable/ic_arrange\"\n        android:title=\"@string/arrange_bookshelf\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_web_start\"\n        android:icon=\"@drawable/ic_web_service_phone\"\n        android:title=\"@string/web_menu\"\n        app:showAsAction=\"never\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_main_drawer.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <group android:id=\"@+id/drawer_menu_book_source\">\n        <item\n            android:id=\"@+id/action_book_source_manage\"\n            android:icon=\"@drawable/ic_book_source_manage\"\n            android:title=\"@string/book_source_manage\"\n            app:tint=\"@color/tv_text_default\" />\n\n        <item\n            android:id=\"@+id/action_replace_rule\"\n            android:icon=\"@drawable/ic_find_replace\"\n            android:title=\"@string/replace_rule_title\"\n            app:tint=\"@color/tv_text_default\" />\n\n        <item\n            android:id=\"@+id/action_download\"\n            android:icon=\"@drawable/ic_download_line\"\n            android:title=\"@string/action_download\"\n            app:tint=\"@color/tv_text_default\" />\n    </group>\n\n    <group android:id=\"@+id/drawer_theme\">\n        <item\n            android:id=\"@+id/action_theme\"\n            android:icon=\"@drawable/ic_theme\"\n            android:title=\"@string/theme\"\n            app:actionLayout=\"@layout/view_night_theme\"\n            app:tint=\"@color/tv_text_default\" />\n    </group>\n\n    <group android:id=\"@+id/drawer_menu_setting\">\n        <item\n            android:id=\"@+id/action_setting\"\n            android:icon=\"@drawable/ic_settings\"\n            android:title=\"@string/setting\"\n            app:tint=\"@color/tv_text_default\" />\n\n        <item\n            android:id=\"@+id/action_about\"\n            android:icon=\"@drawable/ic_about\"\n            android:title=\"@string/about\"\n            app:tint=\"@color/tv_text_default\" />\n\n        <item\n            android:id=\"@+id/action_donate\"\n            android:icon=\"@drawable/ic_donate\"\n            android:title=\"@string/donate\"\n            app:tint=\"@color/tv_text_default\" />\n    </group>\n\n    <group android:id=\"@+id/drawer_menu_backup\">\n        <item\n            android:id=\"@+id/action_backup\"\n            android:icon=\"@drawable/ic_backup\"\n            android:title=\"@string/backup\"\n            app:tint=\"@color/tv_text_default\" />\n\n        <item\n            android:id=\"@+id/action_restore\"\n            android:icon=\"@drawable/ic_restore\"\n            android:title=\"@string/restore\"\n            app:tint=\"@color/tv_text_default\" />\n    </group>\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_qr_code_scan.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/action_choose_from_gallery\"\n        android:title=\"@string/gallery\"\n        app:showAsAction=\"always\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/menu_read_style_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.ReadStyleActivity\">\n\n    <item\n        android:id=\"@+id/action_save\"\n        android:icon=\"@drawable/ic_save\"\n        android:title=\"@string/action_save\"\n        app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_replace_rule_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.BookSourceActivity\">\n\n    <item\n        android:id=\"@+id/action_add_replace_rule\"\n        android:icon=\"@drawable/ic_add\"\n        android:title=\"@string/add\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_select_all\"\n        android:icon=\"@drawable/ic_select_all\"\n        android:title=\"@string/select_all\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_import\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_replace_rule\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_import_onLine\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_replace_rule_on_line\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_del_all\"\n        android:icon=\"@drawable/ic_clear_all\"\n        android:title=\"@string/del_all\"\n        app:showAsAction=\"never\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_search_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n\n    <item\n        android:id=\"@+id/action_search\"\n        android:title=\"@string/search\"\n        app:actionViewClass=\"androidx.appcompat.widget.SearchView\"\n        app:showAsAction=\"always\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/menu_source_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n        android:id=\"@+id/action_check\"\n        android:title=\"@string/success\"\n        android:icon=\"@drawable/ic_check\"\n        app:showAsAction=\"always\" />\n\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/menu_txt_chapter_rule_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.BookSourceActivity\">\n\n    <item\n        android:id=\"@+id/action_add_replace_rule\"\n        android:icon=\"@drawable/ic_add\"\n        android:title=\"@string/add\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_select_all\"\n        android:icon=\"@drawable/ic_select_all\"\n        android:title=\"@string/select_all\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_import\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_replace_rule\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_import_onLine\"\n        android:icon=\"@drawable/ic_import\"\n        android:title=\"@string/import_replace_rule_on_line\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_del_all\"\n        android:icon=\"@drawable/ic_clear_all\"\n        android:title=\"@string/del_all\"\n        app:showAsAction=\"never\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_update_activity.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".view.activity.MainActivity\">\n\n    <item\n        android:id=\"@+id/action_download\"\n        android:title=\"@string/download_update\"\n        app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/book_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/book_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/book_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/book_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/book_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/book_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string-array name=\"bookshelf_layout\">\n        <item>@string/layout_list</item>\n        <item>@string/layout_grid3</item>\n        <item>@string/layout_grid4</item>\n    </string-array>\n\n    <string-array name=\"indent\">\n        <item>@string/indent_0</item>\n        <item>@string/indent_1</item>\n        <item>@string/indent_2</item>\n        <item>@string/indent_3</item>\n        <item>@string/indent_4</item>\n    </string-array>\n\n    <string-array name=\"book_group_array\">\n        <item>@string/all_book</item>\n        <item>@string/pursue_more_book</item>\n        <item>@string/fattening_book</item>\n        <item>@string/finish_book</item>\n        <item>@string/local_book</item>\n    </string-array>\n\n    <string-array name=\"text_suffix\">\n        <item>.txt</item>\n        <item>.json</item>\n        <item>.xml</item>\n    </string-array>\n\n    <string-array name=\"convert_s\">\n        <item>@string/jf_convert_o</item>\n        <item>@string/jf_convert_j</item>\n        <item>@string/jf_convert_f</item>\n    </string-array>\n\n    <string-array name=\"NavBarColors\">\n        <item>自动</item>\n        <item>黑色</item>\n        <item>白色</item>\n        <item>跟随背景</item>\n    </string-array>\n\n    <string-array name=\"screen_time_out\">\n        <item>默认</item>\n        <item>1分钟</item>\n        <item>2分钟</item>\n        <item>3分钟</item>\n        <item>常亮</item>\n    </string-array>\n\n    <integer-array name=\"screen_time_out_value\">\n        <item>0</item>\n        <item>60</item>\n        <item>120</item>\n        <item>180</item>\n        <item>-1</item>\n    </integer-array>\n\n    <string-array name=\"screen_direction_list_title\">\n        <item>@string/screen_unspecified</item>\n        <item>@string/screen_portrait</item>\n        <item>@string/screen_landscape</item>\n        <item>@string/screen_sensor</item>\n    </string-array>\n\n    <string-array name=\"bookshelf_px_title\">\n        <item>@string/bookshelf_px_0</item>\n        <item>@string/bookshelf_px_1</item>\n        <item>@string/bookshelf_px_2</item>\n    </string-array>\n\n    <string-array name=\"bookshelf_px_value\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"select_folder\">\n        <item>@string/sys_folder_picker</item>\n        <item>@string/app_folder_picker</item>\n        <item>@string/default_path</item>\n    </string-array>\n\n    <declare-styleable name=\"Battery\">\n        <attr name=\"batteryOrientation\">\n            <enum name=\"horizontal\" value=\"0\"/>\n            <enum name=\"vertical\" value=\"1\"/>\n        </attr>\n        <attr name=\"batteryColor\" format=\"color\"/>\n        <attr name=\"batteryPower\" format=\"integer\"/>\n    </declare-styleable>\n\n    <string-array name=\"icon_names\">\n        <item>icon1</item>\n        <item>icon2</item>\n    </string-array>\n\n    <string-array name=\"icons\">\n        <item>@string/icon_main</item>\n        <item>@string/icon_book</item>\n    </string-array>\n    \n</resources>"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"RefreshProgressBar\">\n        <attr name=\"max_progress\" format=\"integer\"/>\n        <attr name=\"dur_progress\" format=\"integer\"/>\n        <attr name=\"second_dur_progress\" format=\"dimension\"/>\n        <attr name=\"second_max_progress\" format=\"dimension\"/>\n        <attr name=\"bg_color\" format=\"color\"/>\n        <attr name=\"second_color\" format=\"color\"/>\n        <attr name=\"font_color\" format=\"color\"/>\n        <attr name=\"speed\" format=\"dimension\"/>\n    </declare-styleable>\n\n    <declare-styleable name=\"SmoothCheckBox\">\n        <attr name=\"duration\" format=\"integer\"/>\n        <attr name=\"stroke_width\" format=\"dimension\"/>\n        <attr name=\"color_tick\" format=\"color\"/>\n        <attr name=\"color_checked\" format=\"color\"/>\n        <attr name=\"color_unchecked\" format=\"color\"/>\n        <attr name=\"color_unchecked_stroke\" format=\"color\"/>\n    </declare-styleable>\n\n    <declare-styleable name=\"NumberPickerPreference\">\n        <attr name=\"MinValue\" format=\"integer\" />\n        <attr name=\"MaxValue\" format=\"integer\" />\n        <attr name=\"android:summary\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"RefreshLayout\">\n        <attr name=\"layout_refresh_empty\" format=\"reference\"/>\n        <attr name=\"layout_refresh_error\" format=\"reference\"/>\n        <attr name=\"layout_refresh_loading\" format=\"reference\"/>\n    </declare-styleable>\n\n    <declare-styleable name=\"FastScroller\">\n        <attr name=\"fadeScrollbar\" format=\"boolean\" />\n        <attr name=\"showBubble\" format=\"boolean\" />\n        <attr name=\"showTrack\" format=\"boolean\" />\n        <attr name=\"trackColor\" format=\"color\" />\n        <attr name=\"handleColor\" format=\"color\" />\n        <attr name=\"bubbleColor\" format=\"color\" />\n        <attr name=\"bubbleTextColor\" format=\"color\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"FilletImageView\">\n        <attr name=\"radius\" format=\"dimension\"/>\n        <attr name=\"left_top_radius\" format=\"dimension\"/>\n        <attr name=\"right_top_radius\" format=\"dimension\"/>\n        <attr name=\"right_bottom_radius\" format=\"dimension\"/>\n        <attr name=\"left_bottom_radius\" format=\"dimension\"/>\n    </declare-styleable>\n\n    <declare-styleable name=\"IconListPreference\">\n        <attr name=\"icons\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"RotateLoading\">\n        <attr name=\"loading_width\" format=\"dimension\" />\n        <attr name=\"loading_color\" format=\"color\" />\n        <attr name=\"shadow_position\" format=\"integer\" />\n        <attr name=\"loading_speed\" format=\"integer\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"VerticalSeekBar\">\n        <attr name=\"seekBarRotation\">\n            <!-- Clock wise - 90 deg; top = min, bottom = max -->\n            <enum name=\"CW90\" value=\"90\" />\n            <!-- Clock wise - 270 deg; bottom = min, top = max -->\n            <enum name=\"CW270\" value=\"270\" />\n        </attr>\n    </declare-styleable>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/book_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"book_launcher_background\">#F9F9F9</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <color name=\"error\">#eb4333</color>\n    <color name=\"success\">#439b53</color>\n\n    <color name=\"night_mask\">#00000000</color>\n\n    <color name=\"background\">@color/md_grey_100</color>\n    <color name=\"background_card\">#dedede</color>\n    <color name=\"background_menu\">#fcfcfc</color>\n    <color name=\"transparent\">#00000000</color>\n\n    <color name=\"transparent30\">#30000000</color>\n\n    <color name=\"colorPrimary\">@color/md_grey_100</color>\n    <color name=\"colorPrimaryDark\">@color/md_grey_200</color>\n    <color name=\"colorAccent\">@color/md_grey_900</color>\n\n    <color name=\"highlight\">#d3321b</color>\n\n    <color name=\"btn_bg_press\">#80C0C0C0</color>\n    <color name=\"btn_bg_press_2\">#80858585</color>\n    <color name=\"btn_bg_press_tp\">#88000000</color>\n\n    <color name=\"tv_btn_normal_black\">#737373</color>\n    <color name=\"tv_btn_press_black\">#adadad</color>\n\n    <color name=\"tv_text_default\">#de000000</color>\n    <color name=\"tv_text_secondary\">#b2000000</color>\n    <color name=\"tv_text_summary\">#8a000000</color>\n    <color name=\"tv_text_book_detail\">#dfdfdf</color>\n    <color name=\"menu_color_default\">#383838</color>\n\n\n    <color name=\"tv_text_button_nor\">#efefef</color>\n\n    <color name=\"light_translucent\">#23000000</color>\n    <color name=\"common_gray\">#EEEEEE</color>\n    <color name=\"darker_gray\">#aaaaaaaa</color>\n\n    <!-- 分格线背景色 -->\n    <color name=\"bg_divider_line\">#FFD4D4D4</color>\n    <!--statusBarBackground-->\n    <color name=\"status_bar_bag\">#64000000</color>\n    <color name=\"navigation_bar_bag\">#f4f4f4</color>\n\n    <!--base-color-->\n    <color name=\"translucent\">#99343434</color>\n    <color name=\"black\">#000000</color>\n    <color name=\"white\">#ffffff</color>\n\n    <!--\"control\" means checkbox / radio button-->\n\n    <!--LIGHT-->\n    <!--12%-->\n    <color name=\"ate_switch_track_disabled_light\">#1F000000</color>\n    <color name=\"ate_button_disabled_light\">#1F000000</color>\n    <!-- 26% -->\n    <color name=\"ate_control_disabled_light\">#43000000</color>\n    <color name=\"ate_switch_track_normal_light\">#43000000</color>\n    <color name=\"ate_button_text_disabled_light\">#43000000</color>\n    <!-- 38% -->\n    <color name=\"ate_text_disabled_light\">#61000000</color>\n    <!-- 54% -->\n    <color name=\"ate_secondary_text_light\">#8A000000</color>\n    <color name=\"ate_icon_light\">#8A000000</color>\n    <color name=\"ate_control_normal_light\">#8A000000</color>\n    <!-- 87% -->\n    <color name=\"ate_primary_text_light\">#DE000000</color>\n    <!--Grey 50, #FAFAFA, Opacity  100%-->\n    <color name=\"ate_switch_thumb_normal_light\">#FFFAFAFA</color>\n    <!--Grey 400, #BDBDBD, Opacity  100%-->\n    <color name=\"ate_switch_thumb_disabled_light\">#FFBDBDBD</color>\n    <!---->\n    <color name=\"ate_navigation_drawer_selected_light\">#E8E8E8</color>\n\n    <!--DARK-->\n    <!--10%-->\n    <color name=\"ate_switch_track_disabled_dark\">#1AFFFFFF</color>\n    <!--12%-->\n    <color name=\"ate_button_disabled_dark\">#1F000000</color>\n    <!-- 30% -->\n    <color name=\"ate_control_disabled_dark\">#4DFFFFFF</color>\n    <color name=\"ate_switch_track_normal_dark\">#4DFFFFFF</color>\n    <color name=\"ate_button_text_disabled_dark\">#4DFFFFFF</color>\n    <color name=\"ate_text_disabled_dark\">#4DFFFFFF</color>\n    <!-- 70% -->\n    <color name=\"ate_secondary_text_dark\">#B3FFFFFF</color>\n    <color name=\"ate_icon_dark\">#B3FFFFFF</color>\n    <color name=\"ate_control_normal_dark\">#B3FFFFFF</color>\n    <!-- 100% -->\n    <color name=\"ate_primary_text_dark\">#FFFFFFFF</color>\n    <!--Grey 400, #BDBDBD, Opacity  100%-->\n    <color name=\"ate_switch_thumb_normal_dark\">#FFBDBDBD</color>\n    <!--Grey 800, #424242, Opacity 100%-->\n    <color name=\"ate_switch_thumb_disabled_dark\">#FF424242</color>\n    <!---->\n    <color name=\"ate_navigation_drawer_selected_dark\">#202020</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/colors_material_design.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <!-- Google's Material Design colors from\n    https://www.google.com/design/spec/style/color.html#color-color-palette -->\n\n    <!-- Items Light -->\n    <color name=\"md_light_background\">@color/md_grey_50</color>\n    <!-- 12% -->\n    <color name=\"md_light_dividers\">#1F000000</color>\n    <!-- 38% -->\n    <color name=\"md_light_disabled\">#61000000</color>\n    <!-- 54% -->\n    <color name=\"md_light_secondary\">#8A000000</color>\n    <color name=\"md_light_primary_icon\">#8A000000</color>\n    <!-- 87% -->\n    <color name=\"md_light_primary_text\">#DE000000</color>\n    <!-- other specifications -->\n    <color name=\"md_light_statusbar\">@color/md_grey_300</color>\n    <color name=\"md_light_appbar\">@color/md_grey_100</color>\n    <color name=\"md_light_cards\">@color/md_white_1000</color>\n    <color name=\"md_light_dialogs\">@color/md_white_1000</color>\n\n    <!-- Items Dark -->\n    <color name=\"md_dark_background\">@color/md_grey_850</color>\n    <!-- 12% -->\n    <color name=\"md_dark_dividers\">#1FFFFFFF</color>\n    <!-- 30% -->\n    <color name=\"md_dark_disabled\">#4DFFFFFF</color>\n    <!-- 70% -->\n    <color name=\"md_dark_secondary\">#B3FFFFFF</color>\n    <color name=\"md_dark_primary_icon\">#B3FFFFFF</color>\n    <!-- 100% -->\n    <color name=\"md_dark_primary_text\">#FFFFFFFF</color>\n    <!-- other specifications -->\n    <color name=\"md_dark_statusbar\">@color/md_black_1000</color>\n    <color name=\"md_dark_appbar\">@color/md_grey_900</color>\n    <color name=\"md_dark_cards\">@color/md_grey_800</color>\n    <color name=\"md_dark_dialogs\">@color/md_grey_800</color>\n\n    <!-- Red -->\n    <color name=\"md_red_50\">#FFEBEE</color>\n    <color name=\"md_red_100\">#FFCDD2</color>\n    <color name=\"md_red_200\">#EF9A9A</color>\n    <color name=\"md_red_300\">#E57373</color>\n    <color name=\"md_red_400\">#EF5350</color>\n    <color name=\"md_red_500\">#F44336</color>\n    <color name=\"md_red_600\">#E53935</color>\n    <color name=\"md_red_700\">#D32F2F</color>\n    <color name=\"md_red_800\">#C62828</color>\n    <color name=\"md_red_900\">#B71C1C</color>\n    <color name=\"md_red_A100\">#FF8A80</color>\n    <color name=\"md_red_A200\">#FF5252</color>\n    <color name=\"md_red_A400\">#FF1744</color>\n    <color name=\"md_red_A700\">#D50000</color>\n\n    <!-- Pink -->\n    <color name=\"md_pink_50\">#FCE4EC</color>\n    <color name=\"md_pink_100\">#F8BBD0</color>\n    <color name=\"md_pink_200\">#F48FB1</color>\n    <color name=\"md_pink_300\">#F06292</color>\n    <color name=\"md_pink_400\">#EC407A</color>\n    <color name=\"md_pink_500\">#E91E63</color>\n    <color name=\"md_pink_600\">#D81B60</color>\n    <color name=\"md_pink_700\">#C2185B</color>\n    <color name=\"md_pink_800\">#AD1457</color>\n    <color name=\"md_pink_900\">#880E4F</color>\n    <color name=\"md_pink_A100\">#FF80AB</color>\n    <color name=\"md_pink_A200\">#FF4081</color>\n    <color name=\"md_pink_A400\">#F50057</color>\n    <color name=\"md_pink_A700\">#C51162</color>\n\n    <!-- Purple -->\n    <color name=\"md_purple_50\">#F3E5F5</color>\n    <color name=\"md_purple_100\">#E1BEE7</color>\n    <color name=\"md_purple_200\">#CE93D8</color>\n    <color name=\"md_purple_300\">#BA68C8</color>\n    <color name=\"md_purple_400\">#AB47BC</color>\n    <color name=\"md_purple_500\">#9C27B0</color>\n    <color name=\"md_purple_600\">#8E24AA</color>\n    <color name=\"md_purple_700\">#7B1FA2</color>\n    <color name=\"md_purple_800\">#6A1B9A</color>\n    <color name=\"md_purple_900\">#4A148C</color>\n    <color name=\"md_purple_A100\">#EA80FC</color>\n    <color name=\"md_purple_A200\">#E040FB</color>\n    <color name=\"md_purple_A400\">#D500F9</color>\n    <color name=\"md_purple_A700\">#AA00FF</color>\n\n    <!-- Deep Purple -->\n    <color name=\"md_deep_purple_50\">#EDE7F6</color>\n    <color name=\"md_deep_purple_100\">#D1C4E9</color>\n    <color name=\"md_deep_purple_200\">#B39DDB</color>\n    <color name=\"md_deep_purple_300\">#9575CD</color>\n    <color name=\"md_deep_purple_400\">#7E57C2</color>\n    <color name=\"md_deep_purple_500\">#673AB7</color>\n    <color name=\"md_deep_purple_600\">#5E35B1</color>\n    <color name=\"md_deep_purple_700\">#512DA8</color>\n    <color name=\"md_deep_purple_800\">#4527A0</color>\n    <color name=\"md_deep_purple_900\">#311B92</color>\n    <color name=\"md_deep_purple_A100\">#B388FF</color>\n    <color name=\"md_deep_purple_A200\">#7C4DFF</color>\n    <color name=\"md_deep_purple_A400\">#651FFF</color>\n    <color name=\"md_deep_purple_A700\">#6200EA</color>\n\n    <!-- Indigo -->\n    <color name=\"md_indigo_50\">#E8EAF6</color>\n    <color name=\"md_indigo_100\">#C5CAE9</color>\n    <color name=\"md_indigo_200\">#9FA8DA</color>\n    <color name=\"md_indigo_300\">#7986CB</color>\n    <color name=\"md_indigo_400\">#5C6BC0</color>\n    <color name=\"md_indigo_500\">#3F51B5</color>\n    <color name=\"md_indigo_600\">#3949AB</color>\n    <color name=\"md_indigo_700\">#303F9F</color>\n    <color name=\"md_indigo_800\">#283593</color>\n    <color name=\"md_indigo_900\">#1A237E</color>\n    <color name=\"md_indigo_A100\">#8C9EFF</color>\n    <color name=\"md_indigo_A200\">#536DFE</color>\n    <color name=\"md_indigo_A400\">#3D5AFE</color>\n    <color name=\"md_indigo_A700\">#304FFE</color>\n\n    <!-- Blue -->\n    <color name=\"md_blue_50\">#E3F2FD</color>\n    <color name=\"md_blue_100\">#BBDEFB</color>\n    <color name=\"md_blue_200\">#90CAF9</color>\n    <color name=\"md_blue_300\">#64B5F6</color>\n    <color name=\"md_blue_400\">#42A5F5</color>\n    <color name=\"md_blue_500\">#2196F3</color>\n    <color name=\"md_blue_600\">#1E88E5</color>\n    <color name=\"md_blue_700\">#1976D2</color>\n    <color name=\"md_blue_800\">#1565C0</color>\n    <color name=\"md_blue_900\">#0D47A1</color>\n    <color name=\"md_blue_A100\">#82B1FF</color>\n    <color name=\"md_blue_A200\">#448AFF</color>\n    <color name=\"md_blue_A400\">#2979FF</color>\n    <color name=\"md_blue_A700\">#2962FF</color>\n\n    <!-- Light Blue -->\n    <color name=\"md_light_blue_50\">#E1F5FE</color>\n    <color name=\"md_light_blue_100\">#B3E5FC</color>\n    <color name=\"md_light_blue_200\">#81D4FA</color>\n    <color name=\"md_light_blue_300\">#4FC3F7</color>\n    <color name=\"md_light_blue_400\">#29B6F6</color>\n    <color name=\"md_light_blue_500\">#03A9F4</color>\n    <color name=\"md_light_blue_600\">#039BE5</color>\n    <color name=\"md_light_blue_700\">#0288D1</color>\n    <color name=\"md_light_blue_800\">#0277BD</color>\n    <color name=\"md_light_blue_900\">#01579B</color>\n    <color name=\"md_light_blue_A100\">#80D8FF</color>\n    <color name=\"md_light_blue_A200\">#40C4FF</color>\n    <color name=\"md_light_blue_A400\">#00B0FF</color>\n    <color name=\"md_light_blue_A700\">#0091EA</color>\n\n    <!-- Cyan -->\n    <color name=\"md_cyan_50\">#E0F7FA</color>\n    <color name=\"md_cyan_100\">#B2EBF2</color>\n    <color name=\"md_cyan_200\">#80DEEA</color>\n    <color name=\"md_cyan_300\">#4DD0E1</color>\n    <color name=\"md_cyan_400\">#26C6DA</color>\n    <color name=\"md_cyan_500\">#00BCD4</color>\n    <color name=\"md_cyan_600\">#00ACC1</color>\n    <color name=\"md_cyan_700\">#0097A7</color>\n    <color name=\"md_cyan_800\">#00838F</color>\n    <color name=\"md_cyan_900\">#006064</color>\n    <color name=\"md_cyan_A100\">#84FFFF</color>\n    <color name=\"md_cyan_A200\">#18FFFF</color>\n    <color name=\"md_cyan_A400\">#00E5FF</color>\n    <color name=\"md_cyan_A700\">#00B8D4</color>\n\n    <!-- Teal -->\n    <color name=\"md_teal_50\">#E0F2F1</color>\n    <color name=\"md_teal_100\">#B2DFDB</color>\n    <color name=\"md_teal_200\">#80CBC4</color>\n    <color name=\"md_teal_300\">#4DB6AC</color>\n    <color name=\"md_teal_400\">#26A69A</color>\n    <color name=\"md_teal_500\">#009688</color>\n    <color name=\"md_teal_600\">#00897B</color>\n    <color name=\"md_teal_700\">#00796B</color>\n    <color name=\"md_teal_800\">#00695C</color>\n    <color name=\"md_teal_900\">#004D40</color>\n    <color name=\"md_teal_A100\">#A7FFEB</color>\n    <color name=\"md_teal_A200\">#64FFDA</color>\n    <color name=\"md_teal_A400\">#1DE9B6</color>\n    <color name=\"md_teal_A700\">#00BFA5</color>\n\n    <!-- Green -->\n    <color name=\"md_green_50\">#E8F5E9</color>\n    <color name=\"md_green_100\">#C8E6C9</color>\n    <color name=\"md_green_200\">#A5D6A7</color>\n    <color name=\"md_green_300\">#81C784</color>\n    <color name=\"md_green_400\">#66BB6A</color>\n    <color name=\"md_green_500\">#4CAF50</color>\n    <color name=\"md_green_600\">#43A047</color>\n    <color name=\"md_green_700\">#388E3C</color>\n    <color name=\"md_green_800\">#2E7D32</color>\n    <color name=\"md_green_900\">#1B5E20</color>\n    <color name=\"md_green_A100\">#B9F6CA</color>\n    <color name=\"md_green_A200\">#69F0AE</color>\n    <color name=\"md_green_A400\">#00E676</color>\n    <color name=\"md_green_A700\">#00C853</color>\n\n    <!-- Light Green -->\n    <color name=\"md_light_green_50\">#F1F8E9</color>\n    <color name=\"md_light_green_100\">#DCEDC8</color>\n    <color name=\"md_light_green_200\">#C5E1A5</color>\n    <color name=\"md_light_green_300\">#AED581</color>\n    <color name=\"md_light_green_400\">#9CCC65</color>\n    <color name=\"md_light_green_500\">#8BC34A</color>\n    <color name=\"md_light_green_600\">#7CB342</color>\n    <color name=\"md_light_green_700\">#689F38</color>\n    <color name=\"md_light_green_800\">#558B2F</color>\n    <color name=\"md_light_green_900\">#33691E</color>\n    <color name=\"md_light_green_A100\">#CCFF90</color>\n    <color name=\"md_light_green_A200\">#B2FF59</color>\n    <color name=\"md_light_green_A400\">#76FF03</color>\n    <color name=\"md_light_green_A700\">#64DD17</color>\n\n    <!-- Lime -->\n    <color name=\"md_lime_50\">#F9FBE7</color>\n    <color name=\"md_lime_100\">#F0F4C3</color>\n    <color name=\"md_lime_200\">#E6EE9C</color>\n    <color name=\"md_lime_300\">#DCE775</color>\n    <color name=\"md_lime_400\">#D4E157</color>\n    <color name=\"md_lime_500\">#CDDC39</color>\n    <color name=\"md_lime_600\">#C0CA33</color>\n    <color name=\"md_lime_700\">#AFB42B</color>\n    <color name=\"md_lime_800\">#9E9D24</color>\n    <color name=\"md_lime_900\">#827717</color>\n    <color name=\"md_lime_A100\">#F4FF81</color>\n    <color name=\"md_lime_A200\">#EEFF41</color>\n    <color name=\"md_lime_A400\">#C6FF00</color>\n    <color name=\"md_lime_A700\">#AEEA00</color>\n\n    <!-- Yellow -->\n    <color name=\"md_yellow_50\">#FFFDE7</color>\n    <color name=\"md_yellow_100\">#FFF9C4</color>\n    <color name=\"md_yellow_200\">#FFF59D</color>\n    <color name=\"md_yellow_300\">#FFF176</color>\n    <color name=\"md_yellow_400\">#FFEE58</color>\n    <color name=\"md_yellow_500\">#FFEB3B</color>\n    <color name=\"md_yellow_600\">#FDD835</color>\n    <color name=\"md_yellow_700\">#FBC02D</color>\n    <color name=\"md_yellow_800\">#F9A825</color>\n    <color name=\"md_yellow_900\">#F57F17</color>\n    <color name=\"md_yellow_A100\">#FFFF8D</color>\n    <color name=\"md_yellow_A200\">#FFFF00</color>\n    <color name=\"md_yellow_A400\">#FFEA00</color>\n    <color name=\"md_yellow_A700\">#FFD600</color>\n\n    <!-- Amber -->\n    <color name=\"md_amber_50\">#FFF8E1</color>\n    <color name=\"md_amber_100\">#FFECB3</color>\n    <color name=\"md_amber_200\">#FFE082</color>\n    <color name=\"md_amber_300\">#FFD54F</color>\n    <color name=\"md_amber_400\">#FFCA28</color>\n    <color name=\"md_amber_500\">#FFC107</color>\n    <color name=\"md_amber_600\">#FFB300</color>\n    <color name=\"md_amber_700\">#FFA000</color>\n    <color name=\"md_amber_800\">#FF8F00</color>\n    <color name=\"md_amber_900\">#FF6F00</color>\n    <color name=\"md_amber_A100\">#FFE57F</color>\n    <color name=\"md_amber_A200\">#FFD740</color>\n    <color name=\"md_amber_A400\">#FFC400</color>\n    <color name=\"md_amber_A700\">#FFAB00</color>\n\n    <!-- Orange -->\n    <color name=\"md_orange_50\">#FFF3E0</color>\n    <color name=\"md_orange_100\">#FFE0B2</color>\n    <color name=\"md_orange_200\">#FFCC80</color>\n    <color name=\"md_orange_300\">#FFB74D</color>\n    <color name=\"md_orange_400\">#FFA726</color>\n    <color name=\"md_orange_500\">#FF9800</color>\n    <color name=\"md_orange_600\">#FB8C00</color>\n    <color name=\"md_orange_700\">#F57C00</color>\n    <color name=\"md_orange_800\">#EF6C00</color>\n    <color name=\"md_orange_900\">#E65100</color>\n    <color name=\"md_orange_A100\">#FFD180</color>\n    <color name=\"md_orange_A200\">#FFAB40</color>\n    <color name=\"md_orange_A400\">#FF9100</color>\n    <color name=\"md_orange_A700\">#FF6D00</color>\n\n    <!-- Deep Orange -->\n    <color name=\"md_deep_orange_50\">#FBE9E7</color>\n    <color name=\"md_deep_orange_100\">#FFCCBC</color>\n    <color name=\"md_deep_orange_200\">#FFAB91</color>\n    <color name=\"md_deep_orange_300\">#FF8A65</color>\n    <color name=\"md_deep_orange_400\">#FF7043</color>\n    <color name=\"md_deep_orange_500\">#FF5722</color>\n    <color name=\"md_deep_orange_600\">#F4511E</color>\n    <color name=\"md_deep_orange_700\">#E64A19</color>\n    <color name=\"md_deep_orange_800\">#D84315</color>\n    <color name=\"md_deep_orange_900\">#BF360C</color>\n    <color name=\"md_deep_orange_A100\">#FF9E80</color>\n    <color name=\"md_deep_orange_A200\">#FF6E40</color>\n    <color name=\"md_deep_orange_A400\">#FF3D00</color>\n    <color name=\"md_deep_orange_A700\">#DD2C00</color>\n\n    <!-- Brown -->\n    <color name=\"md_brown_50\">#EFEBE9</color>\n    <color name=\"md_brown_100\">#D7CCC8</color>\n    <color name=\"md_brown_200\">#BCAAA4</color>\n    <color name=\"md_brown_300\">#A1887F</color>\n    <color name=\"md_brown_400\">#8D6E63</color>\n    <color name=\"md_brown_500\">#795548</color>\n    <color name=\"md_brown_600\">#6D4C41</color>\n    <color name=\"md_brown_700\">#5D4037</color>\n    <color name=\"md_brown_800\">#4E342E</color>\n    <color name=\"md_brown_900\">#3E2723</color>\n\n    <!-- Grey -->\n    <color name=\"md_grey_50\">#FAFAFA</color>\n    <color name=\"md_grey_100\">#F5F5F5</color>\n    <color name=\"md_grey_200\">#EEEEEE</color>\n    <color name=\"md_grey_300\">#E0E0E0</color>\n    <color name=\"md_grey_400\">#BDBDBD</color>\n    <color name=\"md_grey_500\">#9E9E9E</color>\n    <color name=\"md_grey_600\">#757575</color>\n    <color name=\"md_grey_700\">#616161</color>\n    <color name=\"md_grey_800\">#424242</color>\n    <color name=\"md_grey_850\">#303030</color>\n    <color name=\"md_grey_900\">#212121</color>\n\n    <!-- Blue Grey -->\n    <color name=\"md_blue_grey_50\">#ECEFF1</color>\n    <color name=\"md_blue_grey_100\">#CFD8DC</color>\n    <color name=\"md_blue_grey_200\">#B0BEC5</color>\n    <color name=\"md_blue_grey_300\">#90A4AE</color>\n    <color name=\"md_blue_grey_400\">#78909C</color>\n    <color name=\"md_blue_grey_500\">#607D8B</color>\n    <color name=\"md_blue_grey_600\">#546E7A</color>\n    <color name=\"md_blue_grey_700\">#455A64</color>\n    <color name=\"md_blue_grey_800\">#37474F</color>\n    <color name=\"md_blue_grey_900\">#263238</color>\n\n    <!-- Black & White -->\n    <color name=\"md_black_1000\">#000000</color>\n    <color name=\"md_white_1000\">#FFFFFF</color>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n\n    <dimen name=\"fast_scroll_min_thumb_height\">24dp</dimen>\n\n    <dimen name=\"line_height\">0.8dp</dimen>\n    <dimen name=\"shadow_height\">10dp</dimen>\n\n    <dimen name=\"desc_icon_size\">18sp</dimen>\n\n    <dimen name=\"toolbar_elevation\">4dp</dimen>\n\n    <dimen name=\"fastscroll_bubble_radius\">44dp</dimen>\n    <dimen name=\"fastscroll_bubble_size\">88dp</dimen>\n    <dimen name=\"fastscroll_bubble_textsize\">48sp</dimen>\n    <dimen name=\"fastscroll_bubble_padding\">16dp</dimen>\n\n    <dimen name=\"fastscroll_handle_height\">40dp</dimen>\n    <dimen name=\"fastscroll_handle_width\">8dp</dimen>\n    <dimen name=\"fastscroll_handle_radius\">0dp</dimen>\n\n    <dimen name=\"fastscroll_track_width\">2dp</dimen>\n\n    <dimen name=\"fastscroll_scrollbar_margin_top\">8dp</dimen>\n    <dimen name=\"fastscroll_scrollbar_margin_bottom\">8dp</dimen>\n    <dimen name=\"fastscroll_scrollbar_padding_start\">8dp</dimen>\n    <dimen name=\"fastscroll_scrollbar_padding_end\">8dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#4f4f4f</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <item name=\"fast_scroller\" type=\"id\" />\n\n    <item name=\"tag1\" type=\"id\" />\n\n    <item name=\"tag2\" type=\"id\" />\n\n    <item name=\"menu_refresh\" type=\"id\" />\n\n    <item name=\"menu_disable_update\" type=\"id\" />\n\n    <item name=\"menu_allow_update\" type=\"id\" />\n\n    <item name=\"menu_edit\" type=\"id\" />\n\n    <item name=\"menu_top\" type=\"id\" />\n\n    <item name=\"menu_del\" type=\"id\" />\n\n    <item name=\"menu_clear\" type=\"id\" />\n\n    <item name=\"menu_disable\" type=\"id\" />\n\n    <item name=\"menu_copy_url\" type=\"id\" />\n\n    <item name=\"menu_share\" type=\"id\" />\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/pref_key_value.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\" tools:ignore=\"MissingTranslation\">\n\n    <string name=\"pk_auto_refresh\">auto_refresh</string>\n    <string name=\"pk_screen_direction\">list_screen_direction</string>\n    <string name=\"pk_full_screen\">full_screen</string>\n    <string name=\"pk_threads_num\">threads_num</string>\n    <string name=\"pk_user_agent\">user_agent</string>\n    <string name=\"pk_bookshelf_px\">bookshelf_px</string>\n    <string name=\"pk_read_type\">read_type</string>\n    <string name=\"pk_find_expand_group\">expandGroupFind</string>\n    <string name=\"pk_default_read\">defaultToRead</string>\n    <string name=\"pk_auto_download\">autoDownload</string>\n    <string name=\"pk_download_path\">downloadPath</string>\n    <string name=\"pk_check_update\">checkUpdate</string>\n    <string name=\"pk_search_result_filter_grade\">search_result_filter_grade</string>\n\n    <string name=\"icon_main\">ic_launcher_round</string>\n    <string name=\"icon_book\">book_launcher_round</string>\n\n    <string name=\"source_rule_url\">https://gedoor.github.io/MyBookshelf/sourcerule.html</string>\n    <string name=\"this_github_url\">https://github.com/gedoor/MyBookshelf</string>\n    <string name=\"disclaimer_url\">https://gedoor.github.io/MyBookshelf/disclaimer.html</string>\n    <string name=\"home_page_url\">https://gedoor.github.io/MyBookshelf/</string>\n    <string name=\"latest_release_url\">https://github.com/gedoor/MyBookshelf/releases/latest</string>\n    <string name=\"latest_release_api\">https://api.github.com/repos/gedoor/MyBookshelf/releases/latest</string>\n\n    <string name=\"vip_title\">🔒%s</string>\n    <string name=\"payed_title\">🔓%s</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\r\n    <string name=\"app_name\">阅读</string>\r\n    <string name=\"bookshelf\">书架</string>\r\n    <string name=\"last_read\">最后阅读</string>\r\n    <string name=\"read_summary\">让阅读成为一种习惯。</string>\r\n    <string name=\"update_log\">更新日志</string>\r\n    <string name=\"receiving_shared_label\">阅读·搜索</string>\r\n    <string name=\"bookshelf_empty\">书架还空着，先去添加吧！</string>\r\n    <string name=\"action_search\">搜索</string>\r\n    <string name=\"action_download\">下载任务</string>\r\n    <string name=\"bookshelf_layout\">书架布局</string>\r\n    <string name=\"book_library\">书城</string>\r\n    <string name=\"book_local\">添加本地</string>\r\n    <string name=\"book_source_manage\">书源管理</string>\r\n    <string name=\"setting\">设置</string>\r\n    <string name=\"theme_setting\">主题设置</string>\r\n    <string name=\"about\">关于</string>\r\n    <string name=\"donate\">捐赠</string>\r\n    <string name=\"exit\">退出</string>\r\n    <string name=\"exit_no_save\">还未保存,是否继续编辑</string>\r\n    <string name=\"read_style\">阅读样式设置</string>\r\n\r\n    <string name=\"navigation_drawer_open\">打开侧边栏</string>\r\n    <string name=\"navigation_drawer_close\">关闭侧边栏</string>\r\n    <string name=\"dialog_title\">提示</string>\r\n    <string name=\"dialog_cancel\">取消</string>\r\n    <string name=\"dialog_confirm\">确定</string>\r\n    <string name=\"dialog_setting\">去设置</string>\r\n    <string name=\"tip_cannot_jump_setting_page\">无法跳转至设置界面</string>\r\n    <string name=\"local\">本地</string>\r\n    <string name=\"search\">搜索</string>\r\n    <string name=\"net_error_10001\">没有网络</string>\r\n    <string name=\"net_error_10002\">网络连接超时</string>\r\n    <string name=\"net_error_10003\">数据解析失败</string>\r\n    <string name=\"origin_format\">来源: %s</string>\r\n    <string name=\"read_dur_progress\">最近: %s</string>\r\n    <string name=\"book_name\">书名</string>\r\n    <string name=\"book_search_last\">最新: %s</string>\r\n    <string name=\"check_add_bookshelf\">是否将《%s》放入书架？</string>\r\n    <string name=\"import_books_count\">共%s个Text文件</string>\r\n    <string name=\"is_loading\">加载中…</string>\r\n    <string name=\"get_data_error\">获取数据失败！</string>\r\n    <string name=\"retry\">重试</string>\r\n    <string name=\"web_service\">web服务</string>\r\n    <string name=\"web_edit_source\">web编辑书源</string>\r\n    <string name=\"http_ip\">http://%s:%d</string>\r\n    <string name=\"web_service_starting_hint_short\">正在启动服务</string>\r\n    <string name=\"web_service_starting_hint_long\">正在启动服务\\n具体信息查看通知栏</string>\r\n    <string name=\"web_service_already_started_hint\">服务已启动</string>\r\n    <string name=\"download_offline\">离线下载</string>\r\n    <string name=\"download_offline_t\">离线下载</string>\r\n    <string name=\"download_offline_s\">下载选择的章节到本地</string>\r\n    <string name=\"change_origin\">换源</string>\r\n    <string name=\"about_description\">\\u3000\\u3000这是一款开源的阅读软件，你可以fork我们的代码自己编译APK。欢迎提交代码帮助改善应用。\\n\\u3000\\u3000公众号[开源阅读]！</string>\r\n    <string name=\"version_name\">Version %s</string>\r\n    <string name=\"pt_auto_refresh\">自动刷新</string>\r\n    <string name=\"ps_auto_refresh\">打开软件时自动更新书籍</string>\r\n    <string name=\"pt_auto_download\">自动下载最新章节</string>\r\n    <string name=\"ps_auto_download\">更新书籍时自动下载最新章节</string>\r\n    <string name=\"backup\">备份</string>\r\n    <string name=\"restore\">恢复</string>\r\n    <string name=\"backup_permission\">备份请给与存储权限</string>\r\n    <string name=\"restore_permission\">恢复请给与存储权限</string>\r\n    <string name=\"ok\">确认</string>\r\n    <string name=\"cancel\">取消</string>\r\n    <string name=\"backup_confirmation\">确认备份吗？</string>\r\n    <string name=\"backup_message\">新备份会替换原有备份。\\n备份文件夹YueDu</string>\r\n    <string name=\"restore_confirmation\">确认恢复吗？</string>\r\n    <string name=\"restore_message\">恢复书架会覆盖现有书架。</string>\r\n    <string name=\"backup_success\">备份成功</string>\r\n    <string name=\"backup_fail\">备份失败</string>\r\n    <string name=\"on_restore\">正在恢复</string>\r\n    <string name=\"restore_success\">恢复成功</string>\r\n    <string name=\"restore_fail\">恢复失败</string>\r\n    <string name=\"screen_direction\">屏幕方向</string>\r\n    <string name=\"screen_sensor\">跟随传感器</string>\r\n    <string name=\"screen_landscape\">横向</string>\r\n    <string name=\"screen_portrait\">竖向</string>\r\n    <string name=\"screen_unspecified\">跟随系统</string>\r\n    <string name=\"disclaimer\">免责声明</string>\r\n    <string name=\"all_chapter_num\">共%d章</string>\r\n    <string name=\"interface_setting\">界面</string>\r\n    <string name=\"brightness\">亮度</string>\r\n    <string name=\"chapter_list\">目录</string>\r\n    <string name=\"next_chapter\">下一章</string>\r\n    <string name=\"previous_chapter\">上一章</string>\r\n    <string name=\"pt_hide_status_bar\">隐藏状态栏</string>\r\n    <string name=\"ps_hide_status_bar\">阅读界面隐藏状态栏</string>\r\n    <string name=\"pt_read_line_adjust\">阅读行数调整</string>\r\n    <string name=\"ps_read_line_adjust\">阅读行数减一行,如阅读界面显示不全可启用</string>\r\n    <string name=\"read_aloud\">朗读</string>\r\n    <string name=\"read_aloud_t\">正在朗读</string>\r\n    <string name=\"read_aloud_s\">点击打开阅读界面</string>\r\n    <string name=\"text_return\">返回</string>\r\n    <string name=\"refresh\">重新加载</string>\r\n    <string name=\"start\">开始</string>\r\n    <string name=\"stop\">停止</string>\r\n    <string name=\"pause\">暂停</string>\r\n    <string name=\"resume\">继续</string>\r\n    <string name=\"set_timer\">定时</string>\r\n    <string name=\"read_aloud_pause\">朗读暂停</string>\r\n    <string name=\"read_aloud_timer\">正在朗读(还剩%d分钟)</string>\r\n    <string name=\"read_aloud_timerlong\">\"正在朗读(还剩%d小时%d分)\"</string>\r\n    <string name=\"read_aloud_timerremaining\">%d分钟</string>\r\n    <string name=\"read_aloud_timerremaininglong\">\"%d小时%d分\"</string>\r\n    <string name=\"read_aloud_timerstop\">\"计时已取消\"</string>\r\n    <string name=\"ps_hide_navigation_bar\">阅读界面隐藏虚拟按键</string>\r\n    <string name=\"pt_hide_navigation_bar\">隐藏导航栏</string>\r\n    <string name=\"re_navigation_bar_color\">导航栏颜色</string>\r\n    <string name=\"git_hub\">GitHub</string>\r\n    <string name=\"scoring\">评分</string>\r\n    <string name=\"send_mail\">发送邮件</string>\r\n    <string name=\"can_not_open\">无法打开</string>\r\n    <string name=\"can_not_share\">分享失败</string>\r\n    <string name=\"no_chapter\">无章节</string>\r\n    <string name=\"add_url\">添加网址</string>\r\n    <string name=\"add_book_url\">添加书籍网址</string>\r\n    <string name=\"background\">背景</string>\r\n    <string name=\"author\">作者</string>\r\n    <string name=\"analyze_error\">站点暂时不支持解析，请反馈</string>\r\n    <string name=\"aloud_stop\">朗读停止</string>\r\n    <string name=\"clear_cache\">清除缓存</string>\r\n    <string name=\"action_save\">保存</string>\r\n    <string name=\"edit_book_source\">编辑书源</string>\r\n    <string name=\"disable_book_source\">禁用书源</string>\r\n    <string name=\"add_book_source\">新建书源</string>\r\n    <string name=\"book_file_selector\">添加书籍</string>\r\n    <string name=\"scan_book_source\">扫描</string>\r\n    <string name=\"copy_source\">拷贝书源</string>\r\n    <string name=\"copy_source_no_find\">拷贝书源无发现</string>\r\n    <string name=\"paste_source\">粘贴书源</string>\r\n    <string name=\"source_rule_s\">书源规则说明</string>\r\n    <string name=\"check_update\">检查更新</string>\r\n    <string name=\"camera_scan\">扫描二维码</string>\r\n    <string name=\"scan_image\">扫描本地图片</string>\r\n    <string name=\"rule_summary\">规则说明</string>\r\n    <string name=\"share\">分享</string>\r\n    <string name=\"share_app\">软件分享</string>\r\n    <string name=\"flow_sys\">跟随系统</string>\r\n    <string name=\"add\">添加</string>\r\n    <string name=\"import_book_source\">导入书源</string>\r\n    <string name=\"import_book_source_local\">本地导入</string>\r\n    <string name=\"import_book_source_on_line\">网络导入</string>\r\n    <string name=\"replace_rule_title\">替换净化</string>\r\n    <string name=\"replace_rule_edit\">替换规则编辑</string>\r\n    <string name=\"replace_rule\">替换规则</string>\r\n    <string name=\"replace_to\">替换为</string>\r\n    <string name=\"img_cover\">封面</string>\r\n    <string name=\"book\">书</string>\r\n    <string name=\"volume_open_page\">音量键翻页</string>\r\n    <string name=\"click_open_page\">点击翻页</string>\r\n    <string name=\"click_all_next_page\">点击总是翻下一页</string>\r\n    <string name=\"open_page_anim\">翻页动画</string>\r\n    <string name=\"keep_light\">屏幕超时</string>\r\n    <string name=\"back\">返回</string>\r\n    <string name=\"menu\">菜单</string>\r\n    <string name=\"adjust\">调节</string>\r\n    <string name=\"scroll_bar\">滚动条</string>\r\n    <string name=\"clear_all_content\">清除缓存会删除所有已保存章节,是否确认删除?</string>\r\n    <string name=\"book_source_share_url\">书源共享</string>\r\n    <string name=\"replace_rule_summary\">替换规则名称</string>\r\n    <string name=\"select_all\">全选</string>\r\n    <string name=\"night_theme\">夜间模式</string>\r\n    <string name=\"welcome\">启动页</string>\r\n    <string name=\"download_start\">开始下载</string>\r\n    <string name=\"download_cancel\">取消下载</string>\r\n    <string name=\"no_download\">暂无任务</string>\r\n    <string name=\"import_select_book\">导入选择书籍</string>\r\n    <string name=\"update_search_threads_num\">更新和搜索线程数,如感觉卡顿请减小线程数,量力而行</string>\r\n    <string name=\"change_icon\">切换图标</string>\r\n    <string name=\"remove_from_bookshelf\">删除书籍</string>\r\n    <string name=\"start_read\">开始阅读</string>\r\n    <string name=\"data_loading\">加载数据中…</string>\r\n    <string name=\"load_error_retry\">加载失败,点击重试</string>\r\n    <string name=\"book_intro\">内容简介</string>\r\n    <string name=\"open_from_other\">打开外部书籍</string>\r\n    <string name=\"origin\">来源</string>\r\n    <string name=\"import_replace_rule\">本地导入</string>\r\n    <string name=\"import_replace_rule_on_line\">网络导入</string>\r\n    <string name=\"bookshelf_px\">书架排序</string>\r\n    <string name=\"check_update_interval\">检查更新间隔</string>\r\n    <string name=\"bookshelf_px_0\">按阅读时间排序</string>\r\n    <string name=\"bookshelf_px_1\">按更新时间排序</string>\r\n    <string name=\"bookshelf_px_2\">手动排序</string>\r\n    <string name=\"read_type\">阅读方式</string>\r\n    <string name=\"del_select\">删除所选</string>\r\n    <string name=\"del_msg\">是否确认删除?</string>\r\n    <string name=\"clear_font\">默认字体</string>\r\n    <string name=\"find_on_www\">发现</string>\r\n    <string name=\"find_source_manage\">发现管理</string>\r\n    <string name=\"find_empty\">没有内容,去书源里自定义吧!</string>\r\n    <string name=\"del_all\">删除所有</string>\r\n    <string name=\"searchHistory\">搜索历史</string>\r\n    <string name=\"clear\">清除</string>\r\n    <string name=\"showTitle\">正文显示标题</string>\r\n    <string name=\"refresh_default\">书源同步</string>\r\n    <string name=\"no_last_chapter\">无最新章节信息</string>\r\n    <string name=\"showTimeBattery\">显示时间和电量</string>\r\n    <string name=\"select_text\">长按选择文本</string>\r\n    <string name=\"showLine\">显示分隔线</string>\r\n    <string name=\"darkStatusIcon\">深色状态栏图标</string>\r\n    <string name=\"content\">内容</string>\r\n    <string name=\"copy_text\">拷贝内容</string>\r\n    <string name=\"download_all\">一键缓存</string>\r\n    <string name=\"content_sl\">这是一段测试文字\\n\\u3000\\u3000只是让你看看效果的</string>\r\n    <string name=\"text_bg_style\">文字颜色和背景(长按自定义)</string>\r\n    <string name=\"immersion_status_bar\">沉浸式状态栏</string>\r\n    <string name=\"un_download\">还剩%d章未下载</string>\r\n    <string name=\"long_click_input_color\">长按输入颜色值</string>\r\n    <string name=\"loading\">加载中…</string>\r\n    <string name=\"group_zg\">追更区</string>\r\n    <string name=\"group_yf\">养肥区</string>\r\n    <string name=\"bookmark\">书签</string>\r\n    <string name=\"bookmark_add\">添加书签</string>\r\n    <string name=\"action_del\">删除</string>\r\n    <string name=\"load_over_time\">加载超时</string>\r\n    <string name=\"join_group\">关注:%s</string>\r\n    <string name=\"copy_complete\">已拷贝</string>\r\n    <string name=\"arrange_bookshelf\">整理书架</string>\r\n    <string name=\"clear_bookshelf_s\">这将会删除所有书籍，请谨慎操作。</string>\r\n    <string name=\"search_book_source\">搜索书源</string>\r\n    <string name=\"search_book_source_num\">搜索(共%d个书源)</string>\r\n    <string name=\"chapter_list_size\">目录(%d)</string>\r\n    <string name=\"text_bold\">加粗</string>\r\n    <string name=\"custom_font\">字体</string>\r\n    <string name=\"text\">文字</string>\r\n    <string name=\"home_page\">软件主页</string>\r\n    <string name=\"right\">右</string>\r\n    <string name=\"left\">左</string>\r\n    <string name=\"bottom\">下</string>\r\n    <string name=\"top\">上</string>\r\n    <string name=\"padding\">边距:</string>\r\n    <string name=\"padding_top\">上边距</string>\r\n    <string name=\"padding_bottom\">下边距</string>\r\n    <string name=\"padding_left\">左边距</string>\r\n    <string name=\"padding_right\">右边距</string>\r\n    <string name=\"check_book_source\">校验书源</string>\r\n    <string name=\"check_select_source\">校验所选</string>\r\n    <string name=\"progress_show\">进度 %d/%d</string>\r\n    <string name=\"tts_fix\">请安装并选择中文TTS!</string>\r\n    <string name=\"tts_init_failed\">TTS初始化失败!</string>\r\n    <string name=\"jf_convert\">简繁转换</string>\r\n    <string name=\"jf_convert_o\">关闭</string>\r\n    <string name=\"jf_convert_f\">简转繁</string>\r\n    <string name=\"jf_convert_j\">繁转简</string>\r\n    <string name=\"page_mode\">翻页模式</string>\r\n    <string name=\"nb_file_sub_count\">%1$d 项</string>\r\n    <string name=\"nb_file_path\">存储卡</string>\r\n    <string name=\"nb_file_add_shelf\">加入书架</string>\r\n    <string name=\"nb_file_add_shelves\">加入书架(%1$d)</string>\r\n    <string name=\"nb_file_add_succeed\">成功添加%1$d本书</string>\r\n    <string name=\"fonts_folder\">请将字体文件放到SD根目录Fonts文件夹下重新选择</string>\r\n    <string name=\"default_font\">默认字体</string>\r\n    <string name=\"select_font\">选择字体</string>\r\n    <string name=\"text_size\">字号</string>\r\n    <string name=\"line_size\">行距</string>\r\n    <string name=\"paragraph_size\">段距</string>\r\n    <string name=\"edit\">编辑</string>\r\n    <string name=\"delete\">删除</string>\r\n    <string name=\"to_top\">置顶</string>\r\n    <string name=\"auto_expand_find\">自动展开发现</string>\r\n    <string name=\"default_expand_first\">默认展开第一组发现</string>\r\n    <string name=\"threads_num\">当前线程数 %s</string>\r\n    <string name=\"read_aloud_speed\">朗读语速</string>\r\n    <string name=\"auto_next_page\">自动翻页</string>\r\n    <string name=\"auto_next_page_stop\">停止自动翻页</string>\r\n    <string name=\"auto_next_page_speed\">自动翻页时每分钟阅读字数(CPM)</string>\r\n    <string name=\"book_info\">书籍信息</string>\r\n    <string name=\"ps_default_read\">默认打开书架</string>\r\n    <string name=\"pt_default_read\">自动跳转最近阅读</string>\r\n    <string name=\"use_to\">替换范围，选填书名或者源名</string>\r\n    <string name=\"menu_action_group\">分组</string>\r\n    <string name=\"backup_path\">备份路径</string>\r\n    <string name=\"download_path\">内容缓存路径</string>\r\n    <string name=\"sys_file_picker\">系统文件选择器</string>\r\n    <string name=\"new_version\">新版本</string>\r\n    <string name=\"download_update\">下载更新</string>\r\n    <string name=\"aloud_volume_page\">朗读时音量键翻页</string>\r\n    <string name=\"tip_margin_change\">Tip边距跟随边距调整</string>\r\n    <string name=\"disable_update\">禁止更新</string>\r\n    <string name=\"allow_update\">允许更新</string>\r\n    <string name=\"revert_selection\">反转选择</string>\r\n    <string name=\"search_book_key\">搜索书名、作者</string>\r\n    <string name=\"debug_hint\">书名、作者、URL</string>\r\n    <string name=\"faq\">常见问题</string>\r\n    <string name=\"pt_show_all_find\">显示所有发现</string>\r\n    <string name=\"ps_show_all_find\">关闭则只显示勾选源的发现</string>\r\n    <string name=\"update_chapter\">更新目录</string>\r\n    <string name=\"txt_chapter_regex\">txt目录正则</string>\r\n    <string name=\"set_charset\">设置编码</string>\r\n    <string name=\"swap_sort\">倒序-顺序</string>\r\n    <string name=\"sort\">排序</string>\r\n    <string name=\"sort_auto\">智能排序</string>\r\n    <string name=\"sort_manual\">手动排序</string>\r\n    <string name=\"sort_pin_yin\">拼音排序</string>\r\n    <string name=\"go_to_top\">滚动到顶部</string>\r\n    <string name=\"go_to_bottom\">滚动到底部</string>\r\n    <string name=\"read_y\">已读: %s</string>\r\n    <string name=\"pursue_more\">追更</string>\r\n    <string name=\"fattening\">养肥</string>\r\n    <string name=\"finish\">完结</string>\r\n    <string name=\"all_book\">所有书籍</string>\r\n    <string name=\"pursue_more_book\">追更书籍</string>\r\n    <string name=\"fattening_book\">养肥书籍</string>\r\n    <string name=\"finish_book\">完结书籍</string>\r\n    <string name=\"local_book\">本地书籍</string>\r\n    <string name=\"status_bar_immersion\">状态栏颜色透明</string>\r\n    <string name=\"navigation_bar_color_change\">导航栏变色</string>\r\n    <string name=\"navigation_bar_color_change_s\">导航栏根据夜间模式变化</string>\r\n    <string name=\"add_to_shelf\">放入书架</string>\r\n    <string name=\"continue_read\">继续阅读</string>\r\n    <string name=\"cover_path\">封面地址</string>\r\n    <string name=\"page_mode_COVER\">覆盖</string>\r\n    <string name=\"page_mode_SIMULATION\">仿真</string>\r\n    <string name=\"page_mode_SLIDE\">滑动</string>\r\n    <string name=\"page_mode_SCROLL\">滚动</string>\r\n    <string name=\"page_mode_NONE\">无动画</string>\r\n    <string name=\"donate_s\">此书源使用了高级功能，复制支付宝红包搜索码领取红包或关注微信公众号[开源阅读]开启。</string>\r\n    <string name=\"up_change_source_last_chapter_t\">后台更新换源最新章节</string>\r\n    <string name=\"up_change_source_last_chapter_s\">开启则会在软件打开1分钟后开始更新</string>\r\n    <string name=\"behavior_main_t\">书架ToolBar自动隐藏</string>\r\n    <string name=\"behavior_main_s\">滚动书架时ToolBar自动隐藏与显示</string>\r\n    <string name=\"login\">登录</string>\r\n    <string name=\"login_source\">登录%s</string>\r\n    <string name=\"success\">成功</string>\r\n    <string name=\"source_no_login\">当前源没有配置登陆地址</string>\r\n    <string name=\"book_source_name\">书源名称(bookSourceName)</string>\r\n    <string name=\"book_source_url\">书源URL(bookSourceUrl)</string>\r\n    <string name=\"book_source_group\">书源分组(bookSourceGroup)</string>\r\n    <string name=\"book_source_login_url\">登录URL(loginUrl)</string>\r\n    <string name=\"rule_book_author\">作者规则(ruleBookAuthor)</string>\r\n    <string name=\"rule_book_content\">正文规则(ruleBookContent)</string>\r\n    <string name=\"rule_book_content_replace\">正文替换规则(ruleBookContentReplace)</string>\r\n    <string name=\"rule_book_name\">书名规则(ruleBookName)</string>\r\n    <string name=\"rule_chapter_list\">目录列表规则(ruleChapterList)</string>\r\n    <string name=\"rule_chapter_name\">章节名称规则(ruleChapterName)</string>\r\n    <string name=\"rule_chapter_list_url\">目录URL规则(ruleChapterUrl)</string>\r\n    <string name=\"rule_chapter_list_url_next\">目录下一页规则(ruleChapterUrlNext)</string>\r\n    <string name=\"rule_content_url\">章节URL规则(ruleContentUrl)</string>\r\n    <string name=\"rule_cover_url\">封面规则(ruleCoverUrl)</string>\r\n    <string name=\"rule_introduce\">简介规则(ruleIntroduce)</string>\r\n    <string name=\"rule_search_author\">搜索作者规则(ruleSearchAuthor)</string>\r\n    <string name=\"rule_find_author\">发现作者规则(ruleFindAuthor)</string>\r\n    <string name=\"rule_search_cover_url\">搜索封面规则(ruleSearchCoverUrl)</string>\r\n    <string name=\"rule_find_cover_url\">发现封面规则(ruleFindCoverUrl)</string>\r\n    <string name=\"rule_search_kind\">搜索分类规则(ruleSearchKind)</string>\r\n    <string name=\"rule_find_kind\">发现分类规则(ruleFindKind)</string>\r\n    <string name=\"rule_search_last_chapter\">搜索最新章节规则(ruleSearchLastChapter)</string>\r\n    <string name=\"rule_find_last_chapter\">发现最新章节规则(ruleFindLastChapter)</string>\r\n    <string name=\"rule_search_list\">搜索列表规则(ruleSearchList)</string>\r\n    <string name=\"rule_find_list\">发现列表规则(ruleFindList)</string>\r\n    <string name=\"rule_search_name\">搜索书名规则(ruleSearchName)</string>\r\n    <string name=\"rule_find_name\">发现书名规则(ruleFindName)</string>\r\n    <string name=\"rule_search_note_url\">搜索书籍URL规则(ruleSearchNoteUrl)</string>\r\n    <string name=\"rule_find_note_url\">发现书籍URL规则(ruleFindNoteUrl)</string>\r\n    <string name=\"rule_search_introduce\">搜索简介规则(ruleSearchIntroduce)</string>\r\n    <string name=\"rule_find_introduce\">发现简介规则(ruleFindIntroduce)</string>\r\n    <string name=\"rule_search_url\">搜索地址(ruleSearchUrl)</string>\r\n    <string name=\"rule_find_url\">发现规则(ruleFindUrl)</string>\r\n    <string name=\"rule_content_url_next\">正文下一页URL规则(ruleContentUrlNext)</string>\r\n    <string name=\"book_url_pattern\">书籍详情URL正则(ruleBookUrlPattern)</string>\r\n    <string name=\"rule_book_info_init\">书籍详情预处理规则(ruleBookInfoInit)</string>\r\n    <string name=\"rule_book_kind\">分类规则(ruleBookKind)</string>\r\n    <string name=\"rule_book_last_chapter\">最新章节规则(ruleBookLastChapter)</string>\r\n    <string name=\"source_user_agent\">HttpUserAgent</string>\r\n    <string name=\"debug_source\">调试书源</string>\r\n    <string name=\"import_by_qr_code\">二维码导入</string>\r\n    <string name=\"scan_qr_code\">扫描二维码</string>\r\n    <string name=\"click_on_selected_show_menu\">选中时点击可弹出菜单</string>\r\n    <string name=\"theme\">主题</string>\r\n    <string name=\"default_theme\">默认主题</string>\r\n    <string name=\"restore_default_theme\">恢复主题为默认配色</string>\r\n    <string name=\"join_qq_group\">加入QQ群</string>\r\n    <string name=\"read_file_error\">文件读取失败</string>\r\n    <string name=\"bg_image_per\">获取背景图片需存储权限</string>\r\n    <string name=\"input_book_source_url\">输入书源网址</string>\r\n    <string name=\"del_file\">删除文件</string>\r\n    <string name=\"del_file_success\">删除文件成功</string>\r\n    <string name=\"sure_del_file\">确定删除文件吗?</string>\r\n    <string name=\"files_tree\">手机目录</string>\r\n    <string name=\"intelligent_import\">智能导入</string>\r\n    <string name=\"find\">发现</string>\r\n    <string name=\"switch_display_style\">切换显示样式</string>\r\n    <string name=\"import_per\">导入本地书籍需存储权限</string>\r\n    <string name=\"click_to_day\">点击可切换到白天模式</string>\r\n    <string name=\"click_to_night\">点击可切换到夜间模式</string>\r\n    <string name=\"get_storage_per\">本软件需要存储权限来存储备份书籍信息</string>\r\n    <string name=\"double_click_exit\">再按一次退出程序</string>\r\n    <string name=\"import_book_per\">导入本地书籍需存储权限</string>\r\n    <string name=\"network_connection_unavailable\">网络连接不可用</string>\r\n    <string name=\"yes\">是</string>\r\n    <string name=\"no\">否</string>\r\n    <string name=\"sure_del_all_book\">是否删除全部书籍？</string>\r\n    <string name=\"sure_del_download_book\">是否同时删除已下载的书籍目录？</string>\r\n    <string name=\"qr_per\">扫描二维码需相机权限</string>\r\n    <string name=\"aloud_can_not_auto_page\">朗读正在运行,不能自动翻页</string>\r\n    <string name=\"input_charset\">输入编码</string>\r\n    <string name=\"text_chapter_list_rule\">TXT目录规则</string>\r\n    <string name=\"open_local_book_per\">打开外部书籍需获取存储权限</string>\r\n    <string name=\"get_book_info_error\">书籍信息获取失败</string>\r\n    <string name=\"get_content_error\">内容获取失败</string>\r\n    <string name=\"get_chapter_list_error\">目录获取失败</string>\r\n    <string name=\"get_web_content_error\">访问网站失败:%s</string>\r\n    <string name=\"no_book_name\">未获取到书名</string>\r\n    <string name=\"input_replace_url\">输入替换规则网址</string>\r\n    <string name=\"get_book_list_success\">搜索列表获取成功%d</string>\r\n    <string name=\"non_null_source_name_url\">书源名称和URL不能为空</string>\r\n    <string name=\"gallery\">图库</string>\r\n    <string name=\"get_ali_pay_hb\">领支付宝红包</string>\r\n    <string name=\"non_update_url\">没有获取到更新地址</string>\r\n    <string name=\"check_host_cookie\">正在打开首页，成功自动返回主界面</string>\r\n    <string name=\"click_check_after_success\">登录成功后请点击右上角图标进行首页访问测试</string>\r\n    <string name=\"chapter\">章</string>\r\n    <string name=\"chapter_s\">章节:</string>\r\n    <string name=\"to\">至</string>\r\n    <string name=\"use_regex\">使用正则表达式</string>\r\n    <string name=\"indent\">缩进</string>\r\n    <string name=\"indent_0\">无缩进</string>\r\n    <string name=\"indent_1\">一字符缩进</string>\r\n    <string name=\"indent_2\">二字符缩进</string>\r\n    <string name=\"indent_3\">三字符缩进</string>\r\n    <string name=\"indent_4\">四字符缩进</string>\r\n    <string name=\"select_folder\">选择文件夹</string>\r\n    <string name=\"select_sd_file\">选择SD卡</string>\r\n    <string name=\"no_find\">没有发现，可以在书源里添加。</string>\r\n    <string name=\"restore_default\">恢复默认</string>\r\n    <string name=\"set_download_per\">自定义缓存路径需要存储权限</string>\r\n    <string name=\"black\">黑色</string>\r\n    <string name=\"content_empty\">文章内容为空</string>\r\n    <string name=\"on_change_source\">正在换源请等待…</string>\r\n    <string name=\"chapter_list_empty\">目录列表为空</string>\r\n    <string name=\"load_error_msg\">加载失败\\n%s</string>\r\n    <string name=\"content_padding\">正文边距</string>\r\n    <string name=\"tip_padding\">Tip边距</string>\r\n    <string name=\"text_letter_spacing\">字距</string>\r\n    <string name=\"default_path\">默认路径</string>\r\n    <string name=\"sys_folder_picker\">系统文件夹选择器</string>\r\n    <string name=\"app_folder_picker\">自带选择器\\n(Android10以上因权限限制可能无法使用)</string>\r\n    <string name=\"a10_permission_toast\">Android10以上因权限限制可能无法读写文件</string>\r\n    <string name=\"e_ink_mode\">E-Ink 模式</string>\r\n    <string name=\"e_ink_mode_detail\">去除动画,优化电纸书使用体验</string>\r\n    <string name=\"web_menu\">Web服务</string>\r\n    <string name=\"web_port_title\">web端口</string>\r\n    <string name=\"web_port_summary\">当前端口 %s</string>\r\n    <string name=\"qr_share\">二维码分享</string>\r\n    <string name=\"str_share\">字符串分享</string>\r\n    <string name=\"wifi_share\">wifi分享</string>\r\n    <string name=\"please_grant_storage_permission\">请给于存储权限</string>\r\n    <string name=\"skip_previous\">上一个</string>\r\n    <string name=\"skip_next\">下一个</string>\r\n    <string name=\"music\">音乐</string>\r\n    <string name=\"audio\">音频</string>\r\n    <string name=\"is_enable\">启用</string>\r\n    <string name=\"all_source\">全部书源</string>\r\n    <string name=\"cannot_empty\">输入不能为空</string>\r\n    <string name=\"clear_find_cache\">清空发现缓存</string>\r\n    <string name=\"edit_find\">编辑发现</string>\r\n    <string name=\"change_icon_summary\">切换软件显示在桌面的图标</string>\r\n    <string name=\"show_enabled\">显示勾选</string>\r\n    <string name=\"to_lh\">扩展到刘海</string>\r\n\r\n    <string name=\"layout_list\">列表视图</string>\r\n    <string name=\"layout_grid3\">网格视图三列</string>\r\n    <string name=\"layout_grid4\">网格视图四列</string>\r\n    <string name=\"cover_change_source\">封面换源</string>\r\n    <string name=\"select_local_image\">选择本地图片</string>\r\n    <string name=\"refresh_cover\">刷新封面</string>\r\n    <string name=\"c_page_key\">自定义翻页按键</string>\r\n\r\n    <string name=\"custom_page_key\">自定义翻页按键</string>\r\n    <string name=\"prev_page_key\">上一页按键</string>\r\n    <string name=\"next_page_key\">下一页按键</string>\r\n\r\n    <string name=\"search_result_filter_grade\">搜索结果过滤等级</string>\r\n    <string name=\"search_result_filter_grade_tip\">调高过滤等级可以避免看到书名/作者无关的搜索结果。\r\n        等级1-9：搜索关键字需要出现到书名/作者中，但是允许有8-0个字（空格不计）没有出现（避免误输入的影响）。默认等级0，不过滤。推荐等级7，允许2个错別字。\r\n        \\n当前过滤等级 %s</string>\r\n    <string name=\"light_novel_paragraph\">自动重分段落</string>\r\n    <string name=\"share_book\">分享书籍</string>\r\n    <string name=\"add_qrcode\">扫二维码</string>\r\n    <string name=\"copy_url\">复制网址</string>\r\n    <string name=\"check_find_source\">标记所选发现源</string>\r\n    <string name=\"replace_ad\">广告话术</string>\r\n    <string name=\"replace_ad_title\">编辑广告话术规则</string>\r\n    <string name=\"replace_add_ad_title\">添加广告话术规则</string>\r\n    <string name=\"replace_ad_intro\">广告话术功能使用了自动优化的算法，不需要使用正则表达式，只需要多次标记不想看的文字，就可以获得较好的广告替换效果。同一个网站的广告话术自动保存在一个规则内，避免了“替换净化”列表里有太多多的规则。</string>\r\n    <string name=\"login_ui\">登录UI(loginUi)</string>\r\n    <string name=\"login_check_js\">登录检查Js(loginCheckJs)</string>\r\n    <string name=\"rule_vip\">VIP标识(ruleVip)</string>\r\n    <string name=\"rule_pay\">购买标识(rulePay)</string>\r\n    <string name=\"chapter_pay\">购买</string>\r\n</resources>\r\n"
  },
  {
    "path": "app/src/main/res/values/strings_me.xml",
    "content": "<resources>\r\n    <string name=\"default_source\">默认书源</string>\r\n    <string name=\"import_by_default\">默认书源导入</string>\r\n    <string name=\"no_default_source\">没有可用的默认书源</string>\r\n    <string name=\"row_0\">单倍</string>\r\n    <string name=\"row_1\">双倍</string>\r\n    <string name=\"row_2\">三倍</string>\r\n    <string name=\"row_def\">默认</string>\r\n    <string name=\"custom_mr\">自定义间距</string>\r\n\r\n    <string name=\"custom_mr_rm\">行间距</string>\r\n    <string name=\"custom_mr_dm\">段间距</string>\r\n    <string name=\"custom_mr_f\">字间距</string>\r\n    <string name=\"custom_mr_z_t\">上边距</string>\r\n    <string name=\"custom_mr_z_b\">下边距</string>\r\n    <string name=\"custom_mr_z_l\">左边距</string>\r\n    <string name=\"custom_mr_z_r\">右边距</string>\r\n\r\n    <string name=\"custom_mr_t_t\">上Tip</string>\r\n    <string name=\"custom_mr_t_b\">下Tip</string>\r\n    <string name=\"custom_mr_t_l\">左Tip</string>\r\n    <string name=\"custom_mr_t_r\">右Tip</string>\r\n\r\n    <declare-styleable name=\"ATEStrokeTextView\">\r\n        <attr name=\"cornerRadius\" format=\"dimension\" />\r\n    </declare-styleable>\r\n\r\n    <dimen name=\"adjust_margin_left\">55dp</dimen>\r\n    <dimen name=\"adjust_margin_right\">45dp</dimen>\r\n    <dimen name=\"adjust_margin_barH\">25dp</dimen>\r\n    <dimen name=\"adjust_margin_titfs\">13sp</dimen>\r\n</resources>"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <!-- 亮色主题 -->\n    <style name=\"CAppTheme\" parent=\"Base.AppTheme\">\n        <item name=\"actionBarStyle\">@style/AppTheme.AppBarOverlay.Light</item>\n    </style>\n\n    <!-- 暗色主题 -->\n    <style name=\"CAppThemeBarDark\" parent=\"Base.AppTheme\">\n        <item name=\"actionBarStyle\">@style/AppTheme.AppBarOverlay.Dark</item>\n    </style>\n\n    <!-- 透明主题 -->\n    <style name=\"CAppTransparentTheme\" parent=\"Base.AppTheme\">\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n    </style>\n\n    <!-- Welcome主题 -->\n    <style name=\"CAppWelcomeTheme\" parent=\"Base.AppTheme\">\n        <item name=\"android:windowNoTitle\">true</item>\n    </style>\n\n    <!-- Base application theme. -->\n    <style name=\"Base.AppTheme\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n        <item name=\"android:windowBackground\">@color/background</item>\n        <item name=\"android:windowTranslucentStatus\">true</item>\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n        <item name=\"actionOverflowMenuStyle\">@style/PopupMenu</item>\n        <item name=\"android:itemTextAppearance\">@style/MenuItemTextStyle</item>\n        <item name=\"android:popupMenuStyle\">@style/PopupMenu</item>\n    </style>\n\n    <style name=\"Activity.Permission\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:windowDisablePreview\">true</item>\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <item name=\"android:windowTranslucentStatus\" tools:targetApi=\"kitkat\">true</item>\n        <item name=\"android:windowAnimationStyle\">@null</item>\n    </style>\n\n    <style name=\"ToolbarTitle\" parent=\"@style/TextAppearance.Widget.AppCompat.Toolbar.Title\">\n        <item name=\"android:textSize\">20sp</item>\n    </style>\n\n    <style name=\"NavigationViewStyle\">\n        <item name=\"android:listPreferredItemHeightSmall\">44dp</item>\n    </style>\n\n    <style name=\"alertDialogTheme\" parent=\"Theme.AppCompat.DayNight.Dialog.Alert\" />\n\n    <style name=\"MenuItemTextStyle\" parent=\"@android:style/TextAppearance.Widget.IconMenu.Item\">\n        <item name=\"android:textColor\">@color/menu_color_default</item>\n        <item name=\"android:textSize\">16sp</item>\n    </style>\n\n    <style name=\"PopupMenu\" parent=\"Widget.AppCompat.PopupMenu\">\n        <item name=\"android:popupBackground\">@color/background_menu</item>\n    </style>\n\n    <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Light\">\n        <item name=\"overlapAnchor\">false</item>\n        <item name=\"colorAccent\">@color/md_grey_900</item>\n    </style>\n\n    <style name=\"NoPaddingToolbar\" parent=\"Base.Widget.AppCompat.Toolbar\">\n        <item name=\"contentInsetStartWithNavigation\">0dp</item>\n    </style>\n\n    <style name=\"Style.Shadow.Top\" parent=\"android:Widget\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">@dimen/shadow_height</item>\n        <item name=\"android:background\">@drawable/bg_shadow_top</item>\n    </style>\n\n    <style name=\"Style.Shadow.Bottom\" parent=\"android:Widget\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">@dimen/shadow_height</item>\n        <item name=\"android:background\">@drawable/bg_shadow_bottom</item>\n    </style>\n\n    <style name=\"AppTheme.AppBarOverlay.Light\" parent=\"ThemeOverlay.AppCompat.Light\">\n        <item name=\"colorAccent\">@color/md_grey_900</item>\n    </style>\n\n    <style name=\"AppTheme.AppBarOverlay.Dark\" parent=\"ThemeOverlay.AppCompat.Dark\">\n        <item name=\"colorAccent\">@color/md_grey_100</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-en/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"login_ui\">登录UI(loginUi)</string>\n    <string name=\"login_check_js\">登录检查Js(loginCheckJs)</string>\n    <string name=\"rule_vip\">VIP标识(ruleVip)</string>\n    <string name=\"rule_pay\">购买标识(rulePay)</string>\n    <string name=\"chapter_pay\">购买</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"background\">@color/md_grey_800</color>\n    <color name=\"background_card\">#353535</color>\n    <color name=\"background_menu\">#282828</color>\n\n    <color name=\"night_mask\">#69000000</color>\n\n    <color name=\"colorPrimary\">@color/md_grey_800</color>\n    <color name=\"colorPrimaryDark\">@color/md_grey_900</color>\n    <color name=\"colorAccent\">@color/md_grey_100</color>\n\n    <color name=\"transparent30\">#30ffffff</color>\n\n    <color name=\"bg_divider_line\">#363636</color>\n    <color name=\"btn_bg_press\">#80696969</color>\n    <color name=\"btn_bg_press_2\">#80686868</color>\n    <color name=\"btn_bg_press_tp\">#88111111</color>\n    <color name=\"darker_gray\">#66666666</color>\n\n    <color name=\"tv_btn_normal_black\">#737373</color>\n    <color name=\"tv_btn_press_black\">#565656</color>\n\n    <color name=\"tv_text_default\">#ffffffff</color>\n    <color name=\"tv_text_secondary\">#e5ffffff</color>\n    <color name=\"tv_text_summary\">#b3ffffff</color>\n    <color name=\"menu_color_default\">#b7b7b7</color>\n\n\n    <color name=\"tv_text_button_nor\">#303030</color>\n\n    <!--statusBarBackground-->\n    <color name=\"navigation_bar_bag\">#222222</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/styles.xml",
    "content": "<resources>\n\n    <style name=\"AppTheme.PopupOverlay\" parent=\"ThemeOverlay.AppCompat.Dark\">\n        <item name=\"overlapAnchor\">false</item>\n        <item name=\"colorAccent\">@color/md_grey_100</item>\n    </style>\n\n    <style name=\"Style.Shadow.Top\" parent=\"android:Widget\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">@dimen/shadow_height</item>\n        <item name=\"android:background\">@drawable/bg_shadow_top_night</item>\n    </style>\n\n    <style name=\"Style.Shadow.Bottom\" parent=\"android:Widget\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">@dimen/shadow_height</item>\n        <item name=\"android:background\">@drawable/bg_shadow_bottom_night</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-v27/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"Base.Theme.AppCompat\" parent=\"@style/Base.V27.Theme.AppCompat\" tools:ignore=\"PrivateResource\" />\n    <style name=\"Base.Theme.AppCompat.Light\" parent=\"@style/Base.V27.Theme.AppCompat.Light\" tools:ignore=\"PrivateResource\" />\n    <style name=\"Base.V27.Theme.AppCompat\" parent=\"@style/Base.V26.Theme.AppCompat\" >\n        <item name=\"android:navigationBarColor\">#ff212121</item>\n        <item name=\"android:windowLightNavigationBar\">false</item>\n        <item name=\"android:navigationBarDividerColor\">#ff545454</item>\n    </style>\n    <style name=\"Base.V27.Theme.AppCompat.Light\" parent=\"@style/Base.V26.Theme.AppCompat.Light\">\n        <item name=\"android:navigationBarColor\">@android:color/white</item>\n        <item name=\"android:windowLightNavigationBar\">true</item>\n        <item name=\"android:navigationBarDividerColor\">#ffdcdcdc</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v28/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"Base.Theme.AppCompat\" parent=\"@style/Base.V28.Theme.AppCompat\" tools:ignore=\"PrivateResource\" />\n    <style name=\"Base.Theme.AppCompat.Light\" parent=\"@style/Base.V28.Theme.AppCompat.Light\" tools:ignore=\"PrivateResource\" />\n    <style name=\"Base.V28.Theme.AppCompat\" parent=\"@style/Base.V26.Theme.AppCompat\" tools:ignore=\"PrivateResource\">\n        <item name=\"android:navigationBarColor\">#ff212121</item>\n        <item name=\"android:windowLightNavigationBar\">false</item>\n        <item name=\"android:navigationBarDividerColor\">#ff545454</item>\n    </style>\n    <style name=\"Base.V28.Theme.AppCompat.Light\" parent=\"@style/Base.V26.Theme.AppCompat.Light\" tools:ignore=\"PrivateResource\">\n        <item name=\"android:navigationBarColor\">@android:color/white</item>\n        <item name=\"android:windowLightNavigationBar\">true</item>\n        <item name=\"android:navigationBarDividerColor\">#ffdcdcdc</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"login_ui\">登录UI(loginUi)</string>\n    <string name=\"login_check_js\">登录检查Js(loginCheckJs)</string>\n    <string name=\"rule_vip\">VIP标识(ruleVip)</string>\n    <string name=\"rule_pay\">购买标识(rulePay)</string>\n    <string name=\"chapter_pay\">购买</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">閱讀</string>\n    <string name=\"bookshelf\">書架</string>\n    <string name=\"last_read\">最後閱讀</string>\n    <string name=\"read_summary\">讓閱讀成為一種習慣。</string>\n    <string name=\"update_log\">更新日誌</string>\n    <string name=\"receiving_shared_label\">閱讀·搜尋</string>\n    <string name=\"bookshelf_empty\">書架還空著，先去添加吧！</string>\n    <string name=\"action_search\">搜尋</string>\n    <string name=\"action_download\">下載任務</string>\n    <string name=\"bookshelf_layout\">書架佈局</string>\n    <string name=\"book_library\">書庫</string>\n    <string name=\"book_local\">添加本機</string>\n    <string name=\"book_source_manage\">書源管理</string>\n    <string name=\"setting\">設定</string>\n    <string name=\"theme_setting\">主題設定</string>\n    <string name=\"about\">關於</string>\n    <string name=\"donate\">捐贈</string>\n    <string name=\"exit\">退出</string>\n    <string name=\"exit_no_save\">還未保存,是否繼續編輯</string>\n    <string name=\"read_style\">閱讀樣式設定</string>\n    <string name=\"navigation_drawer_open\">打開側邊欄</string>\n    <string name=\"navigation_drawer_close\">關閉側邊欄</string>\n    <string name=\"dialog_title\">提示</string>\n    <string name=\"dialog_cancel\">取消</string>\n    <string name=\"dialog_confirm\">確定</string>\n    <string name=\"dialog_setting\">去設定</string>\n    <string name=\"tip_cannot_jump_setting_page\">無法跳轉至設定畫面</string>\n    <string name=\"local\">本機</string>\n    <string name=\"search\">搜尋</string>\n    <string name=\"net_error_10001\">沒有網路</string>\n    <string name=\"net_error_10002\">網路連線逾時</string>\n    <string name=\"net_error_10003\">數據解析失敗</string>\n    <string name=\"origin_format\">來源: %s</string>\n    <string name=\"read_dur_progress\">最近: %s</string>\n    <string name=\"book_name\">書名</string>\n    <string name=\"book_search_last\">最新: %s</string>\n    <string name=\"check_add_bookshelf\">是否將《%s》放入書架？</string>\n    <string name=\"import_books_count\">共%s個Text文件</string>\n    <string name=\"is_loading\">加載中…</string>\n    <string name=\"get_data_error\">獲取數據失敗！</string>\n    <string name=\"retry\">重試</string>\n    <string name=\"web_service\">web服務</string>\n    <string name=\"web_edit_source\">web編輯書源</string>\n    <string name=\"http_ip\">http://%s:%d</string>\n    <string name=\"download_offline\">離線下載</string>\n    <string name=\"download_offline_t\">離線下載</string>\n    <string name=\"download_offline_s\">下載選擇的章節到本地</string>\n    <string name=\"change_origin\">換源</string>\n    <string name=\"about_description\">\\u3000\\u3000這是一款開源的閱讀應用，你可以fork我們的代碼自己編譯APK。</string>\n    <string name=\"version_name\">Version %s</string>\n    <string name=\"pt_auto_refresh\">自動刷新</string>\n    <string name=\"ps_auto_refresh\">打開應用時自動更新書籍</string>\n    <string name=\"pt_auto_download\">自動下載最新章節</string>\n    <string name=\"ps_auto_download\">更新書籍時自動下載最新章節</string>\n    <string name=\"backup\">備份</string>\n    <string name=\"restore\">復原</string>\n    <string name=\"backup_permission\">備份請授予儲存權限</string>\n    <string name=\"restore_permission\">復原請授予儲存權限</string>\n    <string name=\"ok\">確認</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"backup_confirmation\">確認備份嗎？</string>\n    <string name=\"backup_message\">新備份會替換原有備份。 </string>\n    <string name=\"restore_confirmation\">確認復原嗎？</string>\n    <string name=\"restore_message\">復原書架會覆蓋現有書架。</string>\n    <string name=\"backup_success\">備份成功</string>\n    <string name=\"backup_fail\">備份失敗</string>\n    <string name=\"on_restore\">正在復原</string>\n    <string name=\"restore_success\">復原成功</string>\n    <string name=\"restore_fail\">復原失敗</string>\n    <string name=\"screen_direction\">熒幕方向</string>\n    <string name=\"screen_sensor\">跟隨傳感器</string>\n    <string name=\"screen_landscape\">橫向</string>\n    <string name=\"screen_portrait\">直向</string>\n    <string name=\"screen_unspecified\">跟隨系統</string>\n    <string name=\"disclaimer\">免責聲明</string>\n    <string name=\"all_chapter_num\">共%d章</string>\n    <string name=\"interface_setting\">界面</string>\n    <string name=\"brightness\">亮度</string>\n    <string name=\"chapter_list\">目錄</string>\n    <string name=\"next_chapter\">下一章</string>\n    <string name=\"previous_chapter\">上一章</string>\n    <string name=\"pt_hide_status_bar\">隱藏狀態欄</string>\n    <string name=\"ps_hide_status_bar\">閱讀畫面隱藏狀態欄</string>\n    <string name=\"pt_read_line_adjust\">閱讀行數調整</string>\n    <string name=\"ps_read_line_adjust\">閱讀行數減一行,如閱讀畫面顯示不全可啟用</string>\n    <string name=\"read_aloud\">朗讀</string>\n    <string name=\"read_aloud_t\">正在朗讀</string>\n    <string name=\"read_aloud_s\">點擊打開閱讀畫面</string>\n    <string name=\"text_return\">返回</string>\n    <string name=\"refresh\">重新加載</string>\n    <string name=\"start\">開始</string>\n    <string name=\"stop\">停止</string>\n    <string name=\"pause\">暫停</string>\n    <string name=\"resume\">繼續</string>\n    <string name=\"set_timer\">定時</string>\n    <string name=\"read_aloud_pause\">朗讀暫停</string>\n    <string name=\"read_aloud_timer\">正在朗讀(還剩%d分鐘)</string>\n    <string name=\"read_aloud_timerlong\">\"正在朗讀(還剩%d小時%d分)\"</string>\n    <string name=\"read_aloud_timerremaining\">%d分鐘</string>\n    <string name=\"read_aloud_timerremaininglong\">\"%d小時%d分\"</string>\n    <string name=\"read_aloud_timerstop\">\"計時已取消\"</string>\n    <string name=\"ps_hide_navigation_bar\">閱讀畫面隱藏虛擬按鍵</string>\n    <string name=\"pt_hide_navigation_bar\">隱藏導航欄</string>\n    <string name=\"re_navigation_bar_color\">導航欄顏色</string>\n    <string name=\"git_hub\">GitHub上</string>\n    <string name=\"scoring\">評分</string>\n    <string name=\"send_mail\">發送郵件</string>\n    <string name=\"can_not_open\">無法打開</string>\n    <string name=\"can_not_share\">分享失敗</string>\n    <string name=\"no_chapter\">無章節</string>\n    <string name=\"add_url\">添加網址</string>\n    <string name=\"add_book_url\">添加書籍網址</string>\n    <string name=\"background\">背景</string>\n    <string name=\"author\">作者</string>\n    <string name=\"analyze_error\">站點暫時不支持解析，請反饋</string>\n    <string name=\"aloud_stop\">朗讀停止</string>\n    <string name=\"clear_cache\">清除快取</string>\n    <string name=\"action_save\">保存</string>\n    <string name=\"edit_book_source\">編輯書源</string>\n    <string name=\"disable_book_source\">禁用書源</string>\n    <string name=\"add_book_source\">新建書源</string>\n    <string name=\"book_file_selector\">添加書籍</string>\n    <string name=\"scan_book_source\">掃描</string>\n    <string name=\"copy_source\">複製書源</string>\n    <string name=\"copy_source_no_find\">複製書源無發現</string>\n    <string name=\"paste_source\">粘貼書源</string>\n    <string name=\"source_rule_s\">書源規則說明</string>\n    <string name=\"check_update\">檢查更新</string>\n    <string name=\"camera_scan\">掃描二維碼</string>\n    <string name=\"scan_image\">掃描本機相片</string>\n    <string name=\"rule_summary\">規則說明</string>\n    <string name=\"share\">分享</string>\n    <string name=\"share_app\">應用程式分享</string>\n    <string name=\"flow_sys\">跟隨系統</string>\n    <string name=\"add\">添加</string>\n    <string name=\"import_book_source\">匯入書源</string>\n    <string name=\"import_book_source_local\">本地匯入</string>\n    <string name=\"import_book_source_on_line\">網路匯入</string>\n    <string name=\"replace_rule_title\">替換淨化</string>\n    <string name=\"replace_rule_edit\">替換規則編輯</string>\n    <string name=\"replace_rule\">替換規則</string>\n    <string name=\"replace_to\">替換為</string>\n    <string name=\"img_cover\">封面</string>\n    <string name=\"book\">書</string>\n    <string name=\"volume_open_page\">音量鍵翻頁</string>\n    <string name=\"click_open_page\">點擊翻頁</string>\n    <string name=\"click_all_next_page\">點擊總是翻下一頁</string>\n    <string name=\"open_page_anim\">翻頁動畫</string>\n    <string name=\"keep_light\">熒幕逾時</string>\n    <string name=\"back\">返回</string>\n    <string name=\"menu\">選單</string>\n    <string name=\"adjust\">調節</string>\n    <string name=\"scroll_bar\">滾動條</string>\n    <string name=\"clear_all_content\">清除快取會刪除所有已保存章節,是否確認刪除?</string>\n    <string name=\"book_source_share_url\">書源共享</string>\n    <string name=\"replace_rule_summary\">替換規則名稱</string>\n    <string name=\"select_all\">全選</string>\n    <string name=\"night_theme\">夜間模式</string>\n    <string name=\"welcome\">啟動頁</string>\n    <string name=\"download_start\">開始下載</string>\n    <string name=\"download_cancel\">取消下載</string>\n    <string name=\"no_download\">暫無任務</string>\n    <string name=\"import_select_book\">匯入選擇書籍</string>\n    <string name=\"update_search_threads_num\">更新和搜尋線程數,如感覺卡頓請減小線程數,量力而行</string>\n    <string name=\"change_icon\">切換圖示</string>\n    <string name=\"remove_from_bookshelf\">刪除書籍</string>\n    <string name=\"start_read\">開始閱讀</string>\n    <string name=\"data_loading\">加載數據中…</string>\n    <string name=\"load_error_retry\">加載失敗,點擊重試</string>\n    <string name=\"book_intro\">內容簡介</string>\n    <string name=\"open_from_other\">打開外部書籍</string>\n    <string name=\"origin\">來源</string>\n    <string name=\"import_replace_rule\">本機匯入</string>\n    <string name=\"import_replace_rule_on_line\">網路匯入</string>\n    <string name=\"bookshelf_px\">書架排序</string>\n    <string name=\"check_update_interval\">檢查更新間隔</string>\n    <string name=\"bookshelf_px_0\">按閱讀時間排序</string>\n    <string name=\"bookshelf_px_1\">按更新時間排序</string>\n    <string name=\"bookshelf_px_2\">手動排序</string>\n    <string name=\"read_type\">閱讀方式</string>\n    <string name=\"del_select\">刪除所選</string>\n    <string name=\"del_msg\">是否確認刪除?</string>\n    <string name=\"clear_font\">預設字體</string>\n    <string name=\"find_on_www\">發現</string>\n    <string name=\"find_source_manage\">發現管理</string>\n    <string name=\"find_empty\">沒有內容,去書源里自訂吧!</string>\n    <string name=\"del_all\">刪除所有</string>\n    <string name=\"searchHistory\">搜尋歷史</string>\n    <string name=\"clear\">清除</string>\n    <string name=\"showTitle\">正文顯示標題</string>\n    <string name=\"refresh_default\">書源同步</string>\n    <string name=\"no_last_chapter\">無最新章節資訊</string>\n    <string name=\"showTimeBattery\">顯示時間和電量</string>\n    <string name=\"select_text\">長按選取文本</string>\n    <string name=\"showLine\">顯示分隔線</string>\n    <string name=\"darkStatusIcon\">深色狀態欄圖標</string>\n    <string name=\"content\">內容</string>\n    <string name=\"copy_text\">複製內容</string>\n    <string name=\"download_all\">一鍵緩存</string>\n    <string name=\"content_sl\">這是一段測試文字\\n\\u3000\\u3000只是讓你看看效果的</string>\n    <string name=\"text_bg_style\">文字顏色和背景(長按自訂)</string>\n    <string name=\"immersion_status_bar\">沉浸式狀態欄</string>\n    <string name=\"un_download\">還剩%d章未下載</string>\n    <string name=\"long_click_input_color\">長按輸入顏色值</string>\n    <string name=\"loading\">加載中…</string>\n    <string name=\"group_zg\">追更區</string>\n    <string name=\"group_yf\">養肥區</string>\n    <string name=\"bookmark\">書籤</string>\n    <string name=\"bookmark_add\">添加書籤</string>\n    <string name=\"action_del\">刪除</string>\n    <string name=\"load_over_time\">加載逾時</string>\n    <string name=\"join_group\">關注:%s</string>\n    <string name=\"copy_complete\">已複製</string>\n    <string name=\"arrange_bookshelf\">整理書架</string>\n    <string name=\"clear_bookshelf_s\">這將會刪除所有書籍，請謹慎操作。</string>\n    <string name=\"search_book_source\">搜尋書源</string>\n    <string name=\"search_book_source_num\">搜尋(共%d個書源)</string>\n    <string name=\"chapter_list_size\">目錄(%d)</string>\n    <string name=\"text_bold\">加粗</string>\n    <string name=\"custom_font\">字體</string>\n    <string name=\"text\">文字</string>\n    <string name=\"home_page\">應用主頁</string>\n    <string name=\"right\">右</string>\n    <string name=\"left\">左</string>\n    <string name=\"bottom\">下</string>\n    <string name=\"top\">上</string>\n    <string name=\"padding\">邊距:</string>\n    <string name=\"padding_top\">上邊距</string>\n    <string name=\"padding_bottom\">下邊距</string>\n    <string name=\"padding_left\">左邊距</string>\n    <string name=\"padding_right\">右邊距</string>\n    <string name=\"check_book_source\">校驗書源</string>\n    <string name=\"check_select_source\">校驗所選</string>\n    <string name=\"progress_show\">進度 %d/%d</string>\n    <string name=\"tts_fix\">請安裝並選擇中文TTS!</string>\n    <string name=\"tts_init_failed\">TTS初始化失敗!</string>\n    <string name=\"jf_convert\">簡繁轉換</string>\n    <string name=\"jf_convert_o\">關閉</string>\n    <string name=\"jf_convert_f\">簡轉繁</string>\n    <string name=\"jf_convert_j\">繁轉簡</string>\n    <string name=\"page_mode\">翻頁模式</string>\n    <string name=\"nb_file_sub_count\">%1$d 項</string>\n    <string name=\"nb_file_path\">SD記憶卡</string>\n    <string name=\"nb_file_add_shelf\">加入書架</string>\n    <string name=\"nb_file_add_shelves\">加入書架(%1$d)</string>\n    <string name=\"nb_file_add_succeed\">成功添加%1$d本書</string>\n    <string name=\"fonts_folder\">請將字體檔案放到內部存儲空間目錄Fonts資料夾下重新選擇</string>\n    <string name=\"default_font\">預設字體</string>\n    <string name=\"select_font\">選擇字體</string>\n    <string name=\"text_size\">字號</string>\n    <string name=\"line_size\">行距</string>\n    <string name=\"paragraph_size\">段距</string>\n    <string name=\"edit\">編輯</string>\n    <string name=\"delete\">刪除</string>\n    <string name=\"to_top\">置頂</string>\n    <string name=\"auto_expand_find\">自動展開發現</string>\n    <string name=\"default_expand_first\">默認展開第一組發現</string>\n    <string name=\"threads_num\">當前線程數 %s</string>\n    <string name=\"read_aloud_speed\">朗讀語速</string>\n    <string name=\"auto_next_page\">自動翻頁</string>\n    <string name=\"auto_next_page_stop\">停止自動翻頁</string>\n    <string name=\"auto_next_page_speed\">自動翻頁時每分鐘閱讀字數(CPM)</string>\n    <string name=\"book_info\">書籍資訊</string>\n    <string name=\"ps_default_read\">默認打開書架</string>\n    <string name=\"pt_default_read\">自動跳轉最近閱讀</string>\n    <string name=\"use_to\">替換範圍，選填書名或者源名</string>\n    <string name=\"menu_action_group\">分組</string>\n    <string name=\"backup_path\">备份路徑</string>\n    <string name=\"download_path\">內容緩存路徑</string>\n    <string name=\"sys_file_picker\">系統檔案選擇器</string>\n    <string name=\"new_version\">新版本</string>\n    <string name=\"download_update\">下載更新</string>\n    <string name=\"aloud_volume_page\">朗讀時音量鍵翻頁</string>\n    <string name=\"tip_margin_change\">Tip邊距跟隨邊距調整</string>\n    <string name=\"disable_update\">禁止更新</string>\n    <string name=\"allow_update\">允許更新</string>\n    <string name=\"revert_selection\">反轉選擇</string>\n    <string name=\"search_book_key\">搜尋書名、作者</string>\n    <string name=\"debug_hint\">書名、作者、URL</string>\n    <string name=\"faq\">常見問題</string>\n    <string name=\"pt_show_all_find\">顯示所有發現</string>\n    <string name=\"ps_show_all_find\">關閉則只顯示勾選源的發現</string>\n    <string name=\"update_chapter\">更新目錄</string>\n    <string name=\"txt_chapter_regex\">txt目錄正則</string>\n    <string name=\"set_charset\">設定編碼</string>\n    <string name=\"swap_sort\">倒序-順序</string>\n    <string name=\"sort\">排序</string>\n    <string name=\"sort_auto\">智慧排序</string>\n    <string name=\"sort_manual\">手動排序</string>\n    <string name=\"sort_pin_yin\">拼音排序</string>\n    <string name=\"go_to_top\">滾動到頂部</string>\n    <string name=\"go_to_bottom\">滾動到底部</string>\n    <string name=\"read_y\">已讀: %s</string>\n    <string name=\"pursue_more\">追更</string>\n    <string name=\"fattening\">養肥</string>\n    <string name=\"finish\">完結</string>\n    <string name=\"all_book\">所有書籍</string>\n    <string name=\"pursue_more_book\">追更書籍</string>\n    <string name=\"fattening_book\">養肥書籍</string>\n    <string name=\"finish_book\">完結書籍</string>\n    <string name=\"local_book\">本地書籍</string>\n    <string name=\"status_bar_immersion\">狀態欄顏色透明</string>\n    <string name=\"navigation_bar_color_change\">導航欄變色</string>\n    <string name=\"navigation_bar_color_change_s\">導航欄根據夜間模式變化</string>\n    <string name=\"add_to_shelf\">放入書架</string>\n    <string name=\"continue_read\">繼續閱讀</string>\n    <string name=\"cover_path\">封面地址</string>\n    <string name=\"page_mode_COVER\">覆蓋</string>\n    <string name=\"page_mode_SIMULATION\">仿真</string>\n    <string name=\"page_mode_SLIDE\">平移</string>\n    <string name=\"page_mode_SCROLL\">上下平移</string>\n    <string name=\"page_mode_NONE\">無動畫</string>\n    <string name=\"donate_s\">此書源使用了進階功能，複製支付寶紅包搜索碼領取紅包或關注WeChat官方賬號[開源閱讀軟件]開啟。</string>\n    <string name=\"up_change_source_last_chapter_t\">後台更新換源最新章節</string>\n    <string name=\"up_change_source_last_chapter_s\">開啟則會在應用打開1分鐘後開始更新</string>\n    <string name=\"behavior_main_t\">書架ToolBar自動隱藏</string>\n    <string name=\"behavior_main_s\">滾動書架時ToolBar自動隱藏與顯示</string>\n    <string name=\"login\">登錄</string>\n    <string name=\"login_source\">登錄%s</string>\n    <string name=\"success\">成功</string>\n    <string name=\"source_no_login\">當前源沒有配置登陸地址</string>\n    <string name=\"book_source_name\">書源名稱(bookSourceName)</string>\n    <string name=\"book_source_url\">書源URL（bookSourceUrl）</string>\n    <string name=\"book_source_group\">書源類別(bookSourceGroup)</string>\n    <string name=\"book_source_login_url\">登錄URL(loginUrl)</string>\n    <string name=\"rule_book_author\">作者規則(ruleBookAuthor)</string>\n    <string name=\"rule_book_content\">正文規則(ruleBookContent)</string>\n    <string name=\"rule_book_content_replace\">正文替换規則(ruleBookContentReplace)</string>\n    <string name=\"rule_book_name\">書名規則(ruleBookName)</string>\n    <string name=\"rule_chapter_list\">目錄列表規則(ruleChapterList)</string>\n    <string name=\"rule_chapter_name\">章節名稱規則(ruleChapterName)</string>\n    <string name=\"rule_chapter_list_url\">目錄URL規則(ruleChapterUrl)</string>\n    <string name=\"rule_chapter_list_url_next\">目錄下一頁規則(ruleChapterUrlNext)</string>\n    <string name=\"rule_content_url\">章節URL規則(ruleContentUrl)</string>\n    <string name=\"rule_cover_url\">封面規則(ruleCoverUrl)</string>\n    <string name=\"rule_introduce\">簡介規則(ruleIntroduce)</string>\n    <string name=\"rule_search_author\">搜尋作者規則(ruleSearchAuthor)</string>\n    <string name=\"rule_find_author\">發現作者規則(ruleFindAuthor)</string>\n    <string name=\"rule_search_cover_url\">搜尋封面規則(ruleSearchCoverUrl)</string>\n    <string name=\"rule_find_cover_url\">發現封面規則(ruleFindCoverUrl)</string>\n    <string name=\"rule_search_kind\">搜尋類別規則(ruleSearchKind)</string>\n    <string name=\"rule_find_kind\">發現類別規則(ruleFindKind)</string>\n    <string name=\"rule_search_last_chapter\">搜尋最新章節規則(ruleSearchLastChapter)</string>\n    <string name=\"rule_find_last_chapter\">發現最新章節規則(ruleFindLastChapter)</string>\n    <string name=\"rule_search_list\">搜尋列表規則(ruleSearchList)</string>\n    <string name=\"rule_find_list\">發現列表規則(ruleFindList)</string>\n    <string name=\"rule_search_name\">搜尋書名規則(ruleSearchName)</string>\n    <string name=\"rule_find_name\">發現書名規則(ruleFindName)</string>\n    <string name=\"rule_search_note_url\">搜尋書籍URL規則(ruleSearchNoteUrl)</string>\n    <string name=\"rule_find_note_url\">發現書籍URL規則(ruleFindNoteUrl)</string>\n    <string name=\"rule_search_introduce\">搜尋簡介規則(ruleSearchIntroduce)</string>\n    <string name=\"rule_find_introduce\">發現簡介規則(ruleFindIntroduce)</string>\n    <string name=\"rule_search_url\">搜尋地址(ruleSearchUrl)</string>\n    <string name=\"rule_find_url\">發現規則(ruleFindUrl)</string>\n    <string name=\"rule_content_url_next\">正文下一頁URL規則(ruleContentUrlNext)</string>\n    <string name=\"book_url_pattern\">書籍詳情URL正則(ruleBookUrlPattern)</string>\n    <string name=\"rule_book_info_init\">書籍詳情預處理規則(ruleBookInfoInit)</string>\n    <string name=\"rule_book_kind\">類別規則(ruleBookKind)</string>\n    <string name=\"rule_book_last_chapter\">最新章節規則(ruleBookLastChapter)</string>\n    <string name=\"source_user_agent\">HttpUserAgent</string>\n    <string name=\"debug_source\">調試書源</string>\n    <string name=\"import_by_qr_code\">二維碼匯入</string>\n    <string name=\"scan_qr_code\">掃描二維碼</string>\n    <string name=\"click_on_selected_show_menu\">選中時點擊可彈出選單</string>\n    <string name=\"theme\">主題</string>\n    <string name=\"default_theme\">預設主題</string>\n    <string name=\"restore_default_theme\">復原主題為預設配色</string>\n    <string name=\"join_qq_group\">加入QQ群</string>\n    <string name=\"read_file_error\">檔案存取失敗</string>\n    <string name=\"bg_image_per\">獲取背景圖片需儲存權限</string>\n    <string name=\"input_book_source_url\">輸入書源網址</string>\n    <string name=\"del_file\">刪除檔案</string>\n    <string name=\"del_file_success\">刪除檔案成功</string>\n    <string name=\"sure_del_file\">確定刪除檔案嗎?</string>\n    <string name=\"files_tree\">手機目錄</string>\n    <string name=\"intelligent_import\">智慧匯入</string>\n    <string name=\"find\">發現</string>\n    <string name=\"switch_display_style\">切換顯示樣式</string>\n    <string name=\"import_per\">匯入本機書籍需儲存權限</string>\n    <string name=\"click_to_day\">點擊可切換到白天模式</string>\n    <string name=\"click_to_night\">點擊可切換到夜間模式</string>\n    <string name=\"get_storage_per\">本應用需要儲存權限來儲存備份書籍資訊</string>\n    <string name=\"double_click_exit\">再按一次退出應用</string>\n    <string name=\"import_book_per\">匯入本機書籍需儲存權限</string>\n    <string name=\"network_connection_unavailable\">網路連線不可用</string>\n    <string name=\"yes\">是</string>\n    <string name=\"no\">否</string>\n    <string name=\"sure_del_all_book\">是否刪除全部書籍？</string>\n    <string name=\"sure_del_download_book\">是否同時刪除已下載的書籍目錄？</string>\n    <string name=\"qr_per\">掃描二維碼需相機權限</string>\n    <string name=\"aloud_can_not_auto_page\">朗讀正在運行,不能自動翻頁</string>\n    <string name=\"input_charset\">輸入編碼</string>\n    <string name=\"text_chapter_list_rule\">TXT目錄規則</string>\n    <string name=\"open_local_book_per\">打開外部書籍需獲取儲存權限</string>\n    <string name=\"get_book_info_error\">書籍資訊獲取失敗</string>\n    <string name=\"get_content_error\">內容獲取失敗</string>\n    <string name=\"get_chapter_list_error\">目錄獲取失敗</string>\n    <string name=\"get_web_content_error\">訪問網站失敗:%s</string>\n    <string name=\"no_book_name\">未獲取到書名</string>\n    <string name=\"input_replace_url\">輸入替換規則網址</string>\n    <string name=\"get_book_list_success\">搜尋列表獲取成功%d</string>\n    <string name=\"non_null_source_name_url\">書源名稱和URL不能為空</string>\n    <string name=\"gallery\">相簿</string>\n    <string name=\"get_ali_pay_hb\">領支付寶紅包</string>\n    <string name=\"non_update_url\">沒有獲取到更新地址</string>\n    <string name=\"check_host_cookie\">正在打開首頁，成功自動返回主畫面</string>\n    <string name=\"click_check_after_success\">登錄成功後請點擊右上角圖示進行首頁訪問測試</string>\n    <string name=\"chapter\">章</string>\n    <string name=\"chapter_s\">章節:</string>\n    <string name=\"to\">至</string>\n    <string name=\"use_regex\">使用正則表達式</string>\n    <string name=\"indent\">縮進</string>\n    <string name=\"indent_0\">無縮進</string>\n    <string name=\"indent_1\">一字符縮進</string>\n    <string name=\"indent_2\">二字符縮進</string>\n    <string name=\"indent_3\">三字符縮進</string>\n    <string name=\"indent_4\">四字符縮進</string>\n    <string name=\"select_sd_file\">選擇SD記憶卡</string>\n    <string name=\"no_find\">沒有發現，可以在書源里添加。</string>\n    <string name=\"restore_default\">恢復預設</string>\n    <string name=\"set_download_per\">自訂暫存資料路徑需要儲存權限</string>\n    <string name=\"black\">黑色</string>\n    <string name=\"content_empty\">文章內容為空</string>\n    <string name=\"on_change_source\">正在換源請等待…</string>\n    <string name=\"chapter_list_empty\">目錄列表為空</string>\n    <string name=\"load_error_msg\">加載失敗\\n%s</string>\n    <string name=\"content_padding\">正文邊距</string>\n    <string name=\"tip_padding\">Tip邊距</string>\n    <string name=\"text_letter_spacing\">字距</string>\n    <string name=\"e_ink_mode\">E-Ink 模式</string>\n    <string name=\"e_ink_mode_detail\">去除動畫,優化電紙書使用體驗</string>\n    <string name=\"web_menu\">Web服務</string>\n    <string name=\"web_port_title\">web端口</string>\n    <string name=\"web_port_summary\">當前端口 %s</string>\n    <string name=\"qr_share\">二維碼分享</string>\n    <string name=\"str_share\">字符串分享</string>\n    <string name=\"wifi_share\">wifi分享</string>\n    <string name=\"please_grant_storage_permission\">請授予儲存權限</string>\n    <string name=\"skip_previous\">上一個</string>\n    <string name=\"skip_next\">下一個</string>\n    <string name=\"music\">音樂</string>\n    <string name=\"audio\">音訊</string>\n    <string name=\"change_icon_summary\">切換應用顯示在桌面的圖示</string>\n    <string name=\"show_enabled\">顯示勾選</string>\n    <string name=\"to_lh\">擴展到劉海</string>\n    <string name=\"layout_list\">列表視圖</string>\n    <string name=\"layout_grid3\">網格視圖三列</string>\n    <string name=\"layout_grid4\">網格視圖四列</string>\n    <string name=\"all_source\">全部書源</string>\n    <string name=\"search_result_filter_grade\">搜索結果過濾等級</string>\n    <string name=\"search_result_filter_grade_tip\">調高過濾等級可以避免看到書名/作者無關的搜索結果。\n        等級1-9：搜索關鍵字需要出現到書名/作者中，但是允許有8-0個字（空格不計）沒有出現（避免誤輸入的影響）。默認等級0，不過濾。推薦等級7，允許2個錯別字。\n        \\n當前過濾等級 %s</string>\n    <string name=\"login_ui\">登录UI(loginUi)</string>\n    <string name=\"login_check_js\">登录检查Js(loginCheckJs)</string>\n    <string name=\"rule_vip\">VIP标识(ruleVip)</string>\n    <string name=\"rule_pay\">购买标识(rulePay)</string>\n    <string name=\"chapter_pay\">购买</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/file_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path\n        name=\"files_cache\"\n        path=\"Android/data/com.kunfei.bookshelf/cache\" />\n    <external-path\n        name=\"external_storage_root\"\n        path=\".\" />\n</paths>"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\" />\n</network-security-config>"
  },
  {
    "path": "app/src/main/res/xml/pref_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <Preference\n        android:key=\"webDavSetting\"\n        android:summary=\"WebDav备份与恢复\"\n        android:title=\"WebDav设置\" />\n\n    <com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory android:title=\"书架设置\">\n\n        <ListPreference\n            android:defaultValue=\"0\"\n            android:entries=\"@array/bookshelf_px_title\"\n            android:entryValues=\"@array/bookshelf_px_value\"\n            android:key=\"@string/pk_bookshelf_px\"\n            android:title=\"@string/bookshelf_px\" />\n\n        <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n            android:defaultValue=\"true\"\n            android:key=\"@string/pk_auto_refresh\"\n            android:summary=\"@string/ps_auto_refresh\"\n            android:title=\"@string/pt_auto_refresh\" />\n\n        <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"@string/pk_auto_download\"\n            android:summary=\"@string/ps_auto_download\"\n            android:title=\"@string/pt_auto_download\" />\n\n        <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"@string/pk_default_read\"\n            android:summary=\"@string/ps_default_read\"\n            android:title=\"@string/pt_default_read\" />\n\n        <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n            android:defaultValue=\"true\"\n            android:key=\"showAllFind\"\n            android:summary=\"@string/ps_show_all_find\"\n            android:title=\"@string/pt_show_all_find\" />\n\n        <Preference\n            android:key=\"clearCache\"\n            android:title=\"@string/clear_cache\" />\n\n    </com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory>\n\n    <com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory android:title=\"其它设置\">\n\n        <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n            android:defaultValue=\"true\"\n            android:key=\"replaceEnableDefault\"\n            android:title=\"默认启用替换净化\" />\n\n        <Preference\n            android:key=\"backupPath\"\n            android:title=\"@string/backup_path\" />\n\n        <Preference\n            android:key=\"@string/pk_download_path\"\n            android:title=\"@string/download_path\" />\n\n        <com.kunfei.bookshelf.widget.number.NumberPickerPreference\n            android:defaultValue=\"6\"\n            android:key=\"@string/pk_threads_num\"\n            android:summary=\"@string/threads_num\"\n            android:title=\"@string/update_search_threads_num\"\n            app:MaxValue=\"1024\"\n            app:MinValue=\"1\" />\n\n        <com.kunfei.bookshelf.widget.number.NumberPickerPreference\n            android:defaultValue=\"0\"\n            android:key=\"@string/pk_search_result_filter_grade\"\n            android:summary=\"@string/search_result_filter_grade_tip\"\n            android:title=\"@string/search_result_filter_grade\"\n            app:MaxValue=\"9\"\n            app:MinValue=\"0\" />\n\n        <com.kunfei.bookshelf.widget.number.NumberPickerPreference\n            android:defaultValue=\"1122\"\n            android:key=\"webPort\"\n            android:summary=\"@string/web_port_summary\"\n            android:title=\"@string/web_port_title\"\n            app:MaxValue=\"60000\"\n            app:MinValue=\"1024\" />\n\n        <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n            android:defaultValue=\"false\"\n            android:key=\"upChangeSourceLastChapter\"\n            android:summary=\"@string/up_change_source_last_chapter_s\"\n            android:title=\"@string/up_change_source_last_chapter_t\" />\n\n        <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n            android:defaultValue=\"true\"\n            android:key=\"process_text\"\n            android:summary=\"长按文字在操作菜单中显示阅读·搜索\"\n            android:title=\"文字操作显示搜索\" />\n\n    </com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory>\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/pref_settings_theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <com.kunfei.bookshelf.widget.prefs.IconListPreference\n        android:defaultValue=\"@string/icon_main\"\n        android:entries=\"@array/icon_names\"\n        android:entryValues=\"@array/icons\"\n        android:key=\"launcher_icon\"\n        android:summary=\"@string/change_icon_summary\"\n        android:title=\"@string/change_icon\"\n        app:icons=\"@array/icons\" />\n\n    <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n        android:defaultValue=\"false\"\n        android:key=\"immersionStatusBar\"\n        android:summary=\"@string/status_bar_immersion\"\n        android:title=\"@string/immersion_status_bar\" />\n\n    <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n        android:defaultValue=\"false\"\n        android:key=\"navigationBarColorChange\"\n        android:summary=\"@string/navigation_bar_color_change_s\"\n        android:title=\"@string/navigation_bar_color_change\" />\n\n    <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n        android:defaultValue=\"true\"\n        android:key=\"behaviorMain\"\n        android:summary=\"@string/behavior_main_s\"\n        android:title=\"@string/behavior_main_t\" />\n\n    <com.kunfei.bookshelf.widget.prefs.ATESwitchPreference\n        android:defaultValue=\"false\"\n        android:key=\"E-InkMode\"\n        android:summary=\"@string/e_ink_mode_detail\"\n        android:title=\"@string/e_ink_mode\" />\n\n    <Preference\n        android:key=\"defaultTheme\"\n        android:summary=\"@string/restore_default_theme\"\n        android:title=\"@string/default_theme\" />\n\n    <com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory android:title=\"白天\">\n\n        <com.jaredrummler.android.colorpicker.ColorPreference\n            android:defaultValue=\"@color/md_grey_100\"\n            android:key=\"colorPrimary\"\n            android:summary=\"白天，主色调\"\n            android:title=\"主色调\"\n            app:cpv_dialogType=\"preset\" />\n\n        <com.jaredrummler.android.colorpicker.ColorPreference\n            android:defaultValue=\"@color/md_pink_600\"\n            android:key=\"colorAccent\"\n            android:summary=\"白天，强调色\"\n            android:title=\"强调色\"\n            app:cpv_dialogType=\"preset\" />\n\n        <com.jaredrummler.android.colorpicker.ColorPreference\n            android:defaultValue=\"@color/md_grey_100\"\n            android:key=\"colorBackground\"\n            android:summary=\"白天，背景色\"\n            android:title=\"背景色\"\n            app:cpv_dialogType=\"preset\" />\n\n    </com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory>\n\n    <com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory android:title=\"夜间\">\n\n        <com.jaredrummler.android.colorpicker.ColorPreference\n            android:defaultValue=\"@color/md_grey_800\"\n            android:key=\"colorPrimaryNight\"\n            android:summary=\"夜间，主色调\"\n            android:title=\"主色调\"\n            app:cpv_dialogType=\"preset\" />\n\n        <com.jaredrummler.android.colorpicker.ColorPreference\n            android:defaultValue=\"@color/md_pink_800\"\n            android:key=\"colorAccentNight\"\n            android:summary=\"夜间，强调色\"\n            android:title=\"强调色\"\n            app:cpv_dialogType=\"preset\" />\n\n        <com.jaredrummler.android.colorpicker.ColorPreference\n            android:defaultValue=\"@color/md_grey_800\"\n            android:key=\"colorBackgroundNight\"\n            android:summary=\"夜间，背景色\"\n            android:title=\"背景色\"\n            app:cpv_dialogType=\"preset\" />\n\n    </com.kunfei.bookshelf.widget.prefs.ATEPreferenceCategory>\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/pref_settings_web_dav.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <EditTextPreference\n        android:key=\"web_dav_url\"\n        android:title=\"WebDav 服务器地址\"\n        android:summary=\"输入你的服务器地址\"\n        android:singleLine=\"true\"/>\n\n    <EditTextPreference\n        android:key=\"web_dav_account\"\n        android:title=\"WebDav 账号\"\n        android:summary=\"输入你的WebDav账号\"\n        android:singleLine=\"true\"/>\n\n    <EditTextPreference\n        android:key=\"web_dav_password\"\n        android:title=\"WebDav 密码\"\n        android:summary=\"输入你的WebDav授权密码\"\n        android:singleLine=\"true\"\n        android:password=\"true\" />\n\n    <Preference\n        android:key=\"web_dav_restore\"\n        android:title=\"恢复\"\n        android:summary=\"从WebDav恢复\"/>\n\n</PreferenceScreen>"
  },
  {
    "path": "app/src/main/res/xml/shortcuts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shortcuts xmlns:tools=\"http://schemas.android.com/tools\"\n    xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <shortcut\n        android:shortcutId=\"bookRead\"\n        android:enabled=\"true\"\n        android:icon=\"@drawable/ic_read_book\"\n        android:shortcutShortLabel=\"@string/last_read\"\n        android:shortcutLongLabel=\"@string/last_read\"\n        android:shortcutDisabledMessage=\"@string/last_read\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <intent\n            android:action=\"android.intent.action.VIEW\"\n            android:targetPackage=\"${applicationId}\"\n            android:targetClass=\"com.kunfei.bookshelf.view.activity.MainActivity\" />\n        <categories android:name=\"android.shortcut.conversation\" />\n\n        <intent\n            android:action=\"android.intent.action.VIEW\"\n            android:targetPackage=\"${applicationId}\"\n            android:targetClass=\"com.kunfei.bookshelf.view.activity.ReadBookActivity\" />\n        <categories android:name=\"android.shortcut.conversation\"/>\n    </shortcut>\n\n    <shortcut\n        android:shortcutId=\"bookshelf\"\n        android:enabled=\"true\"\n        android:icon=\"@drawable/ic_read_book\"\n        android:shortcutShortLabel=\"@string/bookshelf\"\n        android:shortcutLongLabel=\"@string/bookshelf\"\n        android:shortcutDisabledMessage=\"@string/bookshelf\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <intent\n            android:action=\"android.intent.action.VIEW\"\n            android:targetPackage=\"${applicationId}\"\n            android:targetClass=\"com.kunfei.bookshelf.view.activity.MainActivity\" />\n        <categories android:name=\"android.shortcut.conversation\" />\n    </shortcut>\n</shortcuts>\n"
  },
  {
    "path": "app/src/test/java/com/kunfei/bookshelf/ExampleUnitTest.java",
    "content": "package com.kunfei.bookshelf;\n\nimport org.junit.Test;\nimport static org.junit.Assert.*;\n\n/**\n * To work on unit tests, switch the Test Artifact in the Build Variants view.\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() throws Exception {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "basemvplib/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "basemvplib/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nandroid {\n    compileSdkVersion 30\n    buildToolsVersion '30.0.3'\n\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 30\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n        android {\n        }\n    }\n\n    compileOptions {\n        targetCompatibility 1.8\n        sourceCompatibility 1.8\n    }\n}\n\ndependencies {\n    testImplementation 'junit:junit:4.13.2'\n    api fileTree(dir: 'libs', include: ['*.jar'])\n\n    //support\n    api 'androidx.core:core:1.6.0'\n    api 'androidx.appcompat:appcompat:1.3.0'\n\n}"
  },
  {
    "path": "basemvplib/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in D:\\CodeTool\\Android\\Android_SDK/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n-optimizationpasses 5\n-dontskipnonpubliclibraryclassmembers\n-dontusemixedcaseclassnames\n-classobfuscationdictionary  obfuscationClassNames.txt\n-dontskipnonpubliclibraryclasses\n-verbose\n\n##################OKGO########################\n#okgo\n-dontwarn com.lzy.okgo.**\n-keep class com.lzy.okgo.**{*;}\n\n#okrx\n-dontwarn com.lzy.okrx.**\n-keep class com.lzy.okrx.**{*;}\n\n#okserver\n-dontwarn com.lzy.okserver.**\n-keep class com.lzy.okserver.**{*;}"
  },
  {
    "path": "basemvplib/src/main/AndroidManifest.xml",
    "content": "<manifest package=\"com.monke.basemvplib\">\n\n</manifest>\n"
  },
  {
    "path": "basemvplib/src/main/java/com/kunfei/basemvplib/AppActivityManager.java",
    "content": "package com.kunfei.basemvplib;\n\nimport android.app.Activity;\n\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\n\n/**\n * Activity管理器,管理项目中Activity的状态\n */\npublic class AppActivityManager {\n\n    private static List<WeakReference<Activity>> activities;\n\n    private AppActivityManager() {\n        activities = new ArrayList<>();\n    }\n\n    private static volatile AppActivityManager instance;\n\n    public static AppActivityManager getInstance() {\n        if (null == instance) {\n            synchronized (AppActivityManager.class) {\n                if (null == instance) {\n                    instance = new AppActivityManager();\n                }\n            }\n        }\n        return instance;\n    }\n\n    public List<WeakReference<Activity>> getActivities() {\n        return activities;\n    }\n\n    /**\n     * 添加Activity\n     */\n    public void add(Activity activity) {\n        activities.add(new WeakReference<Activity>(activity));\n    }\n\n    /**\n     * 移除Activity\n     */\n    public void remove(Activity activity) {\n        for (WeakReference<Activity> temp : activities) {\n            if (null != temp.get() && temp.get() == activity) {\n                activities.remove(temp);\n                break;\n            }\n        }\n    }\n\n    /**\n     * 移除Activity\n     */\n    public void remove(Class<?> activityClass) {\n        for (Iterator<WeakReference<Activity>> iterator = activities.iterator(); iterator.hasNext(); ) {\n            WeakReference<Activity> item = iterator.next();\n            if (null != item && null != item.get() && item.get().getClass() == activityClass) {\n                iterator.remove();\n            }\n        }\n    }\n\n    /**\n     * 关闭指定 activity\n     */\n    public void finishActivity(BaseActivity... activities) {\n        for (BaseActivity activity : activities) {\n            if (null != activity) {\n                activity.finish();\n            }\n        }\n    }\n\n    /**\n     * 关闭指定 activity(class)\n     */\n    public void finishActivity(Class<?>... activityClasses) {\n        ArrayList<WeakReference<Activity>> waitFinish = new ArrayList<>();\n        for (WeakReference<Activity> temp : activities) {\n            for (Class<?> activityClass : activityClasses) {\n                if (null != temp.get() && temp.get().getClass() == activityClass) {\n                    waitFinish.add(temp);\n                    break;\n                }\n            }\n        }\n        for (WeakReference<Activity> activityWeakReference : waitFinish) {\n            if (null != activityWeakReference.get()) {\n                activityWeakReference.get().finish();\n            }\n        }\n    }\n\n    /**\n     * 判断指定Activity是否存在\n     */\n    public Boolean isExist(Class<?> activityClass) {\n        boolean result = false;\n        for (WeakReference<Activity> item : activities) {\n            if (null != item && null != item.get() && item.get().getClass() == activityClass) {\n                result = true;\n                break;\n            }\n        }\n        return result;\n    }\n}"
  },
  {
    "path": "basemvplib/src/main/java/com/kunfei/basemvplib/BaseActivity.java",
    "content": "package com.kunfei.basemvplib;\n\nimport android.app.ActivityOptions;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.PorterDuff;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.view.View;\nimport android.widget.Toast;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\nimport com.monke.basemvplib.R;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.AppCompatActivity;\n\npublic abstract class BaseActivity<T extends IPresenter> extends AppCompatActivity implements IView {\n    public final static String START_SHEAR_ELE = \"start_with_share_ele\";\n    public static final int SUCCESS = 1;\n    public static final int ERROR = -1;\n    protected Bundle savedInstanceState;\n    protected T mPresenter;\n    protected boolean isRecreate;\n    private Boolean startShareAnim = false;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        this.savedInstanceState = savedInstanceState;\n        if(getIntent()!=null){\n            isRecreate = getIntent().getBooleanExtra(\"isRecreate\", false);\n            startShareAnim = getIntent().getBooleanExtra(START_SHEAR_ELE, false);\n        }\n        AppActivityManager.getInstance().add(this);\n        initSDK();\n        onCreateActivity();\n        mPresenter = initInjector();\n        attachView();\n        initData();\n        bindView();\n        bindEvent();\n        firstRequest();\n    }\n\n    /**\n     * 首次逻辑操作\n     */\n    protected void firstRequest() {\n\n    }\n\n    /**\n     * 事件触发绑定\n     */\n    protected void bindEvent() {\n\n    }\n\n    /**\n     * 控件绑定\n     */\n    protected void bindView() {\n\n    }\n\n    /**\n     * P层绑定V层\n     */\n    private void attachView() {\n        if (null != mPresenter) {\n            mPresenter.attachView(this);\n        }\n    }\n\n    /**\n     * P层解绑V层\n     */\n    private void detachView() {\n        if (null != mPresenter) {\n            mPresenter.detachView();\n        }\n    }\n\n    /**\n     * SDK初始化\n     */\n    protected void initSDK() {\n\n    }\n\n    /**\n     * P层绑定   若无则返回null;\n     */\n    protected abstract T initInjector();\n\n    /**\n     * 布局载入  setContentView()\n     */\n    protected abstract void onCreateActivity();\n\n    /**\n     * 数据初始化\n     */\n    protected abstract void initData();\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        detachView();\n        AppActivityManager.getInstance().remove(this);\n    }\n\n    @Override\n    public void recreate() {\n        getIntent().putExtra(\"isRecreate\", true);\n        super.recreate();\n    }\n\n    /////////Toast//////////////////\n\n    public void toast(String msg) {\n        toast(msg, Toast.LENGTH_SHORT, 0);\n    }\n\n    public void toast(String msg, int state) {\n        toast(msg, Toast.LENGTH_LONG, state);\n    }\n\n    public void toast(int strId) {\n        toast(strId, 0);\n    }\n\n    public void toast(int strId, int state) {\n        toast(getString(strId), Toast.LENGTH_LONG, state);\n    }\n\n    public void toast(String msg, int length, int state) {\n        Toast toast = Toast.makeText(this, msg, length);\n        try {\n            if (state == SUCCESS) {\n                toast.getView().getBackground().setColorFilter(getResources().getColor(R.color.success), PorterDuff.Mode.SRC_IN);\n            } else if (state == ERROR) {\n                toast.getView().getBackground().setColorFilter(getResources().getColor(R.color.error), PorterDuff.Mode.SRC_IN);\n            }\n        } catch (Exception ignored) {\n        }\n        toast.show();\n    }\n\n    public Context getContext(){\n        return this;\n    }\n\n    public Boolean getStart_share_ele() {\n        return startShareAnim;\n    }\n\n    ////////////////////////////////启动Activity转场动画/////////////////////////////////////////////\n\n    protected void startActivityForResultByAnim(Intent intent, int requestCode, int animIn, int animExit) {\n        startActivityForResult(intent, requestCode);\n        overridePendingTransition(animIn, animExit);\n    }\n\n    protected void startActivityByAnim(Intent intent, int animIn, int animExit) {\n        startActivity(intent);\n        overridePendingTransition(animIn, animExit);\n    }\n\n    protected void startActivityByAnim(Intent intent, @NonNull View view, @NonNull String transitionName, int animIn, int animExit) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            intent.putExtra(START_SHEAR_ELE, true);\n            ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, view, transitionName);\n            startActivity(intent, options.toBundle());\n        } else {\n            startActivityByAnim(intent, animIn, animExit);\n        }\n    }\n}"
  },
  {
    "path": "basemvplib/src/main/java/com/kunfei/basemvplib/BaseFragment.java",
    "content": "package com.kunfei.basemvplib;\n\nimport static com.kunfei.basemvplib.BaseActivity.START_SHEAR_ELE;\n\nimport android.app.ActivityOptions;\nimport android.content.Intent;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.Fragment;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\n\npublic abstract class BaseFragment<T extends IPresenter> extends Fragment implements IView {\n    protected View view;\n    protected Bundle savedInstanceState;\n\n    @Nullable\n    @Override\n    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {\n        this.savedInstanceState = savedInstanceState;\n        initSDK();\n        view = createView(inflater, container);\n        initData();\n        bindView();\n        bindEvent();\n        firstRequest();\n        return view;\n    }\n\n    /**\n     * 事件触发绑定\n     */\n    protected void bindEvent() {\n\n    }\n\n    /**\n     * 控件绑定\n     */\n    protected void bindView() {\n\n    }\n\n    /**\n     * 数据初始化\n     */\n    protected void initData() {\n\n    }\n\n    /**\n     * 首次逻辑操作\n     */\n    protected void firstRequest() {\n\n    }\n\n    /**\n     * 加载布局\n     */\n    protected abstract View createView(LayoutInflater inflater, ViewGroup container);\n\n    /**\n     * 第三方SDK初始化\n     */\n    protected void initSDK() {\n\n    }\n\n    protected void startActivityByAnim(Intent intent, int animIn, int animExit) {\n        startActivity(intent);\n        requireActivity().overridePendingTransition(animIn, animExit);\n    }\n\n    protected void startActivityByAnim(Intent intent, @NonNull View view, @NonNull String transitionName, int animIn, int animExit) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            intent.putExtra(START_SHEAR_ELE, true);\n            ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(getActivity(), view, transitionName);\n            startActivity(intent, options.toBundle());\n        } else {\n            startActivityByAnim(intent, animIn, animExit);\n        }\n    }\n}\n"
  },
  {
    "path": "basemvplib/src/main/java/com/kunfei/basemvplib/BasePresenterImpl.java",
    "content": "package com.kunfei.basemvplib;\n\nimport com.kunfei.basemvplib.impl.IPresenter;\nimport com.kunfei.basemvplib.impl.IView;\n\nimport androidx.annotation.NonNull;\n\npublic abstract class BasePresenterImpl<T extends IView> implements IPresenter {\n    protected T mView;\n\n    @Override\n    public void attachView(@NonNull IView iView) {\n        mView = (T) iView;\n    }\n}\n"
  },
  {
    "path": "basemvplib/src/main/java/com/kunfei/basemvplib/BitIntentDataManager.java",
    "content": "//Copyright (c) 2017. 章钦豪. All rights reserved.\npackage com.kunfei.basemvplib;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\npublic class BitIntentDataManager {\n    private static Map<String, Object> bigData;\n\n    private static BitIntentDataManager instance = null;\n\n    private BitIntentDataManager() {\n        bigData = new HashMap<>();\n    }\n\n    public static BitIntentDataManager getInstance() {\n        if (instance == null) {\n            synchronized (BitIntentDataManager.class) {\n                if (instance == null) {\n                    instance = new BitIntentDataManager();\n                }\n            }\n        }\n        return instance;\n    }\n\n    public Object getData(String key) {\n        Object object = bigData.get(key);\n        bigData.remove(key);\n        return object;\n    }\n\n    public void putData(String key, Object data) {\n        bigData.put(key, data);\n    }\n\n}\n"
  },
  {
    "path": "basemvplib/src/main/java/com/kunfei/basemvplib/impl/IPresenter.java",
    "content": "package com.kunfei.basemvplib.impl;\n\n\nimport androidx.annotation.NonNull;\n\npublic interface IPresenter {\n    /**\n     * 注入View，使之能够与View相互响应\n     */\n    void attachView(@NonNull IView iView);\n\n    /**\n     * 释放资源，如果使用了网络请求 可以在此执行IModel.cancelRequest()\n     */\n    void detachView();\n}\n"
  },
  {
    "path": "basemvplib/src/main/java/com/kunfei/basemvplib/impl/IView.java",
    "content": "package com.kunfei.basemvplib.impl;\n\nimport android.content.Context;\n\npublic interface IView {\n    Context getContext();\n\n    void toast(String msg);\n\n    void toast(int id);\n\n}\n"
  },
  {
    "path": "basemvplib/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"error\">#eb4333</color>\n    <color name=\"success\">#439b53</color>\n</resources>\n"
  },
  {
    "path": "basemvplib/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">BaseMvpLib</string>\n</resources>\n"
  },
  {
    "path": "basemvplib/src/test/java/com/kunfei/basemvplib/ExampleUnitTest.java",
    "content": "package com.kunfei.basemvplib;\n\nimport org.junit.Test;\n\nimport static org.junit.Assert.*;\n\n/**\n * To work on unit tests, switch the Test Artifact in the Build Variants view.\n */\npublic class ExampleUnitTest {\n    @Test\n    public void addition_isCorrect() throws Exception {\n        assertEquals(4, 2 + 2);\n    }\n}"
  },
  {
    "path": "build.gradle",
    "content": "ext {\n    support_library_version = '28.0.0'\n}\nbuildscript {\n    ext.kotlin_version = '1.6.10'\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://maven.aliyun.com/repository/public' }\n        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }\n        maven { url 'https://plugins.gradle.org/m2/' }\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.1.2'\n        classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0'\n        classpath 'com.google.gms:google-services:4.3.10'\n        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1'\n        classpath 'net.ricecode:string-similarity:1.0.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n        classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.4'\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://maven.aliyun.com/repository/public' }\n        maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }\n        maven { url 'https://plugins.gradle.org/m2/' }\n        maven { url 'https://jitpack.io' }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Sat Jun 05 22:59:28 CST 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.2-bin.zip\ndistributionPath=wrapper/dists\nzipStorePath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\n"
  },
  {
    "path": "gradle.properties",
    "content": "## For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n#\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx1024m -XX:MaxPermSize=256m\norg.gradle.jvmargs=-Xms1024m -Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n#\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n#Fri Mar 30 10:51:47 CST 2018\nandroid.useAndroidX=true\nandroid.enableJetifier=true\norg.gradle.parallel=true\norg.gradle.damon=true\norg.gradle.configureondemand=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "mail",
    "content": "gekunfei@live.com\n"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':basemvplib'\n"
  },
  {
    "path": "tool/书源整理工具/BookSourceMgr.dpr",
    "content": "program BookSourceMgr;\r\n\r\nuses\r\n  Forms,\r\n  uFrmMain in 'uFrmMain.pas' {Form1},\r\n  Themes,\r\n  Styles,\r\n  uFrmWait in 'uFrmWait.pas' {Form2},\r\n  uBookSourceBean in 'uBookSourceBean.pas',\r\n  uFrmEditSource in 'uFrmEditSource.pas' {frmEditSource},\r\n  uFrmReplaceGroup in 'uFrmReplaceGroup.pas' {frmReplaceGroup};\r\n\r\n{$R *.res}\r\n\r\nbegin\r\n  Application.Initialize;\r\n  Application.MainFormOnTaskbar := True;\r\n  Application.Title := 'ĶԴ';\r\n  Application.CreateForm(TForm1, Form1);\r\n  Application.Run;\r\nend.\r\n"
  },
  {
    "path": "tool/书源整理工具/BookSourceMgr.dproj",
    "content": "﻿<Project xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\r\n    <PropertyGroup>\r\n        <ProjectGuid>{b1648034-9a9f-40d4-a355-b3335a5a9446}</ProjectGuid>\r\n        <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>\r\n        <DCC_DCCCompiler>DCC32</DCC_DCCCompiler>\r\n        <DCC_DependencyCheckOutputName>bin\\BookSourceMgr.exe</DCC_DependencyCheckOutputName>\r\n        <MainSource>BookSourceMgr.dpr</MainSource>\r\n        <FrameworkType>VCL</FrameworkType>\r\n        <ProjectVersion>18.3</ProjectVersion>\r\n        <Base>True</Base>\r\n        <Config Condition=\"'$(Config)'==''\">Debug</Config>\r\n        <Platform Condition=\"'$(Platform)'==''\">Win32</Platform>\r\n        <TargetedPlatforms>3</TargetedPlatforms>\r\n        <AppType>Application</AppType>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Config)'=='Base' or '$(Base)'!=''\">\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''\">\r\n        <Base_Win32>true</Base_Win32>\r\n        <CfgParent>Base</CfgParent>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"('$(Platform)'=='Win64' and '$(Base)'=='true') or '$(Base_Win64)'!=''\">\r\n        <Base_Win64>true</Base_Win64>\r\n        <CfgParent>Base</CfgParent>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Config)'=='Release' or '$(Cfg_1)'!=''\">\r\n        <Cfg_1>true</Cfg_1>\r\n        <CfgParent>Base</CfgParent>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"('$(Platform)'=='Win32' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win32)'!=''\">\r\n        <Cfg_1_Win32>true</Cfg_1_Win32>\r\n        <CfgParent>Cfg_1</CfgParent>\r\n        <Cfg_1>true</Cfg_1>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"('$(Platform)'=='Win64' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win64)'!=''\">\r\n        <Cfg_1_Win64>true</Cfg_1_Win64>\r\n        <CfgParent>Cfg_1</CfgParent>\r\n        <Cfg_1>true</Cfg_1>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Config)'=='Debug' or '$(Cfg_2)'!=''\">\r\n        <Cfg_2>true</Cfg_2>\r\n        <CfgParent>Base</CfgParent>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"('$(Platform)'=='Win32' and '$(Cfg_2)'=='true') or '$(Cfg_2_Win32)'!=''\">\r\n        <Cfg_2_Win32>true</Cfg_2_Win32>\r\n        <CfgParent>Cfg_2</CfgParent>\r\n        <Cfg_2>true</Cfg_2>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"('$(Platform)'=='Win64' and '$(Cfg_2)'=='true') or '$(Cfg_2_Win64)'!=''\">\r\n        <Cfg_2_Win64>true</Cfg_2_Win64>\r\n        <CfgParent>Cfg_2</CfgParent>\r\n        <Cfg_2>true</Cfg_2>\r\n        <Base>true</Base>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Base)'!=''\">\r\n        <SanitizedProjectName>BookSourceMgr</SanitizedProjectName>\r\n        <DCC_Namespace>Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Data;Datasnap;Web;Soap;Winapi;$(DCC_Namespace)</DCC_Namespace>\r\n        <VerInfo_Locale>2052</VerInfo_Locale>\r\n        <VerInfo_Keys>CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>\r\n        <DCC_UnitSearchPath>D:\\Documents\\Desktop\\SynEdit\\Source;D:\\Documents\\Desktop\\YxdJson\\source;D:\\Documents\\Desktop\\YxdIocp\\YxdIocp\\source\\IOCP;D:\\Documents\\Desktop\\YxdWorker\\YxdWorker\\source;E:\\Projects\\Delphi\\YxdIocp\\source\\IOCP;E:\\Projects\\Delphi\\YxdJson\\source;E:\\Projects\\Delphi\\YxdWorker\\source;\\\\Mac\\Home\\Desktop\\Project\\SynEdit\\SynEdit\\Source;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Base_Win32)'!=''\">\r\n        <DCC_Namespace>System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>\r\n        <BT_BuildType>Debug</BT_BuildType>\r\n        <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>\r\n        <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>\r\n        <VerInfo_Locale>1033</VerInfo_Locale>\r\n        <Manifest_File>$(BDS)\\bin\\default_app.manifest</Manifest_File>\r\n        <Icon_MainIcon>doc\\Main.ico</Icon_MainIcon>\r\n        <AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>\r\n        <UWP_DelphiLogo44>$(BDS)\\bin\\Artwork\\Windows\\UWP\\delphi_UwpDefault_44.png</UWP_DelphiLogo44>\r\n        <UWP_DelphiLogo150>$(BDS)\\bin\\Artwork\\Windows\\UWP\\delphi_UwpDefault_150.png</UWP_DelphiLogo150>\r\n        <AppEnableHighDPI>true</AppEnableHighDPI>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Base_Win64)'!=''\">\r\n        <Manifest_File>$(BDS)\\bin\\default_app.manifest</Manifest_File>\r\n        <Icon_MainIcon>BookSourceMgr_Icon1.ico</Icon_MainIcon>\r\n        <AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>\r\n        <UWP_DelphiLogo44>$(BDS)\\bin\\Artwork\\Windows\\UWP\\delphi_UwpDefault_44.png</UWP_DelphiLogo44>\r\n        <UWP_DelphiLogo150>$(BDS)\\bin\\Artwork\\Windows\\UWP\\delphi_UwpDefault_150.png</UWP_DelphiLogo150>\r\n        <DCC_Namespace>System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace)</DCC_Namespace>\r\n        <BT_BuildType>Debug</BT_BuildType>\r\n        <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>\r\n        <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>\r\n        <VerInfo_Locale>1033</VerInfo_Locale>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Cfg_1)'!=''\">\r\n        <Version>7.0</Version>\r\n        <DCC_DebugInformation>0</DCC_DebugInformation>\r\n        <DCC_LocalDebugSymbols>False</DCC_LocalDebugSymbols>\r\n        <DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>\r\n        <DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Cfg_1_Win32)'!=''\">\r\n        <AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>\r\n        <AppEnableHighDPI>true</AppEnableHighDPI>\r\n        <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>\r\n        <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>\r\n        <VerInfo_Locale>1033</VerInfo_Locale>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Cfg_1_Win64)'!=''\">\r\n        <AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>\r\n        <AppEnableHighDPI>true</AppEnableHighDPI>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Cfg_2)'!=''\">\r\n        <Version>7.0</Version>\r\n        <DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>\r\n        <DCC_ExeOutput>.\\bin</DCC_ExeOutput>\r\n        <DCC_DcuOutput>.\\dcu</DCC_DcuOutput>\r\n        <DCC_ObjOutput>.\\dcu</DCC_ObjOutput>\r\n        <DCC_HppOutput>.\\dcu</DCC_HppOutput>\r\n        <DCC_ResourcePath>D:\\Documents\\Desktop\\YxdJson\\source;D:\\Documents\\Desktop\\YxdIocp\\YxdIocp\\source\\IOCP;D:\\Documents\\Desktop\\YxdWorker\\YxdWorker\\source;$(DCC_ResourcePath)</DCC_ResourcePath>\r\n        <DCC_ObjPath>D:\\Documents\\Desktop\\YxdJson\\source;D:\\Documents\\Desktop\\YxdIocp\\YxdIocp\\source\\IOCP;D:\\Documents\\Desktop\\YxdWorker\\YxdWorker\\source;$(DCC_ObjPath)</DCC_ObjPath>\r\n        <DCC_IncludePath>D:\\Documents\\Desktop\\YxdJson\\source;D:\\Documents\\Desktop\\YxdIocp\\YxdIocp\\source\\IOCP;D:\\Documents\\Desktop\\YxdWorker\\YxdWorker\\source;$(DCC_IncludePath)</DCC_IncludePath>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Cfg_2_Win32)'!=''\">\r\n        <AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>\r\n        <AppEnableHighDPI>true</AppEnableHighDPI>\r\n        <VerInfo_Keys>CompanyName=YangYxd;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=阅读书源管理工具;ProgramID=com.embarcadero.$(MSBuildProjectName)</VerInfo_Keys>\r\n        <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>\r\n        <VerInfo_Locale>1033</VerInfo_Locale>\r\n        <VerInfo_AutoGenVersion>true</VerInfo_AutoGenVersion>\r\n    </PropertyGroup>\r\n    <PropertyGroup Condition=\"'$(Cfg_2_Win64)'!=''\">\r\n        <AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>\r\n        <AppEnableHighDPI>true</AppEnableHighDPI>\r\n        <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>\r\n        <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>\r\n        <VerInfo_Locale>1033</VerInfo_Locale>\r\n        <Icon_MainIcon>doc\\Main.ico</Icon_MainIcon>\r\n    </PropertyGroup>\r\n    <ProjectExtensions>\r\n        <Borland.Personality>Delphi.Personality.12</Borland.Personality>\r\n        <Borland.ProjectType/>\r\n        <BorlandProject>\r\n            <Delphi.Personality>\r\n                <Parameters>\r\n                    <Parameters Name=\"UseLauncher\">False</Parameters>\r\n                    <Parameters Name=\"LoadAllSymbols\">True</Parameters>\r\n                    <Parameters Name=\"LoadUnspecifiedSymbols\">False</Parameters>\r\n                </Parameters>\r\n                <VersionInfo>\r\n                    <VersionInfo Name=\"IncludeVerInfo\">False</VersionInfo>\r\n                    <VersionInfo Name=\"AutoIncBuild\">False</VersionInfo>\r\n                    <VersionInfo Name=\"MajorVer\">1</VersionInfo>\r\n                    <VersionInfo Name=\"MinorVer\">0</VersionInfo>\r\n                    <VersionInfo Name=\"Release\">0</VersionInfo>\r\n                    <VersionInfo Name=\"Build\">0</VersionInfo>\r\n                    <VersionInfo Name=\"Debug\">False</VersionInfo>\r\n                    <VersionInfo Name=\"PreRelease\">False</VersionInfo>\r\n                    <VersionInfo Name=\"Special\">False</VersionInfo>\r\n                    <VersionInfo Name=\"Private\">False</VersionInfo>\r\n                    <VersionInfo Name=\"DLL\">False</VersionInfo>\r\n                    <VersionInfo Name=\"Locale\">2052</VersionInfo>\r\n                    <VersionInfo Name=\"CodePage\">936</VersionInfo>\r\n                </VersionInfo>\r\n                <VersionInfoKeys>\r\n                    <VersionInfoKeys Name=\"CompanyName\"/>\r\n                    <VersionInfoKeys Name=\"FileDescription\"/>\r\n                    <VersionInfoKeys Name=\"FileVersion\">1.0.0.0</VersionInfoKeys>\r\n                    <VersionInfoKeys Name=\"InternalName\"/>\r\n                    <VersionInfoKeys Name=\"LegalCopyright\"/>\r\n                    <VersionInfoKeys Name=\"LegalTrademarks\"/>\r\n                    <VersionInfoKeys Name=\"OriginalFilename\"/>\r\n                    <VersionInfoKeys Name=\"ProductName\"/>\r\n                    <VersionInfoKeys Name=\"ProductVersion\">1.0.0.0</VersionInfoKeys>\r\n                    <VersionInfoKeys Name=\"Comments\"/>\r\n                </VersionInfoKeys>\r\n                <Excluded_Packages>\r\n                    <Excluded_Packages Name=\"$(BDSBIN)\\gitide250.bpl\">Embarcadero Git Integration</Excluded_Packages>\r\n                    <Excluded_Packages Name=\"$(BDSBIN)\\dcloffice2k250.bpl\">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>\r\n                    <Excluded_Packages Name=\"$(BDSBIN)\\dclofficexp250.bpl\">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>\r\n                </Excluded_Packages>\r\n                <Source>\r\n                    <Source Name=\"MainSource\">BookSourceMgr.dpr</Source>\r\n                </Source>\r\n            </Delphi.Personality>\r\n            <Platforms>\r\n                <Platform value=\"Win32\">True</Platform>\r\n                <Platform value=\"Win64\">True</Platform>\r\n            </Platforms>\r\n            <Deployment Version=\"3\">\r\n                <DeployFile LocalName=\"bin\\BookSourceMgr.exe\" Configuration=\"Debug\" Class=\"ProjectOutput\">\r\n                    <Platform Name=\"Win32\">\r\n                        <RemoteName>BookSourceMgr.exe</RemoteName>\r\n                        <Overwrite>true</Overwrite>\r\n                    </Platform>\r\n                </DeployFile>\r\n                <DeployClass Name=\"AdditionalDebugSymbols\">\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\MacOS</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Win32\">\r\n                        <RemoteDir>Contents\\MacOS</RemoteDir>\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"AndroidClassesDexFile\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>classes</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"AndroidGDBServer\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>library\\lib\\armeabi-v7a</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"AndroidLibnativeArmeabiFile\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>library\\lib\\armeabi</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"AndroidLibnativeMipsFile\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>library\\lib\\mips</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"AndroidServiceOutput\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>library\\lib\\armeabi-v7a</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"AndroidSplashImageDef\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"AndroidSplashStyles\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\values</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_DefaultAppIcon\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_LauncherIcon144\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-xxhdpi</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_LauncherIcon36\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-ldpi</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_LauncherIcon48\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-mdpi</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_LauncherIcon72\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-hdpi</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_LauncherIcon96\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-xhdpi</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_SplashImage426\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-small</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_SplashImage470\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-normal</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_SplashImage640\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-large</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"Android_SplashImage960\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>res\\drawable-xlarge</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"DebugSymbols\">\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\MacOS</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Win32\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"DependencyFramework\">\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\MacOS</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.framework</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"Win32\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"DependencyModule\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\MacOS</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"Win32\">\r\n                        <Operation>0</Operation>\r\n                        <Extensions>.dll;.bpl</Extensions>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Required=\"true\" Name=\"DependencyPackage\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\MacOS</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                        <Extensions>.dylib</Extensions>\r\n                    </Platform>\r\n                    <Platform Name=\"Win32\">\r\n                        <Operation>0</Operation>\r\n                        <Extensions>.bpl</Extensions>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"File\">\r\n                    <Platform Name=\"Android\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\Resources\\StartUp\\</RemoteDir>\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Win32\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"iPad_Launch1024\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"iPad_Launch1536\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"iPad_Launch2048\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"iPad_Launch768\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"iPhone_Launch320\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"iPhone_Launch640\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"iPhone_Launch640x1136\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectAndroidManifest\">\r\n                    <Platform Name=\"Android\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectiOSDeviceDebug\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <RemoteDir>..\\$(PROJECTNAME).app.dSYM\\Contents\\Resources\\DWARF</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <RemoteDir>..\\$(PROJECTNAME).app.dSYM\\Contents\\Resources\\DWARF</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectiOSDeviceResourceRules\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectiOSEntitlements\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <RemoteDir>..\\</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <RemoteDir>..\\</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectiOSInfoPList\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectiOSResource\">\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectOSXEntitlements\">\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>..\\</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectOSXInfoPList\">\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectOSXResource\">\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\Resources</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Required=\"true\" Name=\"ProjectOutput\">\r\n                    <Platform Name=\"Android\">\r\n                        <RemoteDir>library\\lib\\armeabi-v7a</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSDevice64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"iOSSimulator\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Linux64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"OSX32\">\r\n                        <RemoteDir>Contents\\MacOS</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Win32\">\r\n                        <Operation>0</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"ProjectUWPManifest\">\r\n                    <Platform Name=\"Win32\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Win64\">\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"UWP_DelphiLogo150\">\r\n                    <Platform Name=\"Win32\">\r\n                        <RemoteDir>Assets</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Win64\">\r\n                        <RemoteDir>Assets</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <DeployClass Name=\"UWP_DelphiLogo44\">\r\n                    <Platform Name=\"Win32\">\r\n                        <RemoteDir>Assets</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                    <Platform Name=\"Win64\">\r\n                        <RemoteDir>Assets</RemoteDir>\r\n                        <Operation>1</Operation>\r\n                    </Platform>\r\n                </DeployClass>\r\n                <ProjectRoot Platform=\"iOSDevice64\" Name=\"$(PROJECTNAME).app\"/>\r\n                <ProjectRoot Platform=\"Win64\" Name=\"$(PROJECTNAME)\"/>\r\n                <ProjectRoot Platform=\"iOSDevice32\" Name=\"$(PROJECTNAME).app\"/>\r\n                <ProjectRoot Platform=\"Linux64\" Name=\"$(PROJECTNAME)\"/>\r\n                <ProjectRoot Platform=\"Win32\" Name=\"$(PROJECTNAME)\"/>\r\n                <ProjectRoot Platform=\"OSX32\" Name=\"$(PROJECTNAME).app\"/>\r\n                <ProjectRoot Platform=\"Android\" Name=\"$(PROJECTNAME)\"/>\r\n                <ProjectRoot Platform=\"iOSSimulator\" Name=\"$(PROJECTNAME).app\"/>\r\n            </Deployment>\r\n        </BorlandProject>\r\n        <ProjectFileVersion>12</ProjectFileVersion>\r\n    </ProjectExtensions>\r\n    <ItemGroup>\r\n        <DelphiCompile Include=\"$(MainSource)\">\r\n            <MainSource>MainSource</MainSource>\r\n        </DelphiCompile>\r\n        <DCCReference Include=\"uFrmMain.pas\">\r\n            <Form>Form1</Form>\r\n        </DCCReference>\r\n        <DCCReference Include=\"uFrmWait.pas\">\r\n            <Form>Form2</Form>\r\n            <FormType>dfm</FormType>\r\n        </DCCReference>\r\n        <DCCReference Include=\"uBookSourceBean.pas\"/>\r\n        <DCCReference Include=\"uFrmEditSource.pas\">\r\n            <Form>frmEditSource</Form>\r\n            <FormType>dfm</FormType>\r\n        </DCCReference>\r\n        <DCCReference Include=\"uFrmReplaceGroup.pas\">\r\n            <Form>frmReplaceGroup</Form>\r\n            <FormType>dfm</FormType>\r\n        </DCCReference>\r\n        <BuildConfiguration Include=\"Debug\">\r\n            <Key>Cfg_2</Key>\r\n            <CfgParent>Base</CfgParent>\r\n        </BuildConfiguration>\r\n        <BuildConfiguration Include=\"Base\">\r\n            <Key>Base</Key>\r\n        </BuildConfiguration>\r\n        <BuildConfiguration Include=\"Release\">\r\n            <Key>Cfg_1</Key>\r\n            <CfgParent>Base</CfgParent>\r\n        </BuildConfiguration>\r\n    </ItemGroup>\r\n    <ItemGroup/>\r\n    <Import Project=\"$(BDS)\\Bin\\CodeGear.Delphi.Targets\"/>\r\n    <Import Project=\"$(BDS)\\Bin\\CodeGear.Delphi.Targets\" Condition=\"Exists('$(BDS)\\Bin\\CodeGear.Delphi.Targets')\"/>\r\n    <Import Project=\"$(APPDATA)\\Embarcadero\\$(BDSAPPDATABASEDIR)\\$(PRODUCTVERSION)\\UserTools.proj\" Condition=\"Exists('$(APPDATA)\\Embarcadero\\$(BDSAPPDATABASEDIR)\\$(PRODUCTVERSION)\\UserTools.proj')\"/>\r\n    <Import Project=\"$(MSBuildProjectName).deployproj\" Condition=\"Exists('$(MSBuildProjectName).deployproj')\"/>\r\n</Project>\r\n"
  },
  {
    "path": "tool/书源整理工具/ReadMe.txt",
    "content": "õ SynEdit\r\n\r\nhttps://github.com/SynEdit/SynEdit"
  },
  {
    "path": "tool/书源整理工具/uBookSourceBean.pas",
    "content": "unit uBookSourceBean;\r\n\r\ninterface\r\n\r\nuses\r\n  YxdJson, Classes, SysUtils, Math;\r\n\r\ntype\r\n  TBookSourceItem = class(JSONObject)\r\n  private\r\n    function GetIndexValue(const Index: Integer): string;\r\n    procedure SetIndexValue(const Index: Integer; const Value: string);\r\n    function GetEnable: Boolean;\r\n    function GetSerialNumber: Integer;\r\n    function GetWeight: Integer;\r\n    procedure SetEnable(const Value: Boolean);\r\n    procedure SetSerialNumber(const Value: Integer);\r\n    procedure SetWeight(const Value: Integer);\r\n  public\r\n    procedure AddGroup(const Name: string);\r\n    procedure RemoveGroup(const Name: string);\r\n    procedure ReplaceGroup(const Name, NewName: string);\r\n    function GetGroupList(): TArray<string>;\r\n\r\n    property bookSourceGroup: string index 0 read GetIndexValue write SetIndexValue;  // Դ\r\n    property bookSourceName: string index 1 read GetIndexValue write SetIndexValue;   // Դ\r\n    property bookSourceUrl: string index 2 read GetIndexValue write SetIndexValue;    // ԴURL\r\n    property httpUserAgent: string index 3 read GetIndexValue write SetIndexValue;    // HttpUserAgent\r\n    property loginUrl: string index 4 read GetIndexValue write SetIndexValue;         // ¼URL\r\n    property ruleBookAuthor: string index 5 read GetIndexValue write SetIndexValue;   // ߹\r\n    property ruleBookContent: string index 6 read GetIndexValue write SetIndexValue;  // Ĺ\r\n    property ruleBookKind: string index 7 read GetIndexValue write SetIndexValue;     // \r\n    property ruleBookLastChapter: string index 8 read GetIndexValue write SetIndexValue;  // ½ڹ\r\n    property ruleBookName: string index 9 read GetIndexValue write SetIndexValue;     // \r\n    property ruleBookUrlPattern: string index 10 read GetIndexValue write SetIndexValue;  // 鼮URL\r\n    property ruleChapterList: string index 11 read GetIndexValue write SetIndexValue;   // Ŀ¼б\r\n    property ruleChapterName: string index 12 read GetIndexValue write SetIndexValue;   // ½ƹ\r\n    property ruleChapterUrl: string index 13 read GetIndexValue write SetIndexValue;    // Ŀ¼URL\r\n    property ruleChapterUrlNext: string index 14 read GetIndexValue write SetIndexValue;  // Ŀ¼һҳUrl\r\n    property ruleContentUrl: string index 15 read GetIndexValue write SetIndexValue;      // ½URL\r\n    property ruleContentUrlNext: string index 16 read GetIndexValue write SetIndexValue;  // һҳURL\r\n    property ruleCoverUrl: string index 17 read GetIndexValue write SetIndexValue;     // \r\n    property ruleFindUrl: string index 18 read GetIndexValue write SetIndexValue;      // ֹ\r\n    property ruleIntroduce: string index 19 read GetIndexValue write SetIndexValue;      // \r\n    property ruleSearchAuthor: string index 20 read GetIndexValue write SetIndexValue;    // ߹\r\n    property ruleSearchCoverUrl: string index 21 read GetIndexValue write SetIndexValue;    // \r\n    property ruleSearchKind: string index 22 read GetIndexValue write SetIndexValue;         // \r\n    property ruleSearchLastChapter: string index 23 read GetIndexValue write SetIndexValue;  // ½ڹ\r\n    property ruleSearchList: string index 24 read GetIndexValue write SetIndexValue;   // б\r\n    property ruleSearchName: string index 25 read GetIndexValue write SetIndexValue;   // \r\n    property ruleSearchNoteUrl: string index 26 read GetIndexValue write SetIndexValue;  // 鼮URL\r\n    property ruleSearchUrl: string index 27 read GetIndexValue write SetIndexValue;    // ַ\r\n\r\n    property enable: Boolean read GetEnable write SetEnable;\r\n    property serialNumber: Integer read GetSerialNumber write SetSerialNumber;\r\n    property weight: Integer read GetWeight write SetWeight;\r\n  end;\r\n\r\nimplementation\r\n\r\n{ TBookSourceItem }\r\n\r\nconst\r\n  SKeyArray: array [0..27] of string = (\r\n    'bookSourceGroup',\r\n    'bookSourceName',\r\n    'bookSourceUrl',\r\n    'httpUserAgent',\r\n    'loginUrl',\r\n    'ruleBookAuthor',\r\n    'ruleBookContent',\r\n    'ruleBookKind',\r\n    'ruleBookLastChapter',\r\n    'ruleBookName',\r\n    'ruleBookUrlPattern',\r\n    'ruleChapterList',\r\n    'ruleChapterName',\r\n    'ruleChapterUrl',\r\n    'ruleChapterUrlNext',\r\n    'ruleContentUrl',\r\n    'ruleContentUrlNext',\r\n    'ruleCoverUrl',\r\n    'ruleFindUrl',\r\n    'ruleIntroduce',\r\n    'ruleSearchAuthor',\r\n    'ruleSearchCoverUrl',\r\n    'ruleSearchKind',\r\n    'ruleSearchLastChapter',\r\n    'ruleSearchList',\r\n    'ruleSearchName',\r\n    'ruleSearchNoteUrl',\r\n    'ruleSearchUrl'\r\n  );\r\n  SEnabled = 'enable';\r\n  SSerialNumber = 'serialNumber';\r\n  SWeight = 'weight';\r\n\r\nprocedure TBookSourceItem.AddGroup(const Name: string);\r\nvar\r\n  S: string;\r\n  List: TArray<string>;\r\n  I: Integer;\r\nbegin\r\n  S := Trim(bookSourceGroup);\r\n  if S = '' then\r\n    bookSourceGroup := Name\r\n  else begin\r\n    List := GetGroupList();\r\n    for I := Low(List) to High(List) do begin\r\n      if Trim(List[I]) = Name then\r\n        Exit;\r\n    end;\r\n    bookSourceGroup := bookSourceGroup + '; ' + Name;\r\n  end;\r\nend;\r\n\r\nfunction TBookSourceItem.GetEnable: Boolean;\r\nbegin\r\n  Result := Self.B[SEnabled];\r\nend;\r\n\r\nfunction TBookSourceItem.GetGroupList: TArray<string>;\r\nvar\r\n  S: string;\r\nbegin\r\n  S := Trim(bookSourceGroup);\r\n  Result := S.Split([',', ';', ':', '', '']);\r\nend;\r\n\r\nfunction TBookSourceItem.GetIndexValue(const Index: Integer): string;\r\nbegin\r\n  Result := Self.S[SKeyArray[Index]];\r\nend;\r\n\r\nfunction TBookSourceItem.GetSerialNumber: Integer;\r\nbegin\r\n  Result := Self.I[SSerialNumber];\r\nend;\r\n\r\nfunction TBookSourceItem.GetWeight: Integer;\r\nbegin\r\n  Result := SElf.I[SWeight];\r\nend;\r\n\r\nprocedure TBookSourceItem.RemoveGroup(const Name: string);\r\nvar\r\n  S: string;\r\n  List: TArray<string>;\r\n  I, J: Integer;\r\n  SB: TStringBuilder;\r\nbegin\r\n  S := Trim(bookSourceGroup);\r\n  if S <> '' then begin\r\n    J := 0;\r\n    List := GetGroupList();\r\n    SB := TStringBuilder.Create(Length(bookSourceGroup) * 2);\r\n    for I := Low(List) to High(List) do begin\r\n      if Trim(List[I]) <> Name then begin\r\n        if J > 0 then\r\n          SB.Append('; ');\r\n        SB.Append(Trim(List[I]));\r\n        Inc(J);\r\n      end;\r\n    end;\r\n    bookSourceGroup := SB.ToString;\r\n  end;\r\nend;\r\n\r\nprocedure TBookSourceItem.ReplaceGroup(const Name, NewName: string);\r\nvar\r\n  S: string;\r\n  List: TArray<string>;\r\n  I, J: Integer;\r\n  SB: TStringBuilder;\r\nbegin\r\n  S := Trim(bookSourceGroup);\r\n  if S <> '' then begin\r\n    J := 0;\r\n    List := GetGroupList();\r\n    SB := TStringBuilder.Create(Length(bookSourceGroup) * 2);\r\n    for I := Low(List) to High(List) do begin\r\n      if Trim(List[I]) <> Name then begin\r\n        if J > 0 then\r\n          SB.Append('; ');\r\n        SB.Append(Trim(List[I]));\r\n        Inc(J);\r\n      end else if NewName <> '' then begin\r\n        if J > 0 then\r\n          SB.Append('; ');\r\n        SB.Append(Trim(NewName));\r\n        Inc(J);\r\n      end;\r\n    end;\r\n    bookSourceGroup := SB.ToString;\r\n  end;\r\nend;\r\n\r\nprocedure TBookSourceItem.SetEnable(const Value: Boolean);\r\nbegin\r\n  Self.B[SEnabled] := Value;\r\nend;\r\n\r\nprocedure TBookSourceItem.SetIndexValue(const Index: Integer;\r\n  const Value: string);\r\nbegin\r\n  Self.S[SKeyArray[Index]] := Value;\r\nend;\r\n\r\nprocedure TBookSourceItem.SetSerialNumber(const Value: Integer);\r\nbegin\r\n  Self.I[SSerialNumber] := Value;\r\nend;\r\n\r\nprocedure TBookSourceItem.SetWeight(const Value: Integer);\r\nbegin\r\n  Self.I[SWeight] := Value;\r\nend;\r\n\r\nend.\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmEditSource.dfm",
    "content": "object frmEditSource: TfrmEditSource\r\n  Left = 0\r\n  Top = 0\r\n  BorderIcons = [biSystemMenu]\r\n  Caption = #32534#36753#20070#28304\r\n  ClientHeight = 743\r\n  ClientWidth = 1198\r\n  Color = clBtnFace\r\n  Font.Charset = DEFAULT_CHARSET\r\n  Font.Color = clWindowText\r\n  Font.Height = -12\r\n  Font.Name = 'Courier New'\r\n  Font.Style = []\r\n  OldCreateOrder = False\r\n  Position = poScreenCenter\r\n  OnClose = FormClose\r\n  OnCreate = FormCreate\r\n  OnMouseWheel = FormMouseWheel\r\n  OnResize = FormResize\r\n  OnShow = FormShow\r\n  PixelsPerInch = 96\r\n  TextHeight = 15\r\n  object ScrollBox1: TScrollBox\r\n    Left = 0\r\n    Top = 0\r\n    Width = 1198\r\n    Height = 700\r\n    HorzScrollBar.Smooth = True\r\n    HorzScrollBar.Style = ssFlat\r\n    VertScrollBar.Smooth = True\r\n    VertScrollBar.Style = ssFlat\r\n    VertScrollBar.Tracking = True\r\n    Align = alClient\r\n    BevelInner = bvNone\r\n    BevelOuter = bvNone\r\n    BorderStyle = bsNone\r\n    Color = clWindow\r\n    Padding.Bottom = 8\r\n    ParentColor = False\r\n    TabOrder = 0\r\n    DesignSize = (\r\n      1181\r\n      700)\r\n    object Label1: TLabel\r\n      Left = 603\r\n      Top = 13\r\n      Width = 184\r\n      Height = 12\r\n      Caption = #20998#32452#21517#31216#65306'(bookSourceGroup)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label2: TLabel\r\n      Left = 8\r\n      Top = 679\r\n      Width = 156\r\n      Height = 12\r\n      Caption = #21457#29616#35268#21017#65306'(ruleFindUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label3: TLabel\r\n      Left = 8\r\n      Top = 13\r\n      Width = 177\r\n      Height = 12\r\n      Caption = #20070#28304#21517#31216#65306'(bookSourceName)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clRed\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label4: TLabel\r\n      Left = 8\r\n      Top = 40\r\n      Width = 165\r\n      Height = 12\r\n      Caption = #20070#28304'URL'#65306'(bookSourceUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clRed\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label5: TLabel\r\n      Left = 8\r\n      Top = 66\r\n      Width = 114\r\n      Height = 12\r\n      Caption = #30331#24405'URL'#65306'(loginUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clTeal\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label6: TLabel\r\n      Left = 8\r\n      Top = 91\r\n      Width = 170\r\n      Height = 12\r\n      Caption = #25628#32034#22320#22336#65306'(ruleSearchUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label7: TLabel\r\n      Left = 8\r\n      Top = 117\r\n      Width = 229\r\n      Height = 12\r\n      Caption = #25628#32034#32467#26524#21015#34920#35268#21017#65306'(ruleSearchList)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label8: TLabel\r\n      Left = 8\r\n      Top = 142\r\n      Width = 229\r\n      Height = 12\r\n      Caption = #25628#32034#32467#26524#20070#21517#35268#21017#65306'(ruleSearchName)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label9: TLabel\r\n      Left = 8\r\n      Top = 168\r\n      Width = 243\r\n      Height = 12\r\n      Caption = #25628#32034#32467#26524#20316#32773#35268#21017#65306'(ruleSearchAuthor)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label10: TLabel\r\n      Left = 8\r\n      Top = 193\r\n      Width = 229\r\n      Height = 12\r\n      Caption = #25628#32034#32467#26524#20998#31867#35268#21017#65306'(ruleSearchKind)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label11: TLabel\r\n      Left = 8\r\n      Top = 219\r\n      Width = 270\r\n      Height = 12\r\n      Caption = #25628#32034#32467#26524#26368#26032#31456#33410#35268#21017#65306'(ruleSearchLastChapter)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label12: TLabel\r\n      Left = 8\r\n      Top = 244\r\n      Width = 257\r\n      Height = 12\r\n      Caption = #25628#32034#32467#26524#23553#38754#35268#21017#65306'(ruleSearchCoverUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label13: TLabel\r\n      Left = 8\r\n      Top = 270\r\n      Width = 271\r\n      Height = 12\r\n      Caption = #25628#32034#32467#26524#20070#31821'URL'#35268#21017#65306'(ruleSearchNoteUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clGreen\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label14: TLabel\r\n      Left = 8\r\n      Top = 295\r\n      Width = 222\r\n      Height = 12\r\n      Caption = #20070#31821#35814#24773'URL'#27491#21017#65306'(ruleBookUrlPattern)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label15: TLabel\r\n      Left = 8\r\n      Top = 321\r\n      Width = 144\r\n      Height = 12\r\n      Caption = #20070#21517#35268#21017#65306'(ruleBookName)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label16: TLabel\r\n      Left = 8\r\n      Top = 346\r\n      Width = 156\r\n      Height = 12\r\n      Caption = #20316#32773#35268#21017#65306'(ruleBookAuthor)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label17: TLabel\r\n      Left = 8\r\n      Top = 372\r\n      Width = 144\r\n      Height = 12\r\n      Caption = #23553#38754#35268#21017#65306'(ruleCoverUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label18: TLabel\r\n      Left = 8\r\n      Top = 397\r\n      Width = 144\r\n      Height = 12\r\n      Caption = #20998#31867#35268#21017#65306'(ruleBookKind)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label19: TLabel\r\n      Left = 8\r\n      Top = 423\r\n      Width = 210\r\n      Height = 12\r\n      Caption = #26368#26032#31456#33410#35268#21017#65306'(ruleBookLastChapter)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label20: TLabel\r\n      Left = 8\r\n      Top = 448\r\n      Width = 170\r\n      Height = 12\r\n      Caption = #31616#20171#35268#21017#65306'(ruleIntroduce)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label21: TLabel\r\n      Left = 8\r\n      Top = 474\r\n      Width = 198\r\n      Height = 12\r\n      Caption = #30446#24405'URL'#35268#21017#65306'(ruleChapterUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label22: TLabel\r\n      Left = 8\r\n      Top = 499\r\n      Width = 210\r\n      Height = 12\r\n      Caption = #30446#24405#21015#34920#35268#21017#65306'(ruleChapterList)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label23: TLabel\r\n      Left = 8\r\n      Top = 525\r\n      Width = 234\r\n      Height = 12\r\n      Caption = #30446#24405#19979#19968#39029'URL'#35268#21017#65306'(ruleChapterUrlNext)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label24: TLabel\r\n      Left = 8\r\n      Top = 550\r\n      Width = 210\r\n      Height = 12\r\n      Caption = #31456#33410#21517#31216#35268#21017#65306'(ruleChapterName)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clNavy\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label25: TLabel\r\n      Left = 8\r\n      Top = 576\r\n      Width = 224\r\n      Height = 12\r\n      Caption = #27491#25991#31456#33410'URL'#35268#21017#65306'(ruleContentUrl)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label26: TLabel\r\n      Left = 8\r\n      Top = 601\r\n      Width = 234\r\n      Height = 12\r\n      Caption = #27491#25991#19979#19968#39029'URL'#35268#21017#65306'(ruleContentUrlNext)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label27: TLabel\r\n      Left = 8\r\n      Top = 627\r\n      Width = 184\r\n      Height = 12\r\n      Caption = #27491#25991#35268#21017#65306'(ruleBookContent)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clNavy\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = [fsBold]\r\n      ParentFont = False\r\n    end\r\n    object Label28: TLabel\r\n      Left = 9\r\n      Top = 653\r\n      Width = 162\r\n      Height = 12\r\n      Caption = #27983#35272#22120#27169#25311#65306'(HttpUserAgent)'\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clTeal\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object ComboBox1: TComboBox\r\n      Left = 793\r\n      Top = 8\r\n      Width = 383\r\n      Height = 23\r\n      Hint = 'bookSourceGroup'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      DropDownCount = 20\r\n      TabOrder = 1\r\n    end\r\n    object Memo1: TMemo\r\n      Left = 296\r\n      Top = 674\r\n      Width = 881\r\n      Height = 200\r\n      Hint = 'ruleFindUrl'\r\n      Margins.Bottom = 8\r\n      Anchors = [akLeft, akTop, akRight]\r\n      ScrollBars = ssVertical\r\n      TabOrder = 27\r\n    end\r\n    object Edit1: TEdit\r\n      Left = 295\r\n      Top = 8\r\n      Width = 290\r\n      Height = 23\r\n      Hint = 'bookSourceName'\r\n      TabOrder = 0\r\n      TextHint = #24517#22635\r\n    end\r\n    object Edit2: TEdit\r\n      Left = 295\r\n      Top = 35\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'bookSourceUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 2\r\n      TextHint = #24517#22635\r\n    end\r\n    object Edit3: TEdit\r\n      Left = 295\r\n      Top = 61\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'loginUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 3\r\n    end\r\n    object Edit4: TEdit\r\n      Left = 295\r\n      Top = 86\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 4\r\n    end\r\n    object Edit5: TEdit\r\n      Left = 295\r\n      Top = 112\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchList'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 5\r\n    end\r\n    object Edit6: TEdit\r\n      Left = 295\r\n      Top = 137\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchName'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 6\r\n    end\r\n    object Edit7: TEdit\r\n      Left = 295\r\n      Top = 163\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchAuthor'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 7\r\n    end\r\n    object Edit8: TEdit\r\n      Left = 295\r\n      Top = 188\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchKind'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 8\r\n    end\r\n    object Edit9: TEdit\r\n      Left = 295\r\n      Top = 214\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchLastChapter'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 9\r\n    end\r\n    object Edit10: TEdit\r\n      Left = 295\r\n      Top = 239\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchCoverUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 10\r\n    end\r\n    object Edit11: TEdit\r\n      Left = 295\r\n      Top = 265\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleSearchNoteUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 11\r\n    end\r\n    object Edit12: TEdit\r\n      Left = 295\r\n      Top = 290\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleBookUrlPattern'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 12\r\n    end\r\n    object Edit13: TEdit\r\n      Left = 295\r\n      Top = 316\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleBookName'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 13\r\n    end\r\n    object Edit14: TEdit\r\n      Left = 295\r\n      Top = 341\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleBookAuthor'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 14\r\n    end\r\n    object Edit15: TEdit\r\n      Left = 295\r\n      Top = 367\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleCoverUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 15\r\n    end\r\n    object Edit16: TEdit\r\n      Left = 295\r\n      Top = 392\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleBookKind'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 16\r\n    end\r\n    object Edit17: TEdit\r\n      Left = 295\r\n      Top = 418\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleBookLastChapter'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 17\r\n    end\r\n    object Edit18: TEdit\r\n      Left = 295\r\n      Top = 443\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleIntroduce'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 18\r\n    end\r\n    object Edit19: TEdit\r\n      Left = 295\r\n      Top = 469\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleChapterUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 19\r\n    end\r\n    object Edit20: TEdit\r\n      Left = 295\r\n      Top = 494\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleChapterList'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 20\r\n    end\r\n    object Edit21: TEdit\r\n      Left = 295\r\n      Top = 520\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleChapterUrlNext'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 21\r\n    end\r\n    object Edit22: TEdit\r\n      Left = 295\r\n      Top = 545\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleChapterName'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 22\r\n    end\r\n    object Edit23: TEdit\r\n      Left = 295\r\n      Top = 571\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleContentUrl'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 23\r\n    end\r\n    object Edit24: TEdit\r\n      Left = 295\r\n      Top = 596\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleContentUrlNext'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 24\r\n    end\r\n    object Edit25: TEdit\r\n      Left = 295\r\n      Top = 622\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'ruleBookContent'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 25\r\n    end\r\n    object Edit26: TEdit\r\n      Left = 296\r\n      Top = 648\r\n      Width = 881\r\n      Height = 23\r\n      Hint = 'httpUserAgent'\r\n      Anchors = [akLeft, akTop, akRight]\r\n      TabOrder = 26\r\n    end\r\n  end\r\n  object Panel1: TPanel\r\n    Left = 0\r\n    Top = 700\r\n    Width = 1198\r\n    Height = 43\r\n    Align = alBottom\r\n    BevelOuter = bvNone\r\n    Font.Charset = DEFAULT_CHARSET\r\n    Font.Color = clWindowText\r\n    Font.Height = -12\r\n    Font.Name = #23435#20307\r\n    Font.Style = []\r\n    ParentFont = False\r\n    ShowCaption = False\r\n    TabOrder = 1\r\n    DesignSize = (\r\n      1198\r\n      43)\r\n    object Label29: TLabel\r\n      Left = 140\r\n      Top = 19\r\n      Width = 36\r\n      Height = 12\r\n      Caption = #26435#37325#65306\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clTeal\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Label30: TLabel\r\n      Left = 291\r\n      Top = 19\r\n      Width = 48\r\n      Height = 12\r\n      Caption = #24207#21015#21495#65306\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clTeal\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Style = []\r\n      ParentFont = False\r\n    end\r\n    object Button1: TButton\r\n      Left = 1077\r\n      Top = 10\r\n      Width = 107\r\n      Height = 25\r\n      Anchors = [akTop, akRight]\r\n      Caption = #30830#23450'(&O)'\r\n      Default = True\r\n      TabOrder = 0\r\n      OnClick = Button1Click\r\n    end\r\n    object Button2: TButton\r\n      Left = 949\r\n      Top = 10\r\n      Width = 107\r\n      Height = 25\r\n      Anchors = [akTop, akRight]\r\n      Cancel = True\r\n      Caption = #21462#28040'(&C)'\r\n      ModalResult = 2\r\n      TabOrder = 1\r\n      OnClick = Button2Click\r\n    end\r\n    object CheckBox1: TCheckBox\r\n      Left = 12\r\n      Top = 16\r\n      Width = 113\r\n      Height = 17\r\n      Caption = #21551#29992#20070#28304\r\n      TabOrder = 2\r\n    end\r\n    object Edit27: TEdit\r\n      Left = 178\r\n      Top = 14\r\n      Width = 87\r\n      Height = 20\r\n      Hint = 'weight'\r\n      NumbersOnly = True\r\n      ParentShowHint = False\r\n      ShowHint = True\r\n      TabOrder = 3\r\n      Text = '0'\r\n    end\r\n    object Edit28: TEdit\r\n      Left = 338\r\n      Top = 14\r\n      Width = 87\r\n      Height = 20\r\n      Hint = 'weight'\r\n      NumbersOnly = True\r\n      ParentShowHint = False\r\n      ShowHint = True\r\n      TabOrder = 4\r\n      Text = '0'\r\n    end\r\n    object Button3: TButton\r\n      Left = 793\r\n      Top = 10\r\n      Width = 107\r\n      Height = 25\r\n      Anchors = [akTop, akRight]\r\n      Caption = #27979#35797#20070#28304'(&T)'\r\n      ModalResult = 2\r\n      TabOrder = 5\r\n      OnClick = Button3Click\r\n    end\r\n  end\r\nend\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmEditSource.pas",
    "content": "unit uFrmEditSource;\r\n\r\ninterface\r\n\r\nuses\r\n  uBookSourceBean, YxdJson,\r\n  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,\r\n  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls;\r\n\r\ntype\r\n  TNotifyEventA = reference to procedure (Item: TBookSourceItem);\r\n\r\n  TfrmEditSource = class(TForm)\r\n    ScrollBox1: TScrollBox;\r\n    ComboBox1: TComboBox;\r\n    Label1: TLabel;\r\n    Label2: TLabel;\r\n    Memo1: TMemo;\r\n    Edit1: TEdit;\r\n    Label3: TLabel;\r\n    Label4: TLabel;\r\n    Edit2: TEdit;\r\n    Label5: TLabel;\r\n    Edit3: TEdit;\r\n    Label6: TLabel;\r\n    Edit4: TEdit;\r\n    Label7: TLabel;\r\n    Edit5: TEdit;\r\n    Label8: TLabel;\r\n    Edit6: TEdit;\r\n    Label9: TLabel;\r\n    Edit7: TEdit;\r\n    Label10: TLabel;\r\n    Edit8: TEdit;\r\n    Label11: TLabel;\r\n    Edit9: TEdit;\r\n    Label12: TLabel;\r\n    Edit10: TEdit;\r\n    Label13: TLabel;\r\n    Edit11: TEdit;\r\n    Label14: TLabel;\r\n    Edit12: TEdit;\r\n    Label15: TLabel;\r\n    Edit13: TEdit;\r\n    Label16: TLabel;\r\n    Edit14: TEdit;\r\n    Label17: TLabel;\r\n    Edit15: TEdit;\r\n    Label18: TLabel;\r\n    Edit16: TEdit;\r\n    Label19: TLabel;\r\n    Edit17: TEdit;\r\n    Label20: TLabel;\r\n    Edit18: TEdit;\r\n    Label21: TLabel;\r\n    Edit19: TEdit;\r\n    Label22: TLabel;\r\n    Edit20: TEdit;\r\n    Label23: TLabel;\r\n    Edit21: TEdit;\r\n    Label24: TLabel;\r\n    Edit22: TEdit;\r\n    Label25: TLabel;\r\n    Edit23: TEdit;\r\n    Label26: TLabel;\r\n    Edit24: TEdit;\r\n    Label27: TLabel;\r\n    Edit25: TEdit;\r\n    Edit26: TEdit;\r\n    Label28: TLabel;\r\n    Panel1: TPanel;\r\n    Button1: TButton;\r\n    Button2: TButton;\r\n    CheckBox1: TCheckBox;\r\n    Label29: TLabel;\r\n    Edit27: TEdit;\r\n    Label30: TLabel;\r\n    Edit28: TEdit;\r\n    Button3: TButton;\r\n    procedure FormShow(Sender: TObject);\r\n    procedure Button1Click(Sender: TObject);\r\n    procedure FormResize(Sender: TObject);\r\n    procedure FormClose(Sender: TObject; var Action: TCloseAction);\r\n    procedure Button2Click(Sender: TObject);\r\n    procedure FormCreate(Sender: TObject);\r\n    procedure Button3Click(Sender: TObject);\r\n    procedure FormMouseWheel(Sender: TObject; Shift: TShiftState;\r\n      WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);\r\n  private\r\n    { Private declarations }\r\n    FDisableChange: Boolean;\r\n  public\r\n    { Public declarations }\r\n    Data: TBookSourceItem;\r\n    CallBack: TNotifyEventA;\r\n\r\n    procedure ApplayEdit(Data: TBookSourceItem);\r\n  end;\r\n\r\nvar\r\n  frmEditSource: TfrmEditSource;\r\n\r\nprocedure ShowEditSource(Item: TBookSourceItem; CallBack: TNotifyEventA = nil);\r\n\r\nimplementation\r\n\r\n{$R *.dfm}\r\n\r\nuses\r\n  uFrmMain, Math;\r\n\r\nvar\r\n  LastW, LastH: Integer;\r\n\r\nprocedure ShowEditSource(Item: TBookSourceItem; CallBack: TNotifyEventA);\r\nvar\r\n  F: TfrmEditSource;\r\nbegin\r\n  F := TfrmEditSource.Create(Application);\r\n  F.Data := Item;\r\n  F.CallBack := CallBack;\r\n  F.Show;\r\nend;\r\n\r\nprocedure TfrmEditSource.ApplayEdit(Data: TBookSourceItem);\r\nvar\r\n  I: Integer;\r\n  Item: TControl;\r\nbegin\r\n  Data.enable := CheckBox1.Checked;\r\n  Data.weight := StrToIntDef(Edit27.Text, Data.weight);\r\n  Data.serialNumber := StrToIntDef(Edit28.Text, Data.serialNumber);\r\n  for I := 0 to ScrollBox1.ControlCount - 1 do begin\r\n    Item := ScrollBox1.Controls[I];\r\n    if not Item.Visible then Continue;\r\n    if Item.Hint = '' then Continue;\r\n\r\n    if Item is TEdit then\r\n      Data.S[Item.Hint] := TEdit(Item).Text\r\n    else if Item is TComboBox then\r\n      Data.S[Item.Hint] := TComboBox(Item).Text\r\n    else if Item is TMemo then\r\n      Data.S[Item.Hint] := TMemo(Item).Text;\r\n  end;\r\nend;\r\n\r\nprocedure TfrmEditSource.Button1Click(Sender: TObject);\r\nbegin\r\n  FDisableChange := True;\r\n  try\r\n    ApplayEdit(Data);\r\n  finally\r\n    FDisableChange := False;\r\n  end;\r\n\r\n  if Assigned(CallBack) then\r\n    CallBack(Data);\r\n  Close;\r\nend;\r\n\r\nprocedure TfrmEditSource.Button2Click(Sender: TObject);\r\nbegin\r\n  Close;\r\nend;\r\n\r\nprocedure TfrmEditSource.Button3Click(Sender: TObject);\r\nvar\r\n  Item: TBookSourceItem;\r\n  Msg: TStrings;\r\nbegin\r\n  Msg := TStringList.Create;\r\n  Item := TBookSourceItem(JSONObject.Create);\r\n  try\r\n    Item.Parse(Data.ToString());\r\n    ApplayEdit(Item);\r\n    if Form1.CheckBookSourceItem(Item, True, Msg) then\r\n      MessageBox(Handle, PChar(Msg.Text), 'ϲ, ͨ!', 64)\r\n    else\r\n      MessageBox(Handle, PChar(Msg.Text), 'Դ쳣', 48)\r\n  finally\r\n    FreeAndNil(Item);\r\n    FreeAndNil(Msg);\r\n  end;\r\nend;\r\n\r\nprocedure TfrmEditSource.FormClose(Sender: TObject; var Action: TCloseAction);\r\nbegin\r\n  Action := caFree;\r\nend;\r\n\r\nprocedure TfrmEditSource.FormCreate(Sender: TObject);\r\nbegin\r\n  if LastW <= 0 then LastW := Self.Width;\r\n  if LastH <= 0 then LastH := Self.Height;\r\n  Self.SetBounds(Left, Top, LastW, LastH);\r\nend;\r\n\r\nprocedure TfrmEditSource.FormMouseWheel(Sender: TObject; Shift: TShiftState;\r\n  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);\r\nbegin\r\n  if WheelDelta < 0 then\r\n    ScrollBox1.Perform(WM_VSCROLL,SB_LINEDOWN,0)\r\n  else\r\n    ScrollBox1.Perform(WM_VSCROLL,SB_LINEUP,0);\r\nend;\r\n\r\nprocedure TfrmEditSource.FormResize(Sender: TObject);\r\nbegin\r\n  LastW := Width;\r\n  LastH := Height;\r\nend;\r\n\r\nprocedure TfrmEditSource.FormShow(Sender: TObject);\r\nvar\r\n  I: Integer;\r\n  Item: TControl;\r\nbegin\r\n  ComboBox1.Items := Form1.bookGroupList.Items;\r\n\r\n  if Assigned(Data) then begin\r\n    FDisableChange := True;\r\n    try\r\n      CheckBox1.Checked := Data.enable;\r\n      Edit27.Text := IntToStr(Data.weight);\r\n      Edit28.Text := IntToStr(Data.serialNumber);\r\n      for I := 0 to ScrollBox1.ControlCount - 1 do begin\r\n        Item := ScrollBox1.Controls[I];\r\n        if not Item.Visible then Continue;\r\n        if Item.Hint = '' then Continue;\r\n\r\n        if Item is TEdit then\r\n          TEdit(Item).Text := Data.S[Item.Hint]\r\n        else if Item is TComboBox then\r\n          TComboBox(Item).Text := Data.S[Item.Hint]\r\n        else if Item is TMemo then\r\n          TMemo(Item).Text := Data.S[Item.Hint];\r\n      end;\r\n    finally\r\n      FDisableChange := False;\r\n    end;\r\n  end;\r\nend;\r\n\r\nend.\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmMain.dfm",
    "content": "object Form1: TForm1\r\n  Left = 0\r\n  Top = 0\r\n  Caption = #38405#35835#20070#28304#25972#29702#24037#20855\r\n  ClientHeight = 548\r\n  ClientWidth = 1145\r\n  Color = 15921906\r\n  DoubleBuffered = True\r\n  Font.Charset = DEFAULT_CHARSET\r\n  Font.Color = clWindowText\r\n  Font.Height = -11\r\n  Font.Name = 'Tahoma'\r\n  Font.Pitch = fpFixed\r\n  Font.Style = []\r\n  Menu = MainMenu1\r\n  OldCreateOrder = False\r\n  OnCreate = FormCreate\r\n  OnDestroy = FormDestroy\r\n  OnShow = FormShow\r\n  PixelsPerInch = 96\r\n  TextHeight = 13\r\n  object Splitter1: TSplitter\r\n    AlignWithMargins = True\r\n    Left = 313\r\n    Top = 37\r\n    Width = 4\r\n    Height = 480\r\n    Margins.Left = 0\r\n    Margins.Top = 37\r\n    Margins.Right = 0\r\n    Margins.Bottom = 0\r\n    Color = clSilver\r\n    ParentColor = False\r\n    ExplicitLeft = 257\r\n    ExplicitTop = 0\r\n    ExplicitHeight = 697\r\n  end\r\n  object Panel1: TPanel\r\n    Left = 0\r\n    Top = 0\r\n    Width = 313\r\n    Height = 517\r\n    Align = alLeft\r\n    BevelOuter = bvNone\r\n    Padding.Left = 4\r\n    Padding.Top = 4\r\n    Padding.Bottom = 4\r\n    TabOrder = 1\r\n    object SrcList: TListBox\r\n      AlignWithMargins = True\r\n      Left = 4\r\n      Top = 38\r\n      Width = 309\r\n      Height = 475\r\n      Hint = #23558#20070#28304#25991#20214#25302#20837#27492#22788\r\n      Margins.Left = 0\r\n      Margins.Top = 2\r\n      Margins.Right = 0\r\n      Margins.Bottom = 0\r\n      Style = lbVirtual\r\n      Align = alClient\r\n      BorderStyle = bsNone\r\n      DoubleBuffered = True\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -12\r\n      Font.Name = #23435#20307\r\n      Font.Pitch = fpFixed\r\n      Font.Style = []\r\n      ItemHeight = 18\r\n      MultiSelect = True\r\n      ParentDoubleBuffered = False\r\n      ParentFont = False\r\n      ParentShowHint = False\r\n      PopupMenu = PopupMenu1\r\n      ShowHint = True\r\n      TabOrder = 0\r\n      OnClick = SrcListClick\r\n      OnData = SrcListData\r\n      OnDblClick = SrcListDblClick\r\n      OnKeyDown = SrcListKeyDown\r\n    end\r\n    object Panel4: TPanel\r\n      Left = 4\r\n      Top = 4\r\n      Width = 309\r\n      Height = 32\r\n      Align = alTop\r\n      BevelOuter = bvNone\r\n      DoubleBuffered = True\r\n      ParentDoubleBuffered = False\r\n      TabOrder = 1\r\n      DesignSize = (\r\n        309\r\n        32)\r\n      object lbCount: TLabel\r\n        Left = 0\r\n        Top = 7\r\n        Width = 60\r\n        Height = 13\r\n        Caption = #20070#28304#21015#34920#65306\r\n        Transparent = False\r\n      end\r\n      object bookGroupList: TComboBox\r\n        Left = 80\r\n        Top = 3\r\n        Width = 223\r\n        Height = 21\r\n        Anchors = [akLeft, akTop, akRight]\r\n        DropDownCount = 24\r\n        Font.Charset = GB2312_CHARSET\r\n        Font.Color = clWindowText\r\n        Font.Height = -11\r\n        Font.Name = 'Tahoma'\r\n        Font.Pitch = fpFixed\r\n        Font.Style = []\r\n        ParentFont = False\r\n        TabOrder = 0\r\n        OnChange = bookGroupListChange\r\n        OnClick = bookGroupListChange\r\n      end\r\n    end\r\n    object StaticText1: TStaticText\r\n      AlignWithMargins = True\r\n      Left = 7\r\n      Top = 66\r\n      Width = 303\r\n      Height = 444\r\n      Margins.Top = 30\r\n      Align = alClient\r\n      Alignment = taCenter\r\n      Caption = #40736#26631#25302#20837#20070#28304#25991#20214#21040#27492#22788\r\n      Color = clWindow\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = 6736896\r\n      Font.Height = -16\r\n      Font.Name = #24494#36719#38597#40657\r\n      Font.Pitch = fpFixed\r\n      Font.Style = []\r\n      ParentColor = False\r\n      ParentFont = False\r\n      PopupMenu = PopupMenu1\r\n      TabOrder = 2\r\n      Transparent = False\r\n      ExplicitLeft = 4\r\n      ExplicitTop = 36\r\n      ExplicitWidth = 309\r\n      ExplicitHeight = 477\r\n    end\r\n  end\r\n  object Panel2: TPanel\r\n    Left = 317\r\n    Top = 0\r\n    Width = 828\r\n    Height = 517\r\n    Align = alClient\r\n    BevelOuter = bvNone\r\n    ParentBackground = False\r\n    ParentColor = True\r\n    TabOrder = 0\r\n    object Splitter2: TSplitter\r\n      Left = 0\r\n      Top = 372\r\n      Width = 828\r\n      Height = 4\r\n      Cursor = crVSplit\r\n      Align = alBottom\r\n      Color = clSilver\r\n      ParentColor = False\r\n      ExplicitTop = 373\r\n    end\r\n    object Panel3: TPanel\r\n      Left = 0\r\n      Top = 0\r\n      Width = 828\r\n      Height = 36\r\n      Align = alTop\r\n      BevelOuter = bvNone\r\n      TabOrder = 1\r\n      object Label1: TLabel\r\n        Left = 144\r\n        Top = 10\r\n        Width = 64\r\n        Height = 13\r\n        Caption = #24037#20316#32447#31243#25968':'\r\n        Transparent = False\r\n      end\r\n      object Button1: TButton\r\n        Left = 6\r\n        Top = 6\r\n        Width = 128\r\n        Height = 24\r\n        Caption = #24320#22987#22788#29702'(&B)'\r\n        TabOrder = 0\r\n        OnClick = Button1Click\r\n      end\r\n      object Edit1: TEdit\r\n        Left = 212\r\n        Top = 7\r\n        Width = 35\r\n        Height = 21\r\n        Alignment = taCenter\r\n        NumbersOnly = True\r\n        TabOrder = 1\r\n        Text = '60'\r\n      end\r\n      object CheckBox1: TCheckBox\r\n        Left = 264\r\n        Top = 9\r\n        Width = 97\r\n        Height = 17\r\n        Caption = #21435#38500#37325#22797\r\n        Checked = True\r\n        State = cbChecked\r\n        TabOrder = 2\r\n      end\r\n      object CheckBox3: TCheckBox\r\n        Left = 344\r\n        Top = 9\r\n        Width = 97\r\n        Height = 17\r\n        Hint = #21435#37325#26102#19981#26816#27979#20070#28304#21517#31216#21644#20998#32452\r\n        Caption = #30495#23454#21435#37325\r\n        Checked = True\r\n        State = cbChecked\r\n        TabOrder = 4\r\n      end\r\n      object CheckBox2: TCheckBox\r\n        Left = 429\r\n        Top = 9\r\n        Width = 97\r\n        Height = 17\r\n        Caption = #26657#39564#20070#28304\r\n        TabOrder = 3\r\n      end\r\n    end\r\n    object EditData: TSynMemo\r\n      Left = 0\r\n      Top = 36\r\n      Width = 828\r\n      Height = 336\r\n      Align = alClient\r\n      Ctl3D = True\r\n      ParentCtl3D = False\r\n      Font.Charset = DEFAULT_CHARSET\r\n      Font.Color = clWindowText\r\n      Font.Height = -13\r\n      Font.Name = 'Courier New'\r\n      Font.Style = []\r\n      PopupMenu = PopupMenu2\r\n      TabOrder = 0\r\n      CodeFolding.GutterShapeSize = 11\r\n      CodeFolding.CollapsedLineColor = clGrayText\r\n      CodeFolding.FolderBarLinesColor = clGrayText\r\n      CodeFolding.IndentGuidesColor = clGray\r\n      CodeFolding.IndentGuides = True\r\n      CodeFolding.ShowCollapsedLine = False\r\n      CodeFolding.ShowHintMark = True\r\n      UseCodeFolding = False\r\n      BorderStyle = bsNone\r\n      Gutter.AutoSize = True\r\n      Gutter.BorderStyle = gbsNone\r\n      Gutter.Color = cl3DLight\r\n      Gutter.BorderColor = clWindowFrame\r\n      Gutter.Font.Charset = DEFAULT_CHARSET\r\n      Gutter.Font.Color = clWindowText\r\n      Gutter.Font.Height = -11\r\n      Gutter.Font.Name = 'Courier New'\r\n      Gutter.Font.Style = []\r\n      Gutter.ShowLineNumbers = True\r\n      Highlighter = SynJSONSyn1\r\n      WordWrap = True\r\n      OnChange = EditDataChange\r\n      FontSmoothing = fsmNone\r\n      ExplicitHeight = 481\r\n    end\r\n    object Panel5: TPanel\r\n      Left = 0\r\n      Top = 376\r\n      Width = 828\r\n      Height = 141\r\n      Align = alBottom\r\n      BevelOuter = bvNone\r\n      TabOrder = 2\r\n      ExplicitLeft = 2\r\n      DesignSize = (\r\n        828\r\n        141)\r\n      object Label2: TLabel\r\n        Left = 6\r\n        Top = 2\r\n        Width = 36\r\n        Height = 13\r\n        Caption = #26085#24535#65306\r\n        Transparent = False\r\n      end\r\n      object SpeedButton1: TSpeedButton\r\n        Left = 783\r\n        Top = 0\r\n        Width = 43\r\n        Height = 20\r\n        Anchors = [akTop, akRight]\r\n        Caption = #28165#31354\r\n        Flat = True\r\n        Font.Charset = DEFAULT_CHARSET\r\n        Font.Color = clTeal\r\n        Font.Height = -11\r\n        Font.Name = 'Tahoma'\r\n        Font.Pitch = fpFixed\r\n        Font.Style = []\r\n        ParentFont = False\r\n        OnClick = SpeedButton1Click\r\n      end\r\n      object edtLog: TSynMemo\r\n        AlignWithMargins = True\r\n        Left = 0\r\n        Top = 20\r\n        Width = 828\r\n        Height = 121\r\n        Margins.Left = 0\r\n        Margins.Top = 20\r\n        Margins.Right = 0\r\n        Margins.Bottom = 0\r\n        Align = alClient\r\n        Ctl3D = False\r\n        ParentCtl3D = False\r\n        Font.Charset = DEFAULT_CHARSET\r\n        Font.Color = clWindowText\r\n        Font.Height = -12\r\n        Font.Name = 'Courier New'\r\n        Font.Style = []\r\n        TabOrder = 0\r\n        CodeFolding.GutterShapeSize = 11\r\n        CodeFolding.CollapsedLineColor = clGrayText\r\n        CodeFolding.FolderBarLinesColor = clGrayText\r\n        CodeFolding.IndentGuidesColor = clGray\r\n        CodeFolding.IndentGuides = False\r\n        CodeFolding.ShowCollapsedLine = False\r\n        CodeFolding.ShowHintMark = True\r\n        UseCodeFolding = False\r\n        BookMarkOptions.EnableKeys = False\r\n        BookMarkOptions.GlyphsVisible = False\r\n        BorderStyle = bsNone\r\n        Gutter.AutoSize = True\r\n        Gutter.BorderStyle = gbsNone\r\n        Gutter.Color = cl3DLight\r\n        Gutter.BorderColor = clWindowFrame\r\n        Gutter.Font.Charset = DEFAULT_CHARSET\r\n        Gutter.Font.Color = clWindowText\r\n        Gutter.Font.Height = -11\r\n        Gutter.Font.Name = 'Courier New'\r\n        Gutter.Font.Style = []\r\n        Gutter.ShowLineNumbers = True\r\n        Gutter.Visible = False\r\n        Gutter.Width = 0\r\n        Options = [eoScrollPastEol, eoShowScrollHint, eoSmartTabDelete, eoSmartTabs, eoTabsToSpaces]\r\n        ReadOnly = True\r\n        RightEdge = 0\r\n        WordWrap = True\r\n        OnChange = EditDataChange\r\n        FontSmoothing = fsmClearType\r\n        ExplicitTop = 36\r\n        ExplicitHeight = 481\r\n      end\r\n    end\r\n  end\r\n  object StatusBar1: TStatusBar\r\n    Left = 0\r\n    Top = 523\r\n    Width = 1145\r\n    Height = 25\r\n    Panels = <\r\n      item\r\n        Width = 500\r\n      end\r\n      item\r\n        Width = 50\r\n      end>\r\n  end\r\n  object ProgressBar1: TProgressBar\r\n    Left = 0\r\n    Top = 517\r\n    Width = 1145\r\n    Height = 6\r\n    Align = alBottom\r\n    Position = 100\r\n    TabOrder = 3\r\n    Visible = False\r\n  end\r\n  object PopupMenu1: TPopupMenu\r\n    Left = 136\r\n    Top = 192\r\n    object C3: TMenuItem\r\n      Caption = #22797#21046#26032#22686'(&A)'\r\n      OnClick = C3Click\r\n    end\r\n    object N7: TMenuItem\r\n      Caption = #26032#24314#20070#28304'(&N)...'\r\n      OnClick = N7Click\r\n    end\r\n    object E1: TMenuItem\r\n      Caption = #32534#36753#20070#28304'(&E)...'\r\n      OnClick = E1Click\r\n    end\r\n    object N5: TMenuItem\r\n      Caption = '-'\r\n    end\r\n    object S2: TMenuItem\r\n      Caption = #25490#24207' - '#20070#28304#21517#31216'(&S)'\r\n      OnClick = S2Click\r\n    end\r\n    object G1: TMenuItem\r\n      Caption = #25490#24207' - '#20998#32452'(&G)'\r\n      OnClick = G1Click\r\n    end\r\n    object N9: TMenuItem\r\n      Caption = '-'\r\n    end\r\n    object H2: TMenuItem\r\n      Caption = #20998#32452#21517#31216#26367#25442'(&H)...'\r\n      OnClick = H2Click\r\n    end\r\n    object N6: TMenuItem\r\n      Caption = '-'\r\n    end\r\n    object D1: TMenuItem\r\n      Caption = #21024#38500#36873#20013#39033'(&D)'\r\n      OnClick = D1Click\r\n    end\r\n    object C1: TMenuItem\r\n      Caption = #28165#31354'(&C)'\r\n      OnClick = C1Click\r\n    end\r\n  end\r\n  object SynJSONSyn1: TSynJSONSyn\r\n    Options.AutoDetectEnabled = False\r\n    Options.AutoDetectLineLimit = 0\r\n    Options.Visible = False\r\n    Left = 552\r\n    Top = 352\r\n  end\r\n  object PopupMenu2: TPopupMenu\r\n    OnPopup = PopupMenu2Popup\r\n    Left = 632\r\n    Top = 352\r\n    object S1: TMenuItem\r\n      Caption = #20445#23384#20462#25913'(&S)'\r\n      ShortCut = 16467\r\n      OnClick = S1Click\r\n    end\r\n    object T1: TMenuItem\r\n      Caption = #27979#35797#20070#28304'(&T)'\r\n      OnClick = T1Click\r\n    end\r\n    object N3: TMenuItem\r\n      Caption = '-'\r\n    end\r\n    object R1: TMenuItem\r\n      Caption = #25764#28040'(&R)'\r\n      OnClick = R1Click\r\n    end\r\n    object Z1: TMenuItem\r\n      Caption = #37325#20570'(&Z)'\r\n      OnClick = Z1Click\r\n    end\r\n    object N1: TMenuItem\r\n      Caption = '-'\r\n    end\r\n    object C2: TMenuItem\r\n      Caption = #22797#21046'(&C)'\r\n      OnClick = C2Click\r\n    end\r\n    object X1: TMenuItem\r\n      Caption = #21098#20999'(&X)'\r\n      OnClick = X1Click\r\n    end\r\n    object P1: TMenuItem\r\n      Caption = #31896#36148'(&P)'\r\n      OnClick = P1Click\r\n    end\r\n    object N2: TMenuItem\r\n      Caption = '-'\r\n    end\r\n    object A1: TMenuItem\r\n      Caption = #20840#36873'(&A)'\r\n      OnClick = A1Click\r\n    end\r\n    object N4: TMenuItem\r\n      Caption = '-'\r\n    end\r\n    object W1: TMenuItem\r\n      Caption = #33258#21160#25442#34892'(&W)'\r\n      OnClick = W1Click\r\n    end\r\n  end\r\n  object Timer1: TTimer\r\n    Interval = 100\r\n    OnTimer = Timer1Timer\r\n    Left = 464\r\n    Top = 440\r\n  end\r\n  object MainMenu1: TMainMenu\r\n    Left = 448\r\n    Top = 136\r\n    object F1: TMenuItem\r\n      Caption = #25991#20214'(&F)'\r\n      object O1: TMenuItem\r\n        Caption = #25171#24320#20070#28304#25991#20214'(&O)...'\r\n        OnClick = O1Click\r\n      end\r\n      object A2: TMenuItem\r\n        Caption = #28155#21152#20070#28304#25991#20214'(&A)...'\r\n        OnClick = A2Click\r\n      end\r\n      object N12: TMenuItem\r\n        Caption = '-'\r\n      end\r\n      object N11: TMenuItem\r\n        Caption = #26032#24314#20070#28304'(&N)...'\r\n        OnClick = N7Click\r\n      end\r\n      object N10: TMenuItem\r\n        Caption = '-'\r\n      end\r\n      object E2: TMenuItem\r\n        Caption = #23548#20986#20070#28304#25991#20214'(&E)...'\r\n        OnClick = E2Click\r\n      end\r\n    end\r\n    object E3: TMenuItem\r\n      Caption = #32534#36753'(&E)'\r\n      object H3: TMenuItem\r\n        Caption = #20998#32452#21517#31216#26367#25442'(&H)...'\r\n        OnClick = H2Click\r\n      end\r\n    end\r\n    object H1: TMenuItem\r\n      Caption = #24110#21161'(&H)'\r\n      object I1: TMenuItem\r\n        Caption = #20851#20110'(&I)'\r\n        OnClick = I1Click\r\n      end\r\n      object N8: TMenuItem\r\n        Caption = '-'\r\n      end\r\n      object W2: TMenuItem\r\n        Caption = #20316#32773#21338#23458'(&W)'\r\n        OnClick = W2Click\r\n      end\r\n      object R2: TMenuItem\r\n        Caption = #23567#35828#38405#35835#22120'(&R)'\r\n        OnClick = R2Click\r\n      end\r\n    end\r\n  end\r\n  object SaveDialog1: TSaveDialog\r\n    Filter = #20070#28304#25991#20214'(*.json)|*.json|'#25152#26377#25991#20214'(*.*)|*.*'\r\n    Options = [ofHideReadOnly, ofPathMustExist, ofNoReadOnlyReturn, ofEnableSizing]\r\n    Title = #23548#20986#20070#28304\r\n    Left = 472\r\n    Top = 352\r\n  end\r\n  object OpenDialog1: TOpenDialog\r\n    Filter = #20070#28304#25991#20214'(*.json)|*.json|'#25152#26377#25991#20214'(*.*)|*.*'\r\n    Options = [ofHideReadOnly, ofPathMustExist, ofFileMustExist, ofEnableSizing]\r\n    Left = 544\r\n    Top = 280\r\n  end\r\nend\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmMain.pas",
    "content": "unit uFrmMain;\r\n\r\ninterface\r\n\r\nuses\r\n  iocp.Http.Client, iocp.Utils.Str,\r\n  YxdJson, YxdStr, YxdHash, YxdWorker, ShellAPI, Math, StrUtils,\r\n  uBookSourceBean,\r\n  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,\r\n  Dialogs, StdCtrls, ExtCtrls, Menus, SynEdit, SynMemo, ComCtrls, SyncObjs,\r\n  SynEditHighlighter, SynHighlighterJSON, Vcl.Buttons;\r\n\r\ntype\r\n  PProcessState = ^TProcessState;\r\n  TProcessState = record\r\n    STime: Int64;\r\n    NeedFree: Boolean;\r\n    Min: Integer;\r\n    Max: Integer;\r\n    Value: Integer;\r\n  end;\r\n\r\ntype\r\n  TForm1 = class(TForm)\r\n    Panel1: TPanel;\r\n    SrcList: TListBox;\r\n    Splitter1: TSplitter;\r\n    Panel2: TPanel;\r\n    PopupMenu1: TPopupMenu;\r\n    C1: TMenuItem;\r\n    Panel3: TPanel;\r\n    EditData: TSynMemo;\r\n    Button1: TButton;\r\n    Panel4: TPanel;\r\n    lbCount: TLabel;\r\n    bookGroupList: TComboBox;\r\n    StatusBar1: TStatusBar;\r\n    ProgressBar1: TProgressBar;\r\n    SynJSONSyn1: TSynJSONSyn;\r\n    PopupMenu2: TPopupMenu;\r\n    S1: TMenuItem;\r\n    N1: TMenuItem;\r\n    C2: TMenuItem;\r\n    X1: TMenuItem;\r\n    P1: TMenuItem;\r\n    A1: TMenuItem;\r\n    N2: TMenuItem;\r\n    N3: TMenuItem;\r\n    R1: TMenuItem;\r\n    Z1: TMenuItem;\r\n    N4: TMenuItem;\r\n    W1: TMenuItem;\r\n    Label1: TLabel;\r\n    Edit1: TEdit;\r\n    CheckBox1: TCheckBox;\r\n    CheckBox2: TCheckBox;\r\n    D1: TMenuItem;\r\n    N6: TMenuItem;\r\n    C3: TMenuItem;\r\n    N5: TMenuItem;\r\n    S2: TMenuItem;\r\n    G1: TMenuItem;\r\n    N7: TMenuItem;\r\n    E1: TMenuItem;\r\n    Timer1: TTimer;\r\n    CheckBox3: TCheckBox;\r\n    MainMenu1: TMainMenu;\r\n    F1: TMenuItem;\r\n    H1: TMenuItem;\r\n    E2: TMenuItem;\r\n    I1: TMenuItem;\r\n    SaveDialog1: TSaveDialog;\r\n    W2: TMenuItem;\r\n    N8: TMenuItem;\r\n    R2: TMenuItem;\r\n    N9: TMenuItem;\r\n    H2: TMenuItem;\r\n    E3: TMenuItem;\r\n    H3: TMenuItem;\r\n    N10: TMenuItem;\r\n    N11: TMenuItem;\r\n    T1: TMenuItem;\r\n    Panel5: TPanel;\r\n    Splitter2: TSplitter;\r\n    Label2: TLabel;\r\n    edtLog: TSynMemo;\r\n    SpeedButton1: TSpeedButton;\r\n    StaticText1: TStaticText;\r\n    O1: TMenuItem;\r\n    N12: TMenuItem;\r\n    OpenDialog1: TOpenDialog;\r\n    A2: TMenuItem;\r\n    procedure FormCreate(Sender: TObject);\r\n    procedure C1Click(Sender: TObject);\r\n    procedure FormShow(Sender: TObject);\r\n    procedure SrcListKeyDown(Sender: TObject; var Key: Word;\r\n      Shift: TShiftState);\r\n    procedure FormDestroy(Sender: TObject);\r\n    procedure SrcListData(Control: TWinControl; Index: Integer;\r\n      var Data: string);\r\n    procedure PopupMenu2Popup(Sender: TObject);\r\n    procedure R1Click(Sender: TObject);\r\n    procedure Z1Click(Sender: TObject);\r\n    procedure C2Click(Sender: TObject);\r\n    procedure X1Click(Sender: TObject);\r\n    procedure P1Click(Sender: TObject);\r\n    procedure A1Click(Sender: TObject);\r\n    procedure S1Click(Sender: TObject);\r\n    procedure SrcListClick(Sender: TObject);\r\n    procedure EditDataChange(Sender: TObject);\r\n    procedure W1Click(Sender: TObject);\r\n    procedure D1Click(Sender: TObject);\r\n    procedure C3Click(Sender: TObject);\r\n    procedure Button1Click(Sender: TObject);\r\n    procedure Timer1Timer(Sender: TObject);\r\n    procedure bookGroupListChange(Sender: TObject);\r\n    procedure N7Click(Sender: TObject);\r\n    procedure E1Click(Sender: TObject);\r\n    procedure SrcListDblClick(Sender: TObject);\r\n    procedure I1Click(Sender: TObject);\r\n    procedure E2Click(Sender: TObject);\r\n    procedure W2Click(Sender: TObject);\r\n    procedure R2Click(Sender: TObject);\r\n    procedure S2Click(Sender: TObject);\r\n    procedure G1Click(Sender: TObject);\r\n    procedure H2Click(Sender: TObject);\r\n    procedure T1Click(Sender: TObject);\r\n    procedure SpeedButton1Click(Sender: TObject);\r\n    procedure O1Click(Sender: TObject);\r\n    procedure A2Click(Sender: TObject);\r\n  private\r\n    { Private declarations }\r\n    OldListWndProc, OldTextWndProc: TWndMethod;\r\n    FBookSrcData: JSONArray;\r\n    FBookGroups: TStringHash;\r\n    FIsChange, FChanging: Boolean;\r\n    FCurIndex: Integer;\r\n\r\n    FWaitStop: Integer;\r\n    FTaskRef: Integer;\r\n    FTaskStartTime: Int64;\r\n    FCheckLastTime: Int64;\r\n\r\n    FFilterList: TList;\r\n    FCurCheckIndex: Integer;\r\n    FCheckCount: Integer;\r\n\r\n    FWaitCheckBookSourceSingId: Integer;\r\n\r\n    FLocker: TCriticalSection;\r\n    FStateMsg: string;\r\n\r\n  protected\r\n    procedure SrcListWndProc(var Message: TMessage);\r\n    procedure SrcListTextWndProc(var Message: TMessage);\r\n    procedure AddSrcFiles(ADrop: Integer);\r\n    procedure WMDropFiles(var Msg: TWMDropFiles); message WM_DROPFILES;\r\n  public\r\n    { Public declarations }\r\n    function CheckItem(Item: TBookSourceItem): Boolean;\r\n    function CheckSaveState(): Boolean;\r\n\r\n\r\n    function CheckBookSourceItem(Item: TBookSourceItem; RaiseErr: Boolean = False; OutLog: TStrings = nil): Boolean; overload;\r\n    function CheckBookSourceItem(Item: TBookSourceItem; Http: THttpClient; Header: THttpHeaders; RaiseErr: Boolean = False; OutLog: TStrings = nil): Boolean; overload;\r\n\r\n    procedure AddSrcFile(const FileName: string);\r\n    procedure UpdateBookGroup(Item: TBookSourceItem);\r\n    \r\n    procedure RemoveRepeat(AJob: PJob);  \r\n    procedure WaitCheckBookSource(AJob: PJob);\r\n    procedure DoCheckBookSourceItem(AJob: PJob);\r\n    \r\n    procedure TaskFinish(AJob: PJob);\r\n    procedure DoNotifyDataChange(AJob: PJob);\r\n    procedure DoUpdateProcess(AJob: PJob);\r\n\r\n    procedure Log(const Msg: string);\r\n    procedure LogD(const Msg: string);\r\n    procedure DispLog();\r\n    procedure NotifyListChange(Flag: Integer = 0);\r\n\r\n    procedure RemoveSelected();\r\n    procedure EditSource(Item: TBookSourceItem);\r\n    \r\n  end;\r\n\r\nvar\r\n  Form1: TForm1;\r\n\r\nimplementation\r\n\r\n{$R *.dfm}\r\n\r\nuses\r\n  uFrmWait, uFrmEditSource, uFrmReplaceGroup;\r\n\r\nprocedure CutOrCopyFiles(FileList: AnsiString; bCopy: Boolean);\r\ntype\r\n  PDropFiles = ^TDropFiles;\r\n \r\n  TDropFiles = record\r\n    pfiles: DWORD;\r\n    pt: TPoint;\r\n    fNC: BOOL;\r\n    fwide: BOOL;\r\n  end;\r\nconst\r\n  DROPEFFECT_COPY = 1;\r\n  DROPEFFECT_MOVE = 2;\r\nvar\r\n  hGblFileList: hGlobal;\r\n  pFileListDate: Pbyte;\r\n  HandleDropEffect: UINT;\r\n  hGblDropEffect: hGlobal;\r\n  pdwDropEffect: PDWORD;\r\n  iLen: Integer;\r\nbegin\r\n  iLen := Length(FileList) + 2;\r\n  FileList := FileList + #0#0;\r\n  hGblFileList := GlobalAlloc(GMEM_ZEROINIT or GMEM_MOVEABLE or GMEM_SHARE,\r\n    SizeOf(TDropFiles) + iLen);\r\n  pFileListDate := GlobalLock(hGblFileList);\r\n  PDropFiles(pFileListDate)^.pfiles := SizeOf(TDropFiles);\r\n  PDropFiles(pFileListDate)^.pt.Y := 0;\r\n  PDropFiles(pFileListDate)^.pt.X := 0;\r\n  PDropFiles(pFileListDate)^.fNC := False;\r\n  PDropFiles(pFileListDate)^.fwide := False;\r\n  Inc(pFileListDate, SizeOf(TDropFiles));\r\n  CopyMemory(pFileListDate, @FileList[1], iLen);\r\n  GlobalUnlock(hGblFileList);\r\n  HandleDropEffect := RegisterClipboardFormat('Preferred DropEffect ');\r\n  hGblDropEffect := GlobalAlloc(GMEM_ZEROINIT or GMEM_MOVEABLE or GMEM_SHARE,\r\n    SizeOf(DWORD));\r\n  pdwDropEffect := GlobalLock(hGblDropEffect);\r\n  if (bCopy) then pdwDropEffect^ := DROPEFFECT_COPY\r\n  else pdwDropEffect^ := DROPEFFECT_MOVE;\r\n  GlobalUnlock(hGblDropEffect);\r\n  if OpenClipboard(0) then begin\r\n    EmptyClipboard();\r\n    SetClipboardData(HandleDropEffect, hGblDropEffect);\r\n    SetClipboardData(CF_HDROP, hGblFileList);\r\n    CloseClipboard();\r\n  end;\r\nend;\r\n\r\n// ļļ #0 ָ\r\nprocedure CopyFileClipbrd(const FName: string);\r\nbegin\r\n  CutOrCopyFiles(AnsiString(FName), True);\r\nend;\r\n\r\nprocedure TForm1.AddSrcFiles(ADrop: Integer);\r\nvar\r\n  i: Integer;\r\n  p: array[0..1023] of Char;\r\nbegin\r\n  for i := 0 to DragQueryFile(ADrop, $FFFFFFFF, nil, 0) - 1 do begin\r\n    DragQueryFile(ADrop, i, p, 1024);\r\n    AddSrcFile(StrPas(p));\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.bookGroupListChange(Sender: TObject);\r\nbegin\r\n  if FChanging then\r\n    Exit; \r\n  FChanging := True;\r\n  try     \r\n    EditData.Text := '';\r\n    FIsChange := False;\r\n    FCurIndex := -1;\r\n    NotifyListChange(1);\r\n  finally\r\n    FChanging := False;\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.Button1Click(Sender: TObject);\r\nbegin\r\n  if Button1.Tag = 0 then begin\r\n    Button1.Tag := 1;\r\n    Button1.Caption := 'ֹͣ(&S)';\r\n    \r\n    FTaskRef := 0;\r\n    FWaitStop := 0;\r\n    FCheckLastTime := GetTimestamp;\r\n    FTaskStartTime := GetTimestamp;\r\n\r\n    Workers.MaxWorkers := Max(1, StrToIntDef(Edit1.Text, 8));\r\n\r\n    edtLog.Lines.Clear;\r\n    Log('ڳʼ...');\r\n\r\n    Timer1.Enabled := True;\r\n    ProgressBar1.Min := 0;\r\n    ProgressBar1.Max := 100;\r\n    ProgressBar1.Position := 0;      \r\n\r\n    if CheckBox1.Checked then begin\r\n      Inc(FTaskRef);\r\n      Log('ȥظ...');\r\n      Workers.Post(RemoveRePeat, Pointer(Integer(CheckBox3.Checked)));\r\n    end;\r\n    if CheckBox2.Checked then begin\r\n      Inc(FTaskRef);\r\n      if not CheckBox1.Checked then\r\n        Workers.SendSignal(FWaitCheckBookSourceSingId);\r\n    end;\r\n\r\n    ShowWaitDlg();\r\n  end else begin\r\n    AtomicIncrement(FTaskRef);\r\n    if AtomicDecrement(FTaskRef) <= 0 then begin\r\n      Button1.Tag := 0;\r\n      Button1.Caption := 'ʼ(&B)';\r\n      Timer1.Enabled := False;\r\n      FTaskStartTime := 0;\r\n      ProgressBar1.Visible := False;\r\n      HideWaitDlg;\r\n      Log('');\r\n    end else begin        \r\n      Button1.Tag := 2;\r\n      Button1.Caption := 'ֹͣ...';      \r\n      AtomicIncrement(FWaitStop);\r\n    end;\r\n  end;\r\n  //Application.ProcessMessages;\r\nend;\r\n\r\nprocedure TForm1.A1Click(Sender: TObject);\r\nbegin\r\n  EditData.SelectAll;\r\nend;\r\n\r\nprocedure TForm1.A2Click(Sender: TObject);\r\nbegin\r\n  OpenDialog1.Title := 'Դļ';\r\n  if OpenDialog1.Execute(Handle) then\r\n    AddSrcFile(OpenDialog1.FileName);\r\nend;\r\n\r\nprocedure TForm1.AddSrcFile(const FileName: string);\r\nvar\r\n  Data: JSONArray;\r\n  Item: TBookSourceItem;\r\n  I: Integer;\r\nbegin\r\n  Data := JSONArray.Create;\r\n  try    \r\n    Data.LoadFromFile(FileName);\r\n    I := Data.Count;\r\n    while I > 0 do begin\r\n      try\r\n        Item := TBookSourceItem(Data.O[0]);\r\n        if Assigned(Item) and (Item.bookSourceUrl <> '') then begin\r\n          UpdateBookGroup(Item);\r\n          FBookSrcData.Add(Data.O[0]);\r\n        end;\r\n      except\r\n      end;\r\n      Dec(I);\r\n    end;\r\n  finally\r\n    Data.Free;\r\n    NotifyListChange;\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.C1Click(Sender: TObject);\r\nbegin\r\n  FBookSrcData.Clear;\r\n  NotifyListChange;\r\nend;\r\n\r\nprocedure TForm1.C2Click(Sender: TObject);\r\nbegin\r\n  EditData.CopyToClipboard;\r\nend;\r\n\r\nprocedure TForm1.C3Click(Sender: TObject);\r\nvar\r\n  S: string;\r\n  Item, NewItem: TBookSourceItem;\r\nbegin\r\n  if SrcList.ItemIndex < 0 then Exit;\r\n  if not CheckSaveState then Exit;\r\n  Item := TBookSourceItem(FFilterList[SrcList.ItemIndex]);\r\n  S := InputBox('Դ', 'Դ', Item.bookSourceName);\r\n\r\n  NewItem := TBookSourceItem(FBookSrcData.AddChildObject);\r\n  NewItem.Parse(Item.ToString);\r\n  NewItem.bookSourceName := S;\r\n\r\n  NotifyListChange;\r\n  if FFilterList.Count > 0 then    \r\n    SrcList.ItemIndex := FFilterList.Count - 1;\r\nend;\r\n\r\nfunction TForm1.CheckBookSourceItem(Item: TBookSourceItem; RaiseErr: Boolean; OutLog: TStrings): Boolean;\r\nvar\r\n  Http: THttpClient;\r\n  Header: THttpHeaders;\r\nbegin\r\n  Result := False;\r\n  try\r\n    if not Assigned(Item) then Exit; \r\n    Http := THttpClient.Create(nil);\r\n    if Assigned(OutLog) then begin    \r\n      Http.ConnectionTimeOut := 6000;\r\n      Http.RecvTimeOut := 30000;\r\n    end else begin\r\n      Http.ConnectionTimeOut := 30000;\r\n      Http.RecvTimeOut := 30000;\r\n    end;\r\n    Header := THttpHeaders.Create;\r\n\r\n    Result := CheckBookSourceItem(Item, Http, Header, RaiseErr, OutLog);\r\n    \r\n  finally\r\n    FreeAndNil(Http);\r\n    FreeAndNil(Header);\r\n  end;\r\nend;\r\n\r\nfunction TForm1.CheckBookSourceItem(Item: TBookSourceItem; Http: THttpClient;\r\n  Header: THttpHeaders; RaiseErr: Boolean; OutLog: TStrings): Boolean;\r\n\r\n  function CheckURL(const URL, Title: string; RaiseErr: Boolean = False; Try404: Boolean = False): Boolean;\r\n  var\r\n    Resp: THttpResult;\r\n    Msg: string;\r\n  begin\r\n    Result := (URL <> '') and (URL <> '-') and (Pos('http', LowerCase(URL)) = 1); \r\n    if Result then begin\r\n      try\r\n        Resp := Http.Get(UrlEncodeEx(URL), nil, Header);\r\n        if (Resp.StatusCode = 200) or (Try404 and (Resp.StatusCode = 404)) then begin       \r\n          Result := True;\r\n          if Assigned(OutLog) then OutLog.Add(Title + 'ӳɹ.');\r\n        end else begin\r\n          Result := False;\r\n          Msg := Format('%sʧ(StatusCode: %d, %s).', [Title, Resp.StatusCode, URL]);\r\n          if Assigned(OutLog) then OutLog.Add(Msg);\r\n          if RaiseErr then\r\n            raise Exception.Create(Msg);\r\n        end;\r\n      except\r\n        Result := False;\r\n        Msg := Format('%sԳ(%s).', [Title, Exception(ExceptObject).Message]);\r\n        if Assigned(OutLog) then OutLog.Add(Msg);\r\n        if RaiseErr then\r\n          raise Exception.Create(Msg);\r\n      end;\r\n    end else\r\n      OutLog.Add('Ч' + Title + '.');\r\n  end;\r\n\r\n  // ⷢб\r\n  function CheckFindURL(const Text, Title: string; RaiseErr: Boolean): Boolean;\r\n  var \r\n    List: TStrings;\r\n    I, J, L: Integer;\r\n    Msg, Item, SubTitle, AURL: string;\r\n  begin\r\n    if Text = '' then begin\r\n      Result := True;\r\n      Exit;\r\n    end;\r\n    try\r\n      J := 1;\r\n      while (J > 0) and (J <= Length(Text)) do begin        \r\n        I := PosEx('&&', Text, J);\r\n        L := 2;\r\n        if I <= 0 then begin\r\n          I := PosEx(#$A, Text, J);\r\n          L := 1;\r\n        end;\r\n        if I > 0 then begin\r\n          Item := MidStr(Text, J, I - J);\r\n          J := I + L;\r\n        end else begin\r\n          Item := Trim(RightStr(Text, Length(Text) - J + 1));\r\n          J := Length(Text) + 1;\r\n        end;\r\n\r\n        if (Item = #$A) or (Item = #13) then\r\n          Continue;\r\n\r\n        Item := StringReplace(Item, #13, '', [rfReplaceAll]);\r\n        Item := StringReplace(Item, #10, '', [rfReplaceAll]);\r\n        Item := StringReplace(Item, '\\n', '', [rfReplaceAll, rfIgnoreCase]);\r\n        Item := Trim(Item);  \r\n              \r\n        I := Pos('::', Item);\r\n        if (Item = '') or (I < 1) then begin\r\n          if Assigned(OutLog) then\r\n            OutLog.Add('бʽ');\r\n          Continue;\r\n        end else begin\r\n          SubTitle := Trim(LeftStr(Item, I - 1));\r\n          AURL := Trim(RightStr(Item, Length(Item) - I - 1));\r\n          CheckURL(AURL, 'б' + SubTitle + '');   \r\n        end;\r\n      end;\r\n    except\r\n      Result := False;\r\n      Msg := Format('%sԳ(%s).', [Title, Exception(ExceptObject).Message]);\r\n      if Assigned(OutLog) then OutLog.Add(Msg);\r\n      if RaiseErr then\r\n        raise Exception.Create(Msg);\r\n    end;\r\n  end;\r\n  \r\nvar\r\n  Resp: THttpResult;\r\n  URL: string;\r\n  T: Int64;\r\nbegin\r\n  Result := False;\r\n  if not Assigned(Item) then Exit; \r\n  if Item.bookSourceUrl <> '' then begin\r\n    T := GetTimestamp;\r\n    Header.Clear;\r\n    if Item.httpUserAgent <> '' then\r\n      Header.Add('User-Agent', Item.httpUserAgent);\r\n\r\n    // ԴURL\r\n    Result := CheckURL(Trim(Item.bookSourceUrl), 'ԴURL', RaiseErr, True);\r\n\r\n    if Result and Assigned(OutLog) then begin\r\n      // URL\r\n      CheckURL(Trim(Item.ruleSearchUrl), 'ַ');\r\n      // ⷢб\r\n      CheckFindURL(Trim(Item.ruleFindUrl), '', RaiseErr);\r\n    end;\r\n\r\n    if Assigned(OutLog) then\r\n      OutLog.Add(Format('ʱ %d ms.', [GetTimestamp - T]));\r\n  end else begin\r\n    if Assigned(OutLog) then\r\n      OutLog.Add('ԴURLδ.');\r\n    raise Exception.Create('ԴURLЧ');\r\n  end;\r\nend;\r\n\r\nfunction TForm1.CheckItem(Item: TBookSourceItem): Boolean;\r\nbegin\r\n  Result := Assigned(Item) and (Item.bookSourceUrl <> '');\r\nend;\r\n\r\nfunction TForm1.CheckSaveState: Boolean;\r\nvar\r\n  LR: Integer;\r\nbegin\r\n  if FIsChange and (FCurIndex >= 0) and (FCurIndex < SrcList.Count) then begin\r\n    LR := MessageBox(Handle, 'ԴѾ޸ģǷ񱣴棿', 'ʾ', 64 + MB_YESNOCANCEL);\r\n    if LR = IDCANCEL then begin\r\n      Result := False;\r\n      Exit;\r\n    end;\r\n    if LR = IDYES then\r\n      S1Click(S1);\r\n  end;\r\n  Result := True;\r\nend;\r\n\r\nprocedure TForm1.D1Click(Sender: TObject);\r\nbegin\r\n  RemoveSelected;\r\nend;\r\n\r\nprocedure TForm1.DispLog;\r\nbegin\r\n  if FTaskStartTime > 0 then begin \r\n    if ProgressBar1.Visible then\r\n      StatusBar1.Panels[1].Text := Format('%s (%d/%d, %d%%) (ʱ: %dms)', \r\n        [FStateMsg, ProgressBar1.Position, ProgressBar1.Max, Round(ProgressBar1.Position / ProgressBar1.Max * 100),\r\n         GetTimestamp - FTaskStartTime])\r\n    else  \r\n      StatusBar1.Panels[1].Text := Format('%s (ʱ: %dms)', [FStateMsg, GetTimestamp - FTaskStartTime])\r\n  end else\r\n    StatusBar1.Panels[1].Text := FStateMsg;\r\nend;\r\n\r\nprocedure TForm1.DoCheckBookSourceItem(AJob: PJob);\r\nvar\r\n  Item: TBookSourceItem;\r\n  State: PProcessState;\r\n  V: Integer;\r\n  IsOK: Boolean;\r\n  Http: THttpClient;\r\n  Header: THttpHeaders;\r\nbegin\r\n  V := 0;\r\n  try\r\n    Http := THttpClient.Create(nil);\r\n    Http.ConnectionTimeOut := 30000;\r\n    Http.RecvTimeOut := 30000;\r\n    Header := THttpHeaders.Create;\r\n    \r\n    while (not AJob.IsTerminated) and (FWaitStop = 0) do begin\r\n      V := AtomicIncrement(FCurCheckIndex) - 1;\r\n      \r\n      FLocker.Enter;\r\n      if (GetTimestamp - FCheckLastTime) > 100 then begin\r\n        FCheckLastTime := GetTimestamp;\r\n        New(State);\r\n        State.Min := 0;\r\n        State.Max := FCheckCount;\r\n        State.Value := V;\r\n        Workers.Post(DoUpdateProcess, State, True);\r\n        Sleep(10);\r\n      end;\r\n      FLocker.Leave;\r\n      \r\n      if V < FCheckCount then begin\r\n        Item := TBookSourceItem(FBookSrcData.O[V]);\r\n        if not Assigned(Item) then Exit;\r\n\r\n        try\r\n          IsOK := CheckBookSourceItem(Item, Http, Header);\r\n        except\r\n          IsOK := False;\r\n        end;\r\n        \r\n        if IsOK then\r\n          Item.RemoveGroup('ʧЧ')\r\n        else\r\n          Item.AddGroup('ʧЧ');   \r\n      end else\r\n        Break;\r\n    end;\r\n  finally\r\n\r\n    if (V >= FCheckCount) or (FWaitStop > 0) then begin\r\n      Sleep(100);\r\n      Workers.Post(TaskFinish, nil, True);\r\n    end;\r\n\r\n    FreeAndNil(Http);\r\n    FreeAndNil(Header);\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.DoNotifyDataChange(AJob: PJob);\r\nbegin\r\n  NotifyListChange;\r\nend;\r\n\r\nprocedure TForm1.DoUpdateProcess(AJob: PJob);\r\nvar\r\n  V: PProcessState;\r\nbegin\r\n  if not Assigned(Self) then Exit;  \r\n  V := AJob.Data;\r\n  if V = nil then\r\n    ProgressBar1.Visible := False\r\n  else begin\r\n    ProgressBar1.Min := V.Min;\r\n    ProgressBar1.Max := V.Max;\r\n    ProgressBar1.Position := V.Value;\r\n    ProgressBar1.Visible := Button1.Tag <> 0;\r\n    if V.NeedFree then\r\n      Dispose(V);\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.E1Click(Sender: TObject);\r\nbegin\r\n  if SrcList.ItemIndex < 0 then Exit;\r\n  EditSource(TBookSourceItem(FFilterList[SrcList.ItemIndex]));\r\nend;\r\n\r\nprocedure TForm1.E2Click(Sender: TObject);\r\nvar\r\n  FName: JSONString;\r\nbegin\r\n  if SaveDialog1.Execute(Handle) then begin\r\n    FName := SaveDialog1.FileName;\r\n    if ExtractFileExt(FName) = '' then\r\n      FName := FName + '.json';\r\n    FBookSrcData.SaveToFile(FName, 4, YxdStr.TTextEncoding.teUTF8, False);\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.EditDataChange(Sender: TObject);\r\nbegin\r\n  FIsChange := True;\r\nend;\r\n\r\nprocedure TForm1.EditSource(Item: TBookSourceItem);\r\nbegin\r\n  ShowEditSource(Item,\r\n    procedure (Item: TBookSourceItem) \r\n    begin\r\n      if FBookSrcData.IndexOfObject(Item) < 0 then\r\n        FBookSrcData.Add(JSONObject(Item));  \r\n      NotifyListChange;\r\n      if (FCurIndex >= 0) and (FCurIndex < FFilterList.Count) then begin\r\n        if TObject(FFilterList[FCurIndex]) = Item then begin        \r\n          EditData.Text := TBookSourceItem(FFilterList[FCurIndex]).ToString(4);\r\n          FIsChange := False;\r\n        end;\r\n      end;  \r\n    end\r\n  );\r\nend;\r\n\r\nprocedure TForm1.FormCreate(Sender: TObject);\r\nbegin\r\n  JsonNameAfterSpace := True;\r\n  JsonCaseSensitive := False;\r\n  FBookSrcData := JSONArray.Create;\r\n  FBookGroups := TStringHash.Create(997);\r\n  FFilterList := TList.Create;\r\n  FLocker := TCriticalSection.Create;\r\n\r\n  FWaitCheckBookSourceSingId := Workers.RegisterSignal('WaitCheckBookSource');\r\n  Workers.PostWait(WaitCheckBookSource, FWaitCheckBookSourceSingId);\r\nend;\r\n\r\nprocedure TForm1.FormDestroy(Sender: TObject);\r\nbegin\r\n  FreeAndNil(FBookSrcData);\r\n  FreeAndNil(FBookGroups);\r\n  FreeAndNil(FFilterList);\r\n  FreeAndNil(FLocker);\r\nend;\r\n\r\nprocedure TForm1.FormShow(Sender: TObject);\r\nbegin\r\n  DragAcceptFiles(SrcList.Handle, True);\r\n  DragAcceptFiles(StaticText1.Handle, True);\r\n\r\n  OldListWndProc := SrcList.WindowProc;\r\n  OldTextWndProc := StaticText1.WindowProc;\r\n  SrcList.WindowProc := SrcListWndProc;\r\n  StaticText1.WindowProc := SrcListTextWndProc;\r\n\r\n  NotifyListChange;\r\nend;\r\n\r\nprocedure TForm1.G1Click(Sender: TObject);\r\nbegin\r\n  FBookSrcData.Sort(\r\n    function (A, B: Pointer): Integer\r\n    var\r\n      Item1: PJSONValue absolute A;\r\n      Item2: PJSONValue absolute B;\r\n      S1, S2: string;\r\n    begin\r\n      if (Item1.FType = Item2.FType) and (Item1.FType = jdtObject) and \r\n        (Item1.AsJsonObject <> nil) and (Item2.AsJsonObject <> nil) \r\n      then begin\r\n        S1 := TBookSourceItem(Item1.AsJsonObject).bookSourceGroup;\r\n        S2 := TBookSourceItem(Item2.AsJsonObject).bookSourceGroup;\r\n        Result := CompareStr(S1, S2);\r\n      end else\r\n        Result := 0;\r\n    end\r\n  );\r\n  NotifyListChange(1);\r\nend;\r\n\r\nprocedure TForm1.H2Click(Sender: TObject);\r\nvar\r\n  FindStr, NewStr: string;\r\n  I, Flag: Integer;\r\n  Item: TBookSourceItem;\r\nbegin\r\n  if ShowReplaceGroup(Self, FindStr, NewStr, Flag) then begin\r\n    if (FindStr <> '') and (Flag = 0) then    \r\n      Exit;\r\n    for I := 0 to FBookSrcData.Count - 1 do begin\r\n      Item := TBookSourceItem(FBookSrcData.O[I]);\r\n      if not Assigned(Item) then Continue;\r\n      if Flag = 0 then begin              \r\n        Item.bookSourceGroup := StringReplace(Trim(Item.bookSourceGroup), FindStr, NewStr, [rfReplaceAll, rfIgnoreCase]);\r\n      end else begin\r\n        if NewStr = '' then\r\n          Item.RemoveGroup(FindStr)\r\n        else\r\n          Item.ReplaceGroup(FindStr, NewStr);\r\n      end;      \r\n    end;\r\n    NotifyListChange();\r\n  end;   \r\nend;\r\n\r\nprocedure TForm1.I1Click(Sender: TObject);\r\nvar\r\n  Msg: string;\r\nbegin\r\n  Msg := Application.Title + sLineBreak + 'YangYxd Ȩ 2019';\r\n  MessageBox(Handle, PChar(Msg), '', 64);\r\nend;\r\n\r\nprocedure TForm1.Log(const Msg: string);\r\nbegin\r\n  LogD(Msg);\r\n  FStateMsg := Msg;\r\n  DispLog();\r\nend;\r\n\r\nprocedure TForm1.LogD(const Msg: string);\r\nbegin\r\n  edtLog.Lines.Add(Format('[%s] %s', [FormatDateTime('hh:mm:ss.zzz', Now), Msg]));\r\nend;\r\n\r\nprocedure TForm1.N7Click(Sender: TObject);\r\nvar\r\n  Item: TBookSourceItem;\r\nbegin\r\n  Item := TBookSourceItem(JSONObject.Create);\r\n  EditSource(Item);\r\nend;\r\n\r\nprocedure TForm1.NotifyListChange(Flag: Integer);\r\nvar\r\n  I, J: Integer;\r\n  Key: string;\r\n  Item: TBookSourceItem;\r\nbegin\r\n  J := FCurIndex;\r\n  \r\n  if Flag = 0 then begin \r\n    for I := 0 to FBookSrcData.Count - 1 do\r\n      UpdateBookGroup(TBookSourceItem(FBookSrcData.O[I])); \r\n    bookGroupList.Items.Clear;\r\n    FBookGroups.GetKeyList(bookGroupList.Items);\r\n  end;   \r\n\r\n  FFilterList.Clear;\r\n  Key := LowerCase(bookGroupList.Text);\r\n  if Key <> '' then begin  \r\n    for I := 0 to FBookSrcData.Count - 1 do begin\r\n      Item := TBookSourceItem(FBookSrcData.O[I]);\r\n      if (Pos(Key, Item.bookSourceGroup) > 0) or (Pos(Key, Item.bookSourceName) > 0) then\r\n        FFilterList.Add(Item);      \r\n    end;\r\n  end else begin\r\n    for I := 0 to FBookSrcData.Count - 1 do \r\n      FFilterList.Add(FBookSrcData.O[I]);  \r\n  end;\r\n  \r\n  SrcList.Count := FFilterList.Count;\r\n  StaticText1.Visible := SrcList.Count = 0;\r\n  if (J < SrcList.Count) and (J >= 0) then begin\r\n    SrcList.ClearSelection;\r\n    SrcList.ItemIndex := J;\r\n    SrcList.Selected[J] := True;\r\n  end;\r\n  \r\n  SrcList.ShowHint := SrcList.Count = 0;\r\n  StatusBar1.Panels[0].Text := Format('Դ%d, ǰ: %d', [FBookSrcData.Count, FFilterList.Count]);\r\nend;\r\n\r\nprocedure TForm1.O1Click(Sender: TObject);\r\nbegin\r\n  OpenDialog1.Title := 'Դļ';\r\n  if OpenDialog1.Execute(Handle) then begin\r\n    FBookSrcData.Clear;\r\n    AddSrcFile(OpenDialog1.FileName);\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.P1Click(Sender: TObject);\r\nbegin\r\n  EditData.PasteFromClipboard;\r\nend;\r\n\r\nprocedure TForm1.PopupMenu2Popup(Sender: TObject);\r\nbegin\r\n  S1.Enabled := SrcList.ItemIndex >= 0;\r\n  P1.Enabled := EditData.CanPaste;\r\n  X1.Enabled := EditData.SelLength > 0;\r\n  C2.Enabled := X1.Enabled;\r\n  R1.Enabled := EditData.CanUndo;\r\n  Z1.Enabled := EditData.CanRedo;\r\n  W1.Checked := EditData.WordWrap;\r\nend;\r\n\r\nprocedure TForm1.R1Click(Sender: TObject);\r\nbegin\r\n  EditData.Undo;\r\nend;\r\n\r\nprocedure TForm1.R2Click(Sender: TObject);\r\nbegin\r\n  ShellExecute(0, 'OPEN', PChar('https://github.com/yangyxd/MyBookshelf'), nil, nil, SW_SHOWMAXIMIZED)\r\nend;\r\n\r\nprocedure TForm1.RemoveRepeat(AJob: PJob);\r\nvar\r\n  CheckName: Boolean;\r\n\r\n  function Equals(A, B: TBookSourceItem): Boolean;\r\n  begin\r\n    Result := \r\n      (LowerCase(A.bookSourceUrl) = LowerCase(B.bookSourceUrl)) and    \r\n      (LowerCase(A.loginUrl) = LowerCase(B.loginUrl)) and    \r\n      (LowerCase(A.ruleBookContent) = LowerCase(B.ruleBookContent)) and    \r\n      (LowerCase(A.httpUserAgent) = LowerCase(B.httpUserAgent)) and    \r\n      (LowerCase(A.ruleBookKind) = LowerCase(B.ruleBookKind)) and    \r\n      (LowerCase(A.ruleBookLastChapter) = LowerCase(B.ruleBookLastChapter)) and    \r\n      (LowerCase(A.ruleBookName) = LowerCase(B.ruleBookName)) and    \r\n      (LowerCase(A.ruleBookUrlPattern) = LowerCase(B.ruleBookUrlPattern)) and    \r\n      (LowerCase(A.ruleChapterList) = LowerCase(B.ruleChapterList)) and    \r\n      (LowerCase(A.ruleChapterName) = LowerCase(B.ruleChapterName)) and    \r\n      (LowerCase(A.ruleChapterUrl) = LowerCase(B.ruleChapterUrl)) and    \r\n      (LowerCase(A.ruleChapterUrlNext) = LowerCase(B.ruleChapterUrlNext)) and    \r\n      (LowerCase(A.ruleContentUrl) = LowerCase(B.ruleContentUrl)) and    \r\n      (LowerCase(A.ruleContentUrlNext) = LowerCase(B.ruleContentUrlNext)) and    \r\n      (LowerCase(A.ruleCoverUrl) = LowerCase(B.ruleCoverUrl)) and    \r\n      (LowerCase(A.ruleFindUrl) = LowerCase(B.ruleFindUrl)) and    \r\n      (LowerCase(A.ruleIntroduce) = LowerCase(B.ruleIntroduce)) and    \r\n      (LowerCase(A.ruleSearchAuthor) = LowerCase(B.ruleSearchAuthor)) and    \r\n      (LowerCase(A.ruleSearchCoverUrl) = LowerCase(B.ruleSearchCoverUrl)) and    \r\n      (LowerCase(A.ruleSearchKind) = LowerCase(B.ruleSearchKind)) and    \r\n      (LowerCase(A.ruleSearchLastChapter) = LowerCase(B.ruleSearchLastChapter)) and    \r\n      (LowerCase(A.ruleSearchList) = LowerCase(B.ruleSearchList)) and    \r\n      (LowerCase(A.ruleSearchName) = LowerCase(B.ruleSearchName)) and    \r\n      (LowerCase(A.ruleSearchNoteUrl) = LowerCase(B.ruleSearchNoteUrl)) and    \r\n      (LowerCase(A.ruleSearchUrl) = LowerCase(B.ruleSearchUrl));\r\n    if not CheckName then\r\n      Result := Result and\r\n        (LowerCase(A.bookSourceName) = LowerCase(B.bookSourceName)) and    \r\n        (LowerCase(A.bookSourceGroup) = LowerCase(B.bookSourceGroup));    \r\n  end;\r\n  \r\nvar\r\n  I, J, LastCount, ST: Integer;\r\n  Item: TBookSourceItem;\r\n  T: TProcessState;\r\n  State: PProcessState;\r\nbegin\r\n  I := 0;\r\n  LastCount := FBookSrcData.Count;\r\n  CheckName := Boolean(Integer(AJob.Data));\r\n  \r\n  T.STime := GetTimestamp;\r\n  T.Min := 0;\r\n  T.Value := 0;\r\n  ST := 1000;\r\n  \r\n  try\r\n    while I < FBookSrcData.Count do begin  \r\n      Item := TBookSourceItem(FBookSrcData.O[I]);\r\n      Inc(I);\r\n      \r\n      for J := FBookSrcData.Count - 1 downto I do begin\r\n        if Equals(Item, TBookSourceItem(FBookSrcData.O[J])) then\r\n          FBookSrcData.Remove(J);\r\n      end;\r\n      if AJob.IsTerminated or (FWaitStop > 0) then\r\n        Break;\r\n      if GetTimestamp - T.STime > ST then begin\r\n        ST := 200;\r\n        T.Value := I;\r\n        T.Max := FBookSrcData.Count;\r\n        New(State);\r\n        State^ := T;\r\n        State.NeedFree := True;\r\n        Workers.Post(DoUpdateProcess, State, True);\r\n      end;\r\n    end;\r\n  finally  \r\n    if LastCount <> FBookSrcData.Count then\r\n      Workers.Post(DoNotifyDataChange, nil, True);\r\n    Sleep(100);\r\n    Workers.Post(TaskFinish, nil, True);\r\n  end; \r\nend;\r\n\r\nprocedure TForm1.RemoveSelected;\r\nvar\r\n  I, V: Integer;\r\nbegin\r\n  for I := SrcList.Count - 1 downto 0 do begin\r\n    if SrcList.Selected[I] then begin\r\n      V := FBookSrcData.IndexOfObject(JSONObject(FFilterList[I]));\r\n      if V >= 0 then       \r\n        FBookSrcData.Remove(V);\r\n    end;\r\n  end;\r\n  NotifyListChange;  \r\nend;\r\n\r\nprocedure TForm1.S1Click(Sender: TObject);\r\nvar\r\n  S: string;\r\n  Item: TBookSourceItem;\r\nbegin\r\n  if (FCurIndex < 0) or (FCurIndex >= SrcList.Count) then Exit;\r\n  Item := TBookSourceItem(FFilterList[FCurIndex]);\r\n  if not Assigned(Item) then Exit;\r\n  try\r\n    FIsChange := False;\r\n    S := Item.ToString();\r\n    Item.Parse(EditData.Text);\r\n    if not CheckItem(Item) then\r\n      Item.Parse(S);\r\n  finally\r\n    NotifyListChange;\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.S2Click(Sender: TObject);\r\nbegin             \r\n  FBookSrcData.Sort(\r\n    function (A, B: Pointer): Integer\r\n    var\r\n      Item1: PJSONValue absolute A;\r\n      Item2: PJSONValue absolute B;\r\n      S1, S2: string;\r\n    begin\r\n      if (Item1.FType = Item2.FType) and (Item1.FType = jdtObject) and \r\n        (Item1.AsJsonObject <> nil) and (Item2.AsJsonObject <> nil) \r\n      then begin\r\n        S1 := TBookSourceItem(Item1.AsJsonObject).bookSourceName;\r\n        S2 := TBookSourceItem(Item2.AsJsonObject).bookSourceName;\r\n        Result := CompareStr(S1, S2);\r\n      end else\r\n        Result := 0;\r\n    end\r\n  );\r\n  NotifyListChange(1);\r\nend;\r\n\r\nprocedure TForm1.SpeedButton1Click(Sender: TObject);\r\nbegin\r\n  edtLog.Lines.Clear;\r\nend;\r\n\r\nprocedure TForm1.SrcListClick(Sender: TObject);\r\nbegin\r\n  if SrcList.ItemIndex < 0 then Exit;\r\n  if not CheckSaveState then Exit;\r\n  FCurIndex := SrcList.ItemIndex;\r\n  EditData.Text := TBookSourceItem(FFilterList[FCurIndex]).ToString(4);\r\n  FIsChange := False;\r\nend;\r\n\r\nprocedure TForm1.SrcListData(Control: TWinControl; Index: Integer;\r\n  var Data: string);\r\nvar\r\n  Item: TBookSourceItem;\r\nbegin\r\n  if Index < FBookSrcData.Count then begin\r\n    Item := TBookSourceItem(FFilterList[index]);\r\n    Data := Format('%s%s', [Item.bookSourceGroup, Item.bookSourceName]);\r\n  end else \r\n    Data := '';\r\nend;\r\n\r\nprocedure TForm1.SrcListDblClick(Sender: TObject);\r\nbegin\r\n  E1Click(E1);  \r\nend;\r\n\r\nprocedure TForm1.SrcListKeyDown(Sender: TObject; var Key: Word;\r\n  Shift: TShiftState);\r\n\r\n  procedure PasteItems();\r\n  var \r\n    pGlobal : Thandle;\r\n  begin\r\n    OpenClipboard(Handle);\r\n    try\r\n      pGlobal := GetClipboardData(CF_HDROP); //ȡļ\r\n      if pGlobal > 0 then\r\n        AddSrcFiles(pGlobal);\r\n    finally\r\n      CloseClipboard;\r\n    end;   \r\n  end;\r\n  \r\nbegin\r\n  if Key = VK_DELETE then begin\r\n    RemoveSelected();\r\n  end else if Key = Ord('V') then begin  // ճ\r\n    PasteItems();\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.SrcListTextWndProc(var Message: TMessage);\r\nbegin\r\n  if Message.Msg = WM_DROPFILES then\r\n    WMDropFiles(TWMDropFiles(Message))\r\n  else\r\n    OldTextWndProc(Message);\r\nend;\r\n\r\nprocedure TForm1.SrcListWndProc(var Message: TMessage);\r\nbegin\r\n  if Message.Msg = WM_DROPFILES then\r\n    WMDropFiles(TWMDropFiles(Message))\r\n  else\r\n    OldListWndProc(Message);\r\nend;\r\n\r\nprocedure TForm1.T1Click(Sender: TObject);\r\nvar\r\n  Item: TBookSourceItem;\r\n  Msg: TStrings;\r\nbegin\r\n  Item := TBookSourceItem(JSONObject.Create);\r\n  try\r\n    Item.Parse(EditData.Text);\r\n    if CheckBookSourceItem(Item, True, edtLog.Lines) then\r\n      LogD('ϲ, ͨ!')\r\n    else\r\n      LogD('Դ쳣!');\r\n  finally\r\n    FreeAndNil(Item);\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.TaskFinish(AJob: PJob);\r\nvar\r\n  I: Integer;\r\nbegin\r\n  I := AtomicDecrement(FTaskRef);\r\n  if (I <= 0) or (FWaitStop > 0) then begin\r\n    if (I = 0) and Assigned(Self) and (not (csDestroying in ComponentState)) then begin\r\n      NotifyListChange();\r\n      Button1Click(Button1);\r\n    end;\r\n  end else if not (csDestroying in ComponentState) then begin\r\n    Log('УԴ...');       \r\n    Workers.SendSignal(FWaitCheckBookSourceSingId);\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.Timer1Timer(Sender: TObject);\r\nbegin\r\n  DispLog;\r\nend;\r\n\r\nprocedure TForm1.UpdateBookGroup(Item: TBookSourceItem);\r\nvar\r\n  J: Integer;   \r\n  ARef: Number;\r\n  ABookGroup: TArray<string>;\r\n  AGroup: string;\r\nbegin\r\n  ABookGroup := Item.GetGroupList;\r\n\r\n  for J := 0 to High(ABookGroup) do begin            \r\n    ARef := 0;\r\n    AGroup := Trim(ABookGroup[J]);\r\n    FBookGroups.TryGetValue(AGroup, ARef);\r\n    Inc(ARef);\r\n    FBookGroups.AddOrUpdate(AGroup, ARef);\r\n  end;\r\nend;\r\n\r\nprocedure TForm1.W1Click(Sender: TObject);\r\nbegin\r\n  EditData.WordWrap := not W1.Checked;\r\nend;\r\n\r\nprocedure TForm1.W2Click(Sender: TObject);\r\nbegin\r\n  ShellExecute(0, 'OPEN', PChar('http://www.cnblogs.com/yangyxd/'), nil, nil, SW_SHOWMAXIMIZED)\r\nend;\r\n\r\nprocedure TForm1.WaitCheckBookSource(AJob: PJob);\r\nvar\r\n  I, J: Integer;\r\nbegin\r\n  if FBookSrcData.Count > 0 then begin\r\n    FCheckCount := FBookSrcData.Count;\r\n    FCurCheckIndex := 0;\r\n    J := Min(FBookSrcData.Count, Workers.MaxWorkers - 1);\r\n    for I := 0 to J - 1 do begin\r\n      if AJob.IsTerminated then\r\n        Break;\r\n      Workers.Post(DoCheckBookSourceItem, nil);\r\n    end;\r\n  end else\r\n    Workers.Post(TaskFinish, nil, True);\r\nend;\r\n\r\nprocedure TForm1.WMDropFiles(var Msg: TWMDropFiles);\r\nbegin\r\n  AddSrcFiles(Msg.Drop);\r\nend;\r\n\r\nprocedure TForm1.X1Click(Sender: TObject);\r\nbegin\r\n  EditData.CopyToClipboard;\r\n  EditData.SelText := '';\r\nend;\r\n\r\nprocedure TForm1.Z1Click(Sender: TObject);\r\nbegin\r\n  EditData.Redo;\r\nend;\r\n\r\nend.\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmReplaceGroup.dfm",
    "content": "object frmReplaceGroup: TfrmReplaceGroup\r\n  Left = 0\r\n  Top = 0\r\n  BorderStyle = bsDialog\r\n  Caption = #26367#25442' - '#20070#28304#20998#32452\r\n  ClientHeight = 166\r\n  ClientWidth = 345\r\n  Color = clWindow\r\n  Font.Charset = DEFAULT_CHARSET\r\n  Font.Color = clWindowText\r\n  Font.Height = -11\r\n  Font.Name = 'Tahoma'\r\n  Font.Style = []\r\n  OldCreateOrder = False\r\n  Position = poMainFormCenter\r\n  DesignSize = (\r\n    345\r\n    166)\r\n  PixelsPerInch = 96\r\n  TextHeight = 13\r\n  object Label1: TLabel\r\n    Left = 24\r\n    Top = 16\r\n    Width = 60\r\n    Height = 13\r\n    Caption = #26597#25214#20869#23481#65306\r\n  end\r\n  object Label2: TLabel\r\n    Left = 24\r\n    Top = 63\r\n    Width = 60\r\n    Height = 13\r\n    Caption = #26367#25442#20869#23481#65306\r\n  end\r\n  object Edit1: TEdit\r\n    Left = 24\r\n    Top = 35\r\n    Width = 289\r\n    Height = 21\r\n    TabOrder = 0\r\n  end\r\n  object Edit2: TEdit\r\n    Left = 24\r\n    Top = 82\r\n    Width = 289\r\n    Height = 21\r\n    TabOrder = 1\r\n  end\r\n  object CheckBox1: TCheckBox\r\n    Left = 24\r\n    Top = 128\r\n    Width = 97\r\n    Height = 17\r\n    Caption = #20840#23383#21305#37197\r\n    Checked = True\r\n    State = cbChecked\r\n    TabOrder = 2\r\n  end\r\n  object Button1: TButton\r\n    Left = 239\r\n    Top = 125\r\n    Width = 75\r\n    Height = 25\r\n    Anchors = [akRight, akBottom]\r\n    Caption = #30830#23450'(&O)'\r\n    Default = True\r\n    TabOrder = 3\r\n    OnClick = Button1Click\r\n    ExplicitLeft = 234\r\n    ExplicitTop = 176\r\n  end\r\n  object Button2: TButton\r\n    Left = 151\r\n    Top = 125\r\n    Width = 75\r\n    Height = 25\r\n    Anchors = [akRight, akBottom]\r\n    Cancel = True\r\n    Caption = #21462#28040'(&C)'\r\n    ModalResult = 2\r\n    TabOrder = 4\r\n    ExplicitLeft = 146\r\n    ExplicitTop = 176\r\n  end\r\nend\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmReplaceGroup.pas",
    "content": "unit uFrmReplaceGroup;\r\n\r\ninterface\r\n\r\nuses\r\n  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,\r\n  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;\r\n\r\ntype\r\n  TfrmReplaceGroup = class(TForm)\r\n    Label1: TLabel;\r\n    Edit1: TEdit;\r\n    Label2: TLabel;\r\n    Edit2: TEdit;\r\n    CheckBox1: TCheckBox;\r\n    Button1: TButton;\r\n    Button2: TButton;\r\n    procedure Button1Click(Sender: TObject);\r\n  private\r\n    { Private declarations }\r\n  public\r\n    { Public declarations }\r\n  end;\r\n\r\nvar\r\n  frmReplaceGroup: TfrmReplaceGroup;\r\n\r\nfunction ShowReplaceGroup(Sender: TComponent; var FindStr, NewStr: string; var Flag: Integer): Boolean;\r\n\r\nimplementation\r\n\r\n{$R *.dfm}\r\n\r\nfunction ShowReplaceGroup(Sender: TComponent; var FindStr, NewStr: string; var Flag: Integer): Boolean;\r\nvar\r\n  F: TfrmReplaceGroup;\r\nbegin\r\n  F := TfrmReplaceGroup.Create(Sender);\r\n  try\r\n    Result := F.ShowModal = mrOk;\r\n    if Result then begin\r\n      FindStr := Trim(F.Edit1.Text);\r\n      NewStr := Trim(F.Edit2.Text);\r\n      Flag := Ord(F.CheckBox1.Checked);\r\n    end;\r\n  finally\r\n    F.Free;\r\n  end;\r\nend;\r\n\r\nprocedure TfrmReplaceGroup.Button1Click(Sender: TObject);\r\nbegin\r\n//  if Trim(Edit1.Text) = '' then begin\r\n//    ShowMessage('Ҫҵ');\r\n//    Exit;\r\n//  end;\r\n  ModalResult := mrOk;\r\nend;\r\n\r\nend.\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmWait.dfm",
    "content": "object Form2: TForm2\r\n  Left = 0\r\n  Top = 0\r\n  BorderStyle = bsNone\r\n  BorderWidth = 1\r\n  Caption = 'Form2'\r\n  ClientHeight = 173\r\n  ClientWidth = 251\r\n  Color = clSilver\r\n  Font.Charset = DEFAULT_CHARSET\r\n  Font.Color = clWindowText\r\n  Font.Height = -11\r\n  Font.Name = 'Tahoma'\r\n  Font.Style = []\r\n  OldCreateOrder = False\r\n  Position = poOwnerFormCenter\r\n  OnClose = FormClose\r\n  PixelsPerInch = 96\r\n  TextHeight = 13\r\n  object Panel1: TPanel\r\n    Left = 0\r\n    Top = 0\r\n    Width = 251\r\n    Height = 173\r\n    Align = alClient\r\n    BevelOuter = bvNone\r\n    Color = clWindow\r\n    ParentBackground = False\r\n    TabOrder = 0\r\n    object Label1: TLabel\r\n      Left = 73\r\n      Top = 88\r\n      Width = 103\r\n      Height = 13\r\n      Alignment = taCenter\r\n      Caption = #27491#22312#22788#29702', '#35831#31561#24453'...'\r\n    end\r\n    object ActivityIndicator1: TActivityIndicator\r\n      AlignWithMargins = True\r\n      Left = 112\r\n      Top = 32\r\n      Animate = True\r\n    end\r\n    object Button1: TButton\r\n      Left = 71\r\n      Top = 120\r\n      Width = 106\r\n      Height = 25\r\n      Cancel = True\r\n      Caption = #21462#28040'(&C)'\r\n      TabOrder = 1\r\n      OnClick = Button1Click\r\n    end\r\n  end\r\n  object Timer1: TTimer\r\n    Enabled = False\r\n    Interval = 100\r\n    OnTimer = Timer1Timer\r\n    Left = 184\r\n    Top = 56\r\n  end\r\nend\r\n"
  },
  {
    "path": "tool/书源整理工具/uFrmWait.pas",
    "content": "unit uFrmWait;\r\n\r\ninterface\r\n\r\nuses\r\n  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,\r\n  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.WinXCtrls,\r\n  Vcl.ExtCtrls;\r\n\r\ntype\r\n  TForm2 = class(TForm)\r\n    Panel1: TPanel;\r\n    ActivityIndicator1: TActivityIndicator;\r\n    Label1: TLabel;\r\n    Button1: TButton;\r\n    Timer1: TTimer;\r\n    procedure Button1Click(Sender: TObject);\r\n    procedure FormClose(Sender: TObject; var Action: TCloseAction);\r\n    procedure Timer1Timer(Sender: TObject);\r\n  private\r\n    { Private declarations }\r\n  public\r\n    { Public declarations }\r\n  end;\r\n\r\nvar\r\n  Form2: TForm2;\r\n\r\nprocedure ShowWaitDlg();\r\nprocedure HideWaitDlg();\r\n\r\nimplementation\r\n\r\n{$R *.dfm}\r\n\r\nuses\r\n  uFrmMain;\r\n\r\nvar\r\n  fWait: TForm2;\r\n\r\nprocedure ShowWaitDlg();\r\nbegin\r\n  if Assigned(fWait) then\r\n    Exit;\r\n  fWait := TForm2.Create(Application);\r\n  fWait.ShowModal;\r\nend;\r\n\r\nprocedure HideWaitDlg();\r\nbegin\r\n  if Assigned(fWait) then\r\n    fWait.Timer1.Enabled := True;\r\nend;\r\n\r\nprocedure TForm2.Button1Click(Sender: TObject);\r\nbegin\r\n  if Form1.Button1.Tag <> 0 then begin\r\n    Timer1.Enabled := True;\r\n    Form1.Button1Click(Button1);\r\n  end;\r\nend;\r\n\r\nprocedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);\r\nbegin\r\n  fWait := nil;\r\n  Action := caFree;\r\nend;\r\n\r\nprocedure TForm2.Timer1Timer(Sender: TObject);\r\nbegin\r\n  if Form1.Button1.Tag = 0 then begin\r\n    ModalResult := mrCancel;\r\n  end;\r\nend;\r\n\r\nend.\r\n"
  }
]