[
  {
    "path": ".editorconfig",
    "content": "[*.{kt,kts}]\nij_kotlin_imports_layout = *\nij_kotlin_line_break_after_multiline_when_entry = false\nktlint_code_style = intellij_idea\nktlint_function_naming_ignore_when_annotated_with = Composable\nktlint_standard_class-signature = disabled\nktlint_standard_mixed-condition-operators = disabled\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug 反馈 / Bug report\ndescription: 提交一个问题报告 / Create a bug report\nlabels:\n  - \"bug\"\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        感谢您愿意为 Ehviewer-NekoInverter 做出贡献！\n        提交问题报告前，还请首先完成文末的自查步骤\n\n        Thanks for your contribution to the Ehviewer-NekoInverter Project!\n        Please complete the self-review steps at the end of the article before submitting the bug report\n\n  - type: textarea\n    id: reproduce\n    attributes:\n      label: 复现步骤 / Steps to reproduce\n      description: |\n        在此处写下复现的方式，请详细描述每一个步骤，包括画廊链接、相关设置等\n        Describe how to reproduce here, please describe each step in detail, include gallery links or settings\n      placeholder: |\n        1.\n        2.\n        3.\n    validations:\n      required: true\n\n  - type: textarea\n    id : expected\n    attributes:\n      label: 预期行为 / Expected behaviour\n      description: |\n        在此处说明正常情况下应用的预期行为\n        Describe what should be happened here\n      placeholder: |\n        它应该 ...\n        It should be ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: actual\n    attributes:\n      label: 实际行为 / Actual behaviour\n      description: |\n        在此处描绘应用的实际行为，最好附上截图或录屏\n        Describe what actually happened here, screenshots or screen recordings are better\n      placeholder: |\n        实际上它 ...\n        Actually it ...\n        [截图或录屏] / [Screenshots or screen recordings]\n    validations:\n      required: true\n\n  - type: textarea\n    id: log\n    attributes:\n      label: 应用日志 / App logs\n      description: |\n        您可以通过设置-高级-导出日志 来获得日志文件，请确保日志完整，过长的日志请以文件形式上传\n        You can get logs file in Settings - Advanced - Dump logcat\n      placeholder: 06-15 17:44:53.704 23382 23382 E ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: more\n    attributes:\n      label: 备注 / Additional details\n      description: |\n        在此处写下其他您想说的内容\n        Describe additional details here\n      placeholder: |\n        其他有用的信息与附件\n        Additional details and attachments\n    validations:\n      required: false\n\n  - type: input\n    id: site\n    attributes:\n      label: 浏览站点 / Browsing site\n      description: E-Hentai / ExHentai\n      placeholder: E-Hentai\n    validations:\n      required: true\n\n  - type: input\n    id: version\n    attributes:\n      label: EhViewer 版本号 / EhViewer version code\n      description: |\n        您可以在设置 - 关于处找到版本号\n        You can get version code in Settings - About\n      placeholder: 1.7.28\n    validations:\n      required: true\n\n  - type: input\n    id: ci\n    attributes:\n      label: EhViewer CI 版本 / EhViewer CI version\n      description: |\n        请确保您已经使用 [最新 CI 版本](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml) 测试，请填入您使用的 CI 版本网址\n        Please make sure you have tested with the [latest CI version](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml), simply drop GitHub Action CI download page url here\n      placeholder: https://github.com/EhViewer-NekoInverter/EhViewer/actions/runs/XXXXXXXXXX\n    validations:\n      required: true\n\n  - type: input\n    id: system\n    attributes:\n      label: Android 系统版本 / Android version\n      description: Android 分支名称 + 版本号 / AOSP fork name + version code\n      placeholder: MIUI 12.5, ArrowOS 12.1\n    validations:\n      required: true\n\n  - type: input\n    id: device\n    attributes:\n      label: 设备型号 / Device model\n      description: 在此填入设备型号 / Put device model here\n      placeholder: OnePlus 7 Pro, Xiaomi 12 Ultra\n    validations:\n      required: true\n\n  - type: input\n    id: SoC\n    attributes:\n      label: SoC 型号 / SoC model\n      description: 在此填入 SoC 型号 / Put SoC model here\n      placeholder: 骁龙 8+ Gen 1, Snapdragon 8+ Gen 1\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: check\n    attributes:\n      label: 自查步骤 / Self-review steps\n      description: |\n        请确保您已经遵守以下所有必选项，否则 issue 会被立即关闭\n        Please ensure you have obtained all needed options, otherwise the issue will be closed immediately\n      options:\n      - label: 如果您有足够的时间和能力，并愿意为修复此问题提交 PR ，请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request\n        required: false\n\n      - label: 您已仔细查看并知情 [Q&A](https://github.com/EhViewer-NekoInverter/EhViewer/issues/18) 中的内容 / You have checked [Q&A](https://github.com/EhViewer-NekoInverter/EhViewer/issues/18) carefully\n        required: true\n\n      - label: 您已搜索过 [Issue Tracker](https://github.com/EhViewer-NekoInverter/EhViewer/issues)，没有找到类似的问题 / I have searched on [Issue Tracker](https://github.com/EhViewer-NekoInverter/EhViewer/issues), No duplicate or related open issue has been found\n        required: true\n\n      - label: 您确保这个 Issue 只提及一个问题。如果您有多个问题报告，烦请发起多个 Issue / Ensure there is only one bug report in this issue. Please make mutiply issue for mutiply bugs\n        required: true\n\n      - label: 您确保已使用 [最新 CI 版本](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml) 测试，并且该问题在最新 CI 版本中并未解决 / This bug have not solved in [latest CI version](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml)\n        required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n - name: 所有其他问题 / All other questions\n   url: https://www.google.com/\n   about: 咨询谷歌 / Ask Google for help"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - '*'\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  linux:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Java\n        uses: actions/setup-java@v4\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v4\n\n      - name: Spotless Check\n        run: ./gradlew spotlessCheck\n\n      - name: Gradle Build\n        run: ./gradlew assembleRelease\n\n      - name: Upload Universal\n        uses: actions/upload-artifact@v4\n        with:\n          name: universal-${{ github.sha }}\n          path: app/build/outputs/apk/release/app-universal-release.apk\n\n      - name: Upload ARM64\n        uses: actions/upload-artifact@v4\n        with:\n          name: arm64-v8a-${{ github.sha }}\n          path: app/build/outputs/apk/release/app-arm64-v8a-release.apk\n\n      - name: Upload ARM32\n        uses: actions/upload-artifact@v4\n        with:\n          name: armeabi-v7a-${{ github.sha }}\n          path: app/build/outputs/apk/release/app-armeabi-v7a-release.apk\n\n      - name: Upload x86_64\n        uses: actions/upload-artifact@v4\n        with:\n          name: x86_64-${{ github.sha }}\n          path: app/build/outputs/apk/release/app-x86_64-release.apk\n\n      - name: Upload mapping\n        uses: actions/upload-artifact@v4\n        with:\n          name: mapping-${{ github.sha }}\n          path: app/build/outputs/mapping/release/mapping.txt\n\n      - name: Upload native debug symbols\n        uses: actions/upload-artifact@v4\n        with:\n          name: native-debug-symbols-${{ github.sha }}\n          path: app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip\n"
  },
  {
    "path": ".github/workflows/releases.yml",
    "content": "name: Releases\n\non:\n  push:\n    tags:\n      - \"*\"\n\njobs:\n  linux:\n    name: Build\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Java\n        uses: actions/setup-java@v4\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v4\n\n      - name: Gradle Build\n        run: ./gradlew assembleRelease\n\n      - name: Rename Apks\n        run: |\n          mv app/build/outputs/apk/release/app-universal-release.apk EhViewer-${{ github.ref_name }}-universal.apk\n          mv app/build/outputs/apk/release/app-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-arm64-v8a.apk\n          mv app/build/outputs/apk/release/app-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-armeabi-v7a.apk\n          mv app/build/outputs/apk/release/app-x86_64-release.apk EhViewer-${{ github.ref_name }}-x86_64.apk\n          mv app/build/outputs/mapping/release/mapping.txt EhViewer-${{ github.ref_name }}-mapping.txt\n          mv app/build/outputs/native-debug-symbols/release/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-native-debug-symbols.zip\n\n      - name: Releases\n        uses: softprops/action-gh-release@v2\n        with:\n          body: Bump Version\n          files: |\n            EhViewer-${{ github.ref_name }}-universal.apk\n            EhViewer-${{ github.ref_name }}-arm64-v8a.apk\n            EhViewer-${{ github.ref_name }}-armeabi-v7a.apk\n            EhViewer-${{ github.ref_name }}-x86_64.apk\n            EhViewer-${{ github.ref_name }}-mapping.txt\n            EhViewer-${{ github.ref_name }}-native-debug-symbols.zip\n"
  },
  {
    "path": ".gitignore",
    "content": ".gradle\n/local.properties\n.DS_Store\n/build\n/src\n*.iml\n.idea\n/captures\nrelease\ngoogle-services.json\n/.kotlin\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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<https://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<https://www.gnu.org/licenses/why-not-lgpl.html>."
  },
  {
    "path": "NOTICE",
    "content": "EhViewer\nCopyright 2014-2016 Hippo Seven\n\nAn Unofficial E-Hentai Application for Android.\n\n香风智乃是最可爱的女孩子。\nchino kafuu is the cutest girl.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"right\">\n  <strong>English</strong>\n  <span> | </span>\n  <a href=\"/docs/README/zh-cn.md\">\n  简体中文\n  </a>\n</p>\n\n<h1 align=\"center\">\n  <img src=\"https://github.com/EhViewer-NekoInverter/Arts/blob/main/launcher_icon-web.webp\" width=\"150\" alt=\"EhViewer\">\n  <br>EhViewer<br>\n</h1>\n\n<p align=\"center\">\n  <a href=\"https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml\">\n    <img src=\"https://img.shields.io/github/actions/workflow/status/EhViewer-NekoInverter/EhViewer/ci.yml?style=flat-square\" alt=\"Github Actions\">\n  </a>\n  <a href=\"/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/EhViewer-NekoInverter/EhViewer?style=flat-square\" alt=\"LICENSE\">\n  </a>\n  <a href=\"https://github.com/EhViewer-NekoInverter/Ehviewer/releases\">\n    <img src=\"https://img.shields.io/github/v/release/EhViewer-NekoInverter/Ehviewer?style=flat-square&include_prereleases\" alt=\"Releases\">\n  </a>\n  <a href=\"https://github.com/EhViewer-NekoInverter/EhViewer/issues\">\n    <img src=\"https://img.shields.io/github/issues/EhViewer-NekoInverter/EhViewer?style=flat-square\" alt=\"Issues\">\n  </a>\n</p>\n\n<div align=\"center\">\n  <h3>\n    <a href=\"#description\">\n    Description\n    </a>\n    <span> | </span>\n    <a href=\"#download\">\n    Download\n    </a>\n    <span> | </span>\n    <a href=\"#screenshot\">\n    Screenshot\n    </a>\n    <span> | </span>\n    <a href=\"#thanks\">\n    Thanks\n    </a>\n    <span> | </span>\n    <a href=\"#license\">\n    License\n    </a>\n  </h3>\n</div>\n\n# Description\n\nAn EhViewer fork with classic Material Design 2 style\n\nThis fork is for personal use and does not accept feature requests; for common usage issues, please refer to the [Q&A](https://github.com/EhViewer-NekoInverter/EhViewer/issues/18)\n\nIf you prefer Material Design 3, consider using [EhViewer-Overhauled](https://github.com/FooIbar/EhViewer)\n\n# Download\n\n| Android Version | Notes                        |\n|-----------------|------------------------------|\n| 6.0-8.1         | No support for animated WebP |\n| 9.0+            | Full support                 |\n\n- Please go to **[GitHub Releases](https://github.com/EhViewer-NekoInverter/EhViewer/releases)** to download the release version\n\n- If the release version has unresolved issues, go to **[GitHub Actions](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml?query=branch%3Amaster)** to download the CI version (GitHub account login required)\n\n# Screenshot\n\n![screenshot-01](https://github.com/EhViewer-NekoInverter/Arts/blob/main/screenshot-01.webp)\n![screenshot-02](https://github.com/EhViewer-NekoInverter/Arts/blob/main/screenshot-02.webp)\n\n# Thanks\n\nHere are the libraries\n\n- [AOSP & AndroidX](https://source.android.com/)\n- [Kotlin & KotlinX](https://kotlinlang.org/)\n- [Coil](https://coil-kt.github.io/coil/)\n- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)\n- [Jsoup](https://jsoup.org/)\n- [Ktor](https://ktor.io/)\n- [Libarchive](http://www.libarchive.org/)\n- [MDC-Android](https://github.com/material-components/material-components-android)\n- [OkHttp](https://square.github.io/okhttp/)\n- [RikkaX](https://github.com/RikkaApps/RikkaX)\n\nTag translation\n\n- [EhTagTranslation](https://github.com/EhTagTranslation/Database)\n\nTranslators\n\n- ja: [Re*Index. (ot_inc)](https://github.com/reindex-ot)\n\n# License\n\n    Copyright 2014-2019 Hippo Seven\n    Copyright 2020-2022 NekoInverter\n    Copyright 2022-2025 Moedog\n\n    EhViewer is free software:\n    you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,\n    either version 3 of the License, or (at your option) any later version.\n\n    EhViewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n    without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n    See the GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along with EhViewer.\n    If not, see <https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n/src/main/java-gen\nmanifest-merger-release-report.txt\n/src/main/libs\n/src/main/obj\n# Intellij\n*.iml\n/.cxx\n"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "import java.time.Instant\nimport java.time.ZoneOffset\nimport java.time.format.DateTimeFormatter\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nval isRelease: Boolean\n    get() = gradle.startParameter.taskNames.any { it.contains(\"Release\") }\nval supportedAbis = arrayOf(\"arm64-v8a\", \"x86_64\", \"armeabi-v7a\")\n\nplugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.kotlin.android)\n    alias(libs.plugins.kotlin.parcelize)\n    alias(libs.plugins.kotlin.serialization)\n    alias(libs.plugins.ksp)\n    alias(libs.plugins.spotless)\n}\n\n@Suppress(\"UnstableApiUsage\")\nandroid {\n    androidResources {\n        localeFilters += listOf(\n            \"zh\",\n            \"zh-rCN\",\n            \"zh-rHK\",\n            \"zh-rTW\",\n            \"ja\",\n        )\n    }\n\n    splits {\n        abi {\n            isEnable = true\n            reset()\n            if (isRelease) {\n                include(*supportedAbis)\n                isUniversalApk = true\n            } else {\n                include(\"x86_64\", \"x86\")\n            }\n        }\n    }\n\n    val signConfig = signingConfigs.create(\"release\") {\n        storeFile = File(projectDir.path + \"/keystore/androidkey.jks\")\n        storePassword = \"000000\"\n        keyAlias = \"key0\"\n        keyPassword = \"000000\"\n        enableV3Signing = true\n        enableV4Signing = true\n    }\n\n    val commitSha by lazy {\n        val stdout = providers.exec {\n            commandLine = \"git rev-parse --short=7 HEAD\".split(' ')\n        }.standardOutput\n        stdout.asText.get().trim()\n    }\n\n    val buildTime by lazy {\n        val formatter = DateTimeFormatter.ofPattern(\"yyyy/MM/dd HH:mm\").withZone(ZoneOffset.UTC)\n        formatter.format(Instant.now())\n    }\n\n    defaultConfig {\n        applicationId = \"org.moedog.ehviewer\"\n        versionCode = 180014\n        versionName = \"1.8.13\"\n        buildConfigField(\"String\", \"VERSION_CODE\", \"\\\"${defaultConfig.versionCode}\\\"\")\n        buildConfigField(\"String\", \"COMMIT_SHA\", \"\\\"$commitSha\\\"\")\n        ndk {\n            if (isRelease) {\n                abiFilters.addAll(supportedAbis)\n            }\n            debugSymbolLevel = \"FULL\"\n        }\n    }\n\n    externalNativeBuild {\n        cmake {\n            path = File(\"src/main/cpp/CMakeLists.txt\")\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n\n    lint {\n        abortOnError = true\n        checkReleaseBuilds = false\n        disable.add(\"MissingTranslation\")\n    }\n\n    packaging {\n        resources {\n            excludes += \"/META-INF/**\"\n            excludes += \"/kotlin/**\"\n            excludes += \"**.txt\"\n            excludes += \"**.bin\"\n        }\n    }\n\n    dependenciesInfo.includeInApk = false\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            isShrinkResources = true\n            proguardFiles(\"proguard-rules.pro\")\n            signingConfig = signConfig\n            buildConfigField(\"String\", \"BUILD_TIME\", \"\\\"$buildTime\\\"\")\n        }\n        debug {\n            applicationIdSuffix = \".debug\"\n            buildConfigField(\"String\", \"BUILD_TIME\", \"\\\"\\\"\")\n        }\n    }\n\n    buildFeatures {\n        buildConfig = true\n    }\n\n    namespace = \"com.hippo.ehviewer\"\n}\n\ndependencies {\n    // https://developer.android.com/jetpack/androidx/releases/activity\n    implementation(libs.androidx.activity)\n    implementation(libs.androidx.appcompat)\n    implementation(libs.androidx.biometric)\n    implementation(libs.androidx.browser)\n    implementation(libs.androidx.collection)\n    implementation(libs.androidx.webkit)\n\n    implementation(libs.androidx.core)\n\n    implementation(libs.androidx.coordinatorlayout)\n    implementation(libs.androidx.fragment)\n    // https://developer.android.com/jetpack/androidx/releases/lifecycle\n    implementation(libs.androidx.lifecycle.process)\n\n    // https://developer.android.com/jetpack/androidx/releases/paging\n    implementation(libs.androidx.paging.runtime)\n    implementation(libs.androidx.preference)\n    implementation(libs.androidx.recyclerview)\n\n    // https://developer.android.com/jetpack/androidx/releases/room\n    ksp(libs.androidx.room.compiler)\n    implementation(libs.androidx.room.paging)\n\n    implementation(libs.androidx.swiperefreshlayout)\n    implementation(libs.drawer)\n    implementation(libs.material)\n\n    // https://square.github.io/okhttp/changelogs/changelog/\n    implementation(platform(libs.okhttp.bom))\n    implementation(libs.okhttp.coroutines)\n    implementation(libs.okhttp.tls)\n\n    implementation(libs.okio.jvm)\n\n    // https://github.com/RikkaApps/RikkaX\n    implementation(libs.bundles.rikkax)\n\n    // https://coil-kt.github.io/coil/changelog/\n    implementation(platform(libs.coil.bom))\n    implementation(libs.bundles.coil)\n\n    implementation(libs.kotlinx.coroutines.android)\n    implementation(libs.kotlinx.datetime)\n    implementation(libs.kotlinx.serialization.cbor)\n    implementation(libs.ktor.utils)\n    implementation(libs.jsoup)\n\n    coreLibraryDesugaring(libs.desugar)\n}\n\nkotlin {\n    compilerOptions {\n        jvmTarget = JvmTarget.JVM_21\n        progressiveMode = true\n        optIn.addAll(\n            \"coil3.annotation.ExperimentalCoilApi\",\n            \"kotlin.contracts.ExperimentalContracts\",\n            \"kotlin.time.ExperimentalTime\",\n            \"kotlinx.coroutines.ExperimentalCoroutinesApi\",\n            \"kotlinx.coroutines.FlowPreview\",\n            \"kotlinx.coroutines.InternalCoroutinesApi\",\n            \"kotlinx.serialization.ExperimentalSerializationApi\",\n        )\n    }\n}\n\nconfigurations.all {\n    exclude(\"dev.rikka.rikkax.appcompat\", \"appcompat\")\n}\n\nksp {\n    arg(\"room.schemaLocation\", \"$projectDir/schemas\")\n    arg(\"room.generateKotlin\", \"true\")\n}\n\nval ktlintVersion = libs.ktlint.get().version\n\nspotless {\n    kotlin {\n        // https://github.com/diffplug/spotless/issues/111\n        target(\"src/**/*.kt\")\n        ktlint(ktlintVersion)\n    }\n    kotlinGradle {\n        ktlint(ktlintVersion)\n    }\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "-keepclassmembers class * implements android.os.Parcelable {\n    public static final ** CREATOR;\n}\n\n-keepclasseswithmembernames,includedescriptorclasses class * {\n    native <methods>;\n}\n\n-keepattributes LineNumberTable,SourceFile\n-renamesourcefileattribute SourceFile\n\n-repackageclasses\n-allowaccessmodification\n-overloadaggressively\n"
  },
  {
    "path": "app/schemas/com.hippo.network.CookiesDatabase/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"c801ee62729ba20a203b150211234d5c\",\n    \"entities\": [\n      {\n        \"tableName\": \"OK_HTTP_3_COOKIE\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT, `VALUE` TEXT, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT, `PATH` TEXT, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"NAME\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"VALUE\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"expiresAt\",\n            \"columnName\": \"EXPIRES_AT\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domain\",\n            \"columnName\": \"DOMAIN\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"path\",\n            \"columnName\": \"PATH\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"secure\",\n            \"columnName\": \"SECURE\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"httpOnly\",\n            \"columnName\": \"HTTP_ONLY\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"persistent\",\n            \"columnName\": \"PERSISTENT\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"hostOnly\",\n            \"columnName\": \"HOST_ONLY\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"_id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c801ee62729ba20a203b150211234d5c')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.hippo.network.CookiesDatabase/2.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 2,\n    \"identityHash\": \"1b80cd29939b0f43934721f1289ba94d\",\n    \"entities\": [\n      {\n        \"tableName\": \"OK_HTTP_3_COOKIE\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT NOT NULL, `VALUE` TEXT NOT NULL, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT NOT NULL, `PATH` TEXT NOT NULL, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"NAME\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"value\",\n            \"columnName\": \"VALUE\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"expiresAt\",\n            \"columnName\": \"EXPIRES_AT\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"domain\",\n            \"columnName\": \"DOMAIN\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"path\",\n            \"columnName\": \"PATH\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"secure\",\n            \"columnName\": \"SECURE\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"httpOnly\",\n            \"columnName\": \"HTTP_ONLY\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"persistent\",\n            \"columnName\": \"PERSISTENT\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"hostOnly\",\n            \"columnName\": \"HOST_ONLY\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"_id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"_id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b80cd29939b0f43934721f1289ba94d')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/src/debug/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <string name=\"app_name\" translatable=\"false\">EhViewer Debug</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-feature android:name=\"android.software.leanback\" android:required=\"false\" />\n    <uses-feature android:name=\"android.hardware.touchscreen\" android:required=\"false\" />\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.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_DATA_SYNC\" />\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"28\" />\n\n    <uses-feature\n        android:glEsVersion=\"0x00030000\"\n        android:required=\"true\" />\n\n    <application\n        android:name=\"com.hippo.ehviewer.EhApplication\"\n        android:allowBackup=\"true\"\n        android:appCategory=\"image\"\n        android:dataExtractionRules=\"@xml/data_extraction_rules\"\n        android:enableOnBackInvokedCallback=\"false\"\n        android:fullBackupContent=\"@xml/backup_scheme\"\n        android:hasFragileUserData=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:localeConfig=\"@xml/locale_config\"\n        android:largeHeap=\"true\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:theme=\"@style/AppTheme\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <activity\n            android:name=\"com.hippo.ehviewer.ui.MainActivity\"\n            android:banner=\"@mipmap/ic_leanback_banner\"\n            android:configChanges=\"screenSize\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTask\"\n            android:windowSoftInputMode=\"adjustResize|stateAlwaysHidden\">\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.app.shortcuts\"\n                android:resource=\"@xml/shortcuts\" />\n\n            <intent-filter android:autoVerify=\"true\">\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"http\" />\n                <data android:scheme=\"https\" />\n                <data android:host=\"exhentai.org\" />\n                <data android:host=\"e-hentai.org\" />\n            </intent-filter>\n\n            <intent-filter android:label=\"@string/keyword_search\">\n                <action android:name=\"android.intent.action.SEND\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <data android:mimeType=\"text/plain\" />\n            </intent-filter>\n\n            <intent-filter android:label=\"@string/image_search\">\n                <action android:name=\"android.intent.action.SEND\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <data android:mimeType=\"image/*\" />\n            </intent-filter>\n        </activity>\n\n        <activity\n            android:name=\"com.hippo.ehviewer.ui.GalleryActivity\"\n            android:configChanges=\"screenSize\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTask\"\n            android:theme=\"@style/AppTheme.Gallery\">\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"file\" />\n                <data android:scheme=\"content\" />\n                <data android:host=\"*\" />\n                <data android:mimeType=\"application/octet-stream\" />\n                <data android:mimeType=\"application/7z\" />\n                <data android:mimeType=\"application/rar\" />\n                <data android:mimeType=\"application/zip\" />\n                <data android:mimeType=\"application/x-7z-compressed\" />\n                <data android:mimeType=\"application/x-rar-compressed\" />\n                <data android:mimeType=\"application/x-zip-compressed\" />\n                <data android:mimeType=\"application/x-tar\" />\n                <data android:mimeType=\"application/x-xz\" />\n                <data android:mimeType=\"application/gzip\" />\n                <data android:mimeType=\"application/x-cbz\" />\n                <data android:mimeType=\"application/x-cbr\" />\n                <data android:mimeType=\"application/x-cbt\" />\n                <data android:mimeType=\"application/x-cb7\" />\n\n                <!-- untested -->\n                <data android:mimeType=\"application/vnd.comicbook+zip\" />\n                <data android:mimeType=\"application/vnd.comicbook-rar\" />\n                <data android:mimeType=\"application/x-compressed-tar\" />\n                <data android:mimeType=\"application/x-bzip-compressed-tar\" />\n                <data android:mimeType=\"application/x-lzma-compressed-tar\" />\n                <data android:mimeType=\"application/x-xz-compressed-tar\" />\n            </intent-filter>\n        </activity>\n\n        <activity\n            android:name=\"com.hippo.ehviewer.ui.SettingsActivity\"\n            android:configChanges=\"screenSize\"\n            android:label=\"@string/settings\"\n            android:theme=\"@style/AppTheme\" />\n\n        <activity\n            android:name=\"com.hippo.ehviewer.shortcuts.ShortcutsActivity\"\n            android:excludeFromRecents=\"true\"\n            android:launchMode=\"singleTask\"\n            android:taskAffinity=\"\"\n            android:theme=\"@android:style/Theme.Translucent.NoTitleBar\" />\n        <activity\n            android:name=\".ui.WebViewActivity\" />\n\n        <service\n            android:name=\"androidx.appcompat.app.AppLocalesMetadataHolderService\"\n            android:enabled=\"false\"\n            android:exported=\"false\">\n            <meta-data\n                android:name=\"autoStoreLocales\"\n                android:value=\"true\" />\n        </service>\n\n        <service\n            android:name=\"com.hippo.ehviewer.download.DownloadService\"\n            android:foregroundServiceType=\"dataSync\"\n            android:label=\"@string/download_service_label\" />\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/filepaths\" />\n\n        </provider>\n    </application>\n</manifest>\n"
  },
  {
    "path": "app/src/main/cpp/0001-Insert-link-libs.patch",
    "content": "From aee8f7147ddb262fd9e5feee8d5b17094cf3470f Mon Sep 17 00:00:00 2001\nFrom: FooIbar <118464521+FooIbar@users.noreply.github.com>\nDate: Tue, 26 Sep 2023 00:09:25 +0800\nSubject: [PATCH] Insert link libs\n\n---\n CMakeLists.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/CMakeLists.txt b/CMakeLists.txt\nindex ec97e4c7..420f204c 100644\n--- a/CMakeLists.txt\n+++ b/CMakeLists.txt\n@@ -438,7 +438,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS \"${__GNUWIN32PATH}\")\n   # #  endif\n ENDIF(DEFINED __GNUWIN32PATH AND EXISTS \"${__GNUWIN32PATH}\")\n \n-SET(ADDITIONAL_LIBS \"\")\n+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})\n #\n # Find ZLIB\n #\n-- \n2.34.1\n\n"
  },
  {
    "path": "app/src/main/cpp/0002-Fix-zip_time-performance.patch",
    "content": "From e971b9f23727833cc39b9e325db30f383e5bfc30 Mon Sep 17 00:00:00 2001\nFrom: Dude so hot <djohn@fbi.gov>\nDate: Thu, 14 Nov 2024 00:28:13 +0800\nSubject: [PATCH] Fix zip_time performance\n\nSigned-off-by: Dude so hot <djohn@fbi.gov>\n---\n libarchive/archive_time.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/libarchive/archive_time.c b/libarchive/archive_time.c\nindex 3352c809..9289bf1e 100644\n--- a/libarchive/archive_time.c\n+++ b/libarchive/archive_time.c\n@@ -53,6 +53,7 @@ FILETIME_to_ntfs(const FILETIME* filetime)\n int64_t\n dos_to_unix(uint32_t dos_time)\n {\n+\treturn 0;\n \tuint16_t msTime, msDate;\n \tstruct tm ts;\n \ttime_t t;\n-- \n2.47.0\n\n"
  },
  {
    "path": "app/src/main/cpp/0003-Use-UTF-8-as-default-charset-on-bionic.patch",
    "content": "From 86d199429b3fabc83f7dcc9afd72e7b4c7750b1a Mon Sep 17 00:00:00 2001\nFrom: FooIbar <118464521+FooIbar@users.noreply.github.com>\nDate: Thu, 14 Nov 2024 20:22:52 +0800\nSubject: [PATCH] Use UTF-8 as default charset on bionic\n\n---\n libarchive/archive_string.c | 4 +++-\n 1 file changed, 3 insertions(+), 1 deletion(-)\n\ndiff --git a/libarchive/archive_string.c b/libarchive/archive_string.c\nindex abf7ad66..63a73c58 100644\n--- a/libarchive/archive_string.c\n+++ b/libarchive/archive_string.c\n@@ -423,7 +423,9 @@ static const char *\n default_iconv_charset(const char *charset) {\n \tif (charset != NULL && charset[0] != '\\0')\n \t\treturn charset;\n-#if HAVE_LOCALE_CHARSET && !defined(__APPLE__)\n+#ifdef __BIONIC__\n+\treturn \"UTF-8\";\n+#elif HAVE_LOCALE_CHARSET && !defined(__APPLE__)\n \t/* locale_charset() is broken on Mac OS */\n \treturn locale_charset();\n #elif HAVE_NL_LANGINFO\n-- \n2.43.0\n\n"
  },
  {
    "path": "app/src/main/cpp/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.14)\nproject(ehviewer C)\ninclude(FetchContent)\n\nif (NOT CMAKE_BUILD_TYPE STREQUAL \"Debug\")\n    set(CMAKE_C_FLAGS \"${CMAKE_C_FLAGS} -Ofast -fvisibility=hidden -fvisibility-inlines-hidden -funroll-loops -flto \\\n           -mllvm -polly \\\n           -mllvm -polly-run-dce \\\n           -mllvm -polly-run-inliner \\\n           -mllvm -polly-isl-arg=--no-schedule-serialize-sccs \\\n           -mllvm -polly-ast-use-context \\\n           -mllvm -polly-detect-keep-going \\\n           -mllvm -polly-position=before-vectorizer \\\n           -mllvm -polly-vectorizer=stripmine \\\n           -mllvm -polly-detect-profitability-min-per-loop-insts=40 \\\n           -mllvm -polly-invariant-load-hoisting\")\nendif (NOT CMAKE_BUILD_TYPE STREQUAL \"Debug\")\n\noption(BUILD_TESTING OFF)\noption(XZ_DOC OFF)\noption(XZ_LZIP_DECODER OFF)\noption(XZ_MICROLZMA_DECODER OFF)\noption(XZ_MICROLZMA_ENCODER OFF)\noption(XZ_TOOL_LZMADEC OFF)\noption(XZ_TOOL_LZMAINFO OFF)\noption(XZ_TOOL_XZ OFF)\noption(XZ_TOOL_XZDEC OFF)\nFetchContent_Declare(\n        liblzma\n        GIT_REPOSITORY https://github.com/tukaani-project/xz.git\n        GIT_TAG v5.8.1\n        GIT_SHALLOW 1\n)\n\nFetchContent_MakeAvailable(liblzma)\ninclude_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)\n\n# Build GNUTLS libnettle\nFetchContent_Declare(\n        nettle\n        URL https://ftp.gnu.org/gnu/nettle/nettle-3.10.2.tar.gz\n        URL_MD5 b28bcbf6f045ff007940a9401673600d\n        SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/nettle/nettle\n)\n\nFetchContent_MakeAvailable(nettle)\nadd_subdirectory(nettle)\n\n# Configure libnettle support for libarchive\ninclude_directories(nettle)\ninclude_directories(.)\nset(HAVE_LIBNETTLE 1)\nset(HAVE_NETTLE_AES_H 1)\nset(HAVE_NETTLE_HMAC_H 1)\nset(HAVE_NETTLE_MD5_H 1)\nset(HAVE_NETTLE_PBKDF2_H 1)\nset(HAVE_NETTLE_RIPEMD160_H 1)\nset(HAVE_NETTLE_SHA_H 1)\n\n# Configure lzma support for libarchive\nSET(HAVE_LIBLZMA 1)\nSET(HAVE_LZMA_H 1)\nSET(HAVE_LZMA_STREAM_ENCODER_MT 1)\nSET(HAVE_LZMADEC_H 1)\nSET(HAVE_LIBLZMADEC 1)\n\noption(ENABLE_OPENSSL OFF)\noption(ENABLE_TAR OFF)\noption(ENABLE_CPIO OFF)\noption(ENABLE_CAT OFF)\noption(ENABLE_UNZIP OFF)\noption(ENABLE_TEST OFF)\n\n# Configure libarchive link's static lib\nSET(LIBARCHIVE_CUSTOM_LIBS \"nettle\" \"liblzma\")\n\nset(LIBARCHIVE_PATCH\n        ${CMAKE_CURRENT_LIST_DIR}/0001-Insert-link-libs.patch\n        ${CMAKE_CURRENT_LIST_DIR}/0002-Fix-zip_time-performance.patch\n        ${CMAKE_CURRENT_LIST_DIR}/0003-Use-UTF-8-as-default-charset-on-bionic.patch\n)\nFetchContent_Declare(\n        libarchive\n        GIT_REPOSITORY https://github.com/libarchive/libarchive.git\n        GIT_TAG v3.8.1\n        GIT_SHALLOW 1\n        PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}\n)\n\nFetchContent_MakeAvailable(libarchive)\ninclude_directories(${libarchive_SOURCE_DIR}/libarchive)\n\n# Build and link our app's native lib\nadd_library(${PROJECT_NAME} SHARED archive.c image.c gifutils.c hash.c natsort/strnatcmp.c)\ntarget_link_libraries(${PROJECT_NAME} archive_static log jnigraphics GLESv3 -Wl,--exclude-libs,ALL)\n"
  },
  {
    "path": "app/src/main/cpp/archive.c",
    "content": "/*\n * Copyright 2022-2024 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for\n * more details.\n *\n * You should have received a copy of the GNU General Public License along with\n * EhViewer. If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <stdbool.h>\n#include <stdlib.h>\n#include <string.h>\n#include <errno.h>\n#include <pthread.h>\n#include <sys/mman.h>\n\n#include <jni.h>\n#include <android/log.h>\n\n#include <archive.h>\n#include <archive_entry.h>\n\n#define LOG_TAG \"libarchive_wrapper\"\n\n#include \"natsort/strnatcmp.h\"\n#include \"ehviewer.h\"\n\ntypedef struct {\n    int using;\n    int next_index;\n    struct archive *arc;\n    struct archive_entry *entry;\n} archive_ctx;\n\ntypedef struct {\n    const char *filename;\n    int index;\n    ssize_t size;\n    void *addr;\n} entry;\n\n#define CTX_POOL_SIZE 20\n#define MAX_PARALLEL_DECOMP 4\n#define max(a, b) ((a) > (b) ? (a) : (b))\n\nstatic pthread_mutex_t ctx_pool_mutex = PTHREAD_MUTEX_INITIALIZER;\nstatic archive_ctx **ctx_pool = NULL;\nstatic pthread_mutex_t buffer_mutex = PTHREAD_MUTEX_INITIALIZER;\nstatic void *decode_buffer[MAX_PARALLEL_DECOMP];\nstatic bool need_encrypt = false;\nstatic char *passwd = NULL;\nstatic void *archiveAddr = MAP_FAILED;\nstatic size_t archiveSize = 0;\nstatic entry *entries = NULL;\nstatic size_t entryCount = 0;\nstatic ssize_t max_file_size = 0;\n\n#define SUPPORT_EXT_COUNT 11\n\nconst char supportExt[SUPPORT_EXT_COUNT][5] = {\n        \"jpeg\",\n        \"jpg\",\n        \"png\",\n        \"gif\",\n        \"webp\",\n        \"bmp\",\n        \"ico\",\n        \"wbmp\",\n        \"heic\",\n        \"heif\",\n        \"avif\"\n};\n\nstatic inline int filename_is_playable_file(const char *name) {\n    if (!name)\n        return false;\n    const char *dotptr = strrchr(name, '.');\n    if (!dotptr++)\n        return false;\n    int i;\n    for (i = 0; i < SUPPORT_EXT_COUNT; i++)\n        if (strcmp(dotptr, supportExt[i]) == 0)\n            return true;\n    return false;\n}\n\nstatic inline bool archive_entry_is_file(struct archive_entry *entry) {\n    return archive_entry_filetype(entry) == AE_IFREG;\n}\n\nstatic inline bool archive_entry_is_playable(struct archive_entry *entry) {\n    return archive_entry_is_file(entry) &&\n           filename_is_playable_file(archive_entry_pathname(entry));\n}\n\nstatic inline int compare_entries(const void *a, const void *b) {\n    const char *fa = ((entry *) a)->filename;\n    const char *fb = ((entry *) b)->filename;\n    return strnatcmp(fa, fb);\n}\n\n#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)\n\nstatic bool fill_entry_zero_copy(struct archive *arc, entry *entry) {\n    void *buffer = NULL;\n    size_t buffer_size = 0;\n    la_int64_t output_ofs = 0;\n    archive_read_data_block(arc, (const void **) &buffer, &buffer_size, &output_ofs);\n    bool zero_copy = ADDR_IN_FILE_MAPPING(buffer) && !output_ofs && buffer_size == entry->size;\n    entry->addr = zero_copy ? buffer : NULL;\n    return zero_copy;\n}\n\nstatic void archive_map_entries_index(archive_ctx *ctx, bool sort) {\n    int count = 0;\n    bool zero_copy = true;\n    while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {\n        const char *name = archive_entry_pathname(ctx->entry);\n        if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {\n            entries[count].filename = strdup(name);\n            entries[count].index = count;\n            ssize_t size = archive_entry_size(ctx->entry);\n            max_file_size = max(size, max_file_size);\n            entries[count].size = size;\n            // We don't expect zero copy if first content can't do zero copy\n            if (zero_copy) zero_copy = fill_entry_zero_copy(ctx->arc, &entries[count]);\n            count++;\n        }\n    }\n    if (sort) qsort(entries, entryCount, sizeof(entry), compare_entries);\n}\n\nstatic void *acquire_decode_buffer() {\n    void *addr = NULL;\n    pthread_mutex_lock(&buffer_mutex);\n    for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {\n        addr = decode_buffer[i];\n        if (addr) {\n            decode_buffer[i] = NULL;\n            break;\n        }\n    }\n    pthread_mutex_unlock(&buffer_mutex);\n    if (!addr) addr = malloc(max_file_size);\n    return addr;\n}\n\nstatic void release_decode_buffer(void *buffer) {\n    pthread_mutex_lock(&buffer_mutex);\n    for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {\n        void *addr = decode_buffer[i];\n        if (!addr) {\n            decode_buffer[i] = buffer;\n            pthread_mutex_unlock(&buffer_mutex);\n            return;\n        }\n    }\n    pthread_mutex_unlock(&buffer_mutex);\n    free(buffer);\n}\n\nstatic int archive_list_all_entries(archive_ctx *ctx) {\n    int count = 0;\n    while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)\n        if (archive_entry_is_playable(ctx->entry))\n            count++;\n    return count;\n}\n\nstatic void archive_release_ctx(archive_ctx *ctx) {\n    if (ctx) {\n        archive_read_close(ctx->arc);\n        archive_read_free(ctx->arc);\n        free(ctx);\n    }\n}\n\nstatic archive_ctx *archive_alloc_ctx() {\n    archive_ctx *ctx = calloc(1, sizeof(archive_ctx));\n    ctx->arc = archive_read_new();\n    ctx->using = 1;\n    archive_read_support_format_tar(ctx->arc);\n    archive_read_support_format_7zip(ctx->arc);\n    archive_read_support_format_rar5(ctx->arc);\n    archive_read_support_format_zip(ctx->arc);\n    archive_read_support_filter_gzip(ctx->arc);\n    archive_read_support_filter_xz(ctx->arc);\n    archive_read_set_option(ctx->arc, \"zip\", \"ignorecrc32\", \"1\");\n    if (passwd)\n        archive_read_add_passphrase(ctx->arc, passwd);\n    int err = archive_read_open_memory(ctx->arc, archiveAddr, archiveSize);\n    if (err < ARCHIVE_OK) {\n        LOGE(\"%s%s\", \"Open archive failed: \", archive_error_string(ctx->arc));\n        archive_read_free(ctx->arc);\n        free(ctx);\n        return NULL;\n    }\n    return ctx;\n}\n\nstatic int archive_skip_to_index(archive_ctx *ctx, int index) {\n    while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {\n        if (!archive_entry_is_playable(ctx->entry))\n            continue;\n        if (ctx->next_index++ == index) {\n            return ctx->next_index - 1;\n        }\n    }\n    return ARCHIVE_FATAL;\n}\n\nstatic int archive_get_ctx(archive_ctx **ctxptr, int idx) {\n    int ret;\n    archive_ctx *ctx = NULL;\n    pthread_mutex_lock(&ctx_pool_mutex);\n    for (int i = 0; i < CTX_POOL_SIZE; i++) {\n        if (!ctx_pool[i])\n            continue;\n        if (ctx_pool[i]->using)\n            continue;\n        if (ctx_pool[i]->next_index > idx)\n            continue;\n        if (!ctx || ctx_pool[i]->next_index > ctx->next_index)\n            ctx = ctx_pool[i];\n        if (ctx->next_index == idx)\n            break;\n    }\n    if (ctx)\n        ctx->using = 1;\n    pthread_mutex_unlock(&ctx_pool_mutex);\n\n    if (!ctx) {\n        archive_ctx *victimCtx = NULL;\n        int victimIdx = 0;\n        int replace = 1;\n        ctx = archive_alloc_ctx();\n        pthread_mutex_lock(&ctx_pool_mutex);\n        for (int i = 0; i < CTX_POOL_SIZE; i++) {\n            if (!ctx_pool[i]) {\n                ctx_pool[i] = ctx;\n                replace = 0;\n                break;\n            }\n            if (ctx_pool[i]->using)\n                continue;\n            if (!victimCtx || ctx_pool[i]->next_index > victimCtx->next_index) {\n                victimCtx = ctx_pool[i];\n                victimIdx = i;\n            }\n        }\n        if (replace) ctx_pool[victimIdx] = ctx;\n        pthread_mutex_unlock(&ctx_pool_mutex);\n        if (replace) archive_release_ctx(victimCtx);\n    }\n    ret = archive_skip_to_index(ctx, idx);\n    if (ret != idx) {\n        ret = archive_errno(ctx->arc);\n        LOGE(\"Skip to index failed: %s\", archive_error_string(ctx->arc));\n        archive_release_ctx(ctx);\n        return ret;\n    }\n    *ctxptr = ctx;\n    return 0;\n}\n\nJNIEXPORT jint JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {\n    EH_UNUSED(env);\n    EH_UNUSED(thiz);\n    archive_ctx *ctx = NULL;\n    archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);\n    if (archiveAddr == MAP_FAILED) {\n        LOGE(\"%s%s\", \"mmap failed with error \", strerror(errno));\n        return 0;\n    }\n    archiveSize = size;\n    ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));\n    ctx = archive_alloc_ctx();\n    if (!ctx) return 0;\n\n    entryCount = archive_list_all_entries(ctx);\n    LOGI(\"%s%zu%s\", \"Found \", entryCount, \" images in archive\");\n    if (!entryCount) {\n        LOGE(\"%s%s\", \"Archive read failed: \", archive_error_string(ctx->arc));\n        archive_release_ctx(ctx);\n        return 0;\n    }\n\n    // We must read through the file|vm then we can know whether it is encrypted\n    int encryptRet = archive_read_has_encrypted_entries(ctx->arc);\n    switch (encryptRet) {\n        case 1: // At lease 1 encrypted entry\n            need_encrypt = true;\n            break;\n        case 0: // format supports but no encrypted entry found\n        default:\n            need_encrypt = false;\n    }\n\n    int format = archive_format(ctx->arc);\n    switch (format) {\n        case ARCHIVE_FORMAT_ZIP:\n        case ARCHIVE_FORMAT_RAR_V5:\n            madvise_log_if_error(archiveAddr, archiveSize, MADV_SEQUENTIAL);\n            break;\n        case ARCHIVE_FORMAT_7ZIP: // Seek is bad\n            madvise_log_if_error(archiveAddr, archiveSize, MADV_RANDOM);\n            break;\n        default:;\n    }\n    archive_release_ctx(ctx);\n\n    ctx = archive_alloc_ctx();\n    if (!ctx) return 0;\n    entries = calloc(entryCount, sizeof(entry));\n    archive_map_entries_index(ctx, sort_entries);\n    archive_release_ctx(ctx);\n    return (int) entryCount;\n}\n\nJNIEXPORT jobject JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz, jint index) {\n    EH_UNUSED(env);\n    EH_UNUSED(thiz);\n    entry *entry = &entries[index];\n    ssize_t size = entry->size;\n    if (entry->addr) {\n        return (*env)->NewDirectByteBuffer(env, entry->addr, size);\n    } else {\n        archive_ctx *ctx = NULL;\n        if (!archive_get_ctx(&ctx, entry->index)) {\n            void *addr = acquire_decode_buffer();\n            ssize_t bytes = archive_read_data(ctx->arc, addr, size);\n            ctx->using = 0;\n            if (bytes == size) {\n                return (*env)->NewDirectByteBuffer(env, addr, size);\n            } else {\n                if (bytes < 0) {\n                    LOGE(\"%s%s\", \"Archive read failed: \", archive_error_string(ctx->arc));\n                } else {\n                    LOGE(\"%s\", \"No enough data read, WTF?\");\n                }\n            }\n            release_decode_buffer(addr);\n        }\n    }\n    return 0;\n}\n\nJNIEXPORT void JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {\n    EH_UNUSED(env);\n    EH_UNUSED(thiz);\n    if (ctx_pool) {\n        for (int i = 0; i < CTX_POOL_SIZE; i++)\n            archive_release_ctx(ctx_pool[i]);\n        free(ctx_pool);\n        ctx_pool = NULL;\n    }\n    free(passwd);\n    passwd = NULL;\n    need_encrypt = false;\n    if (archiveAddr != MAP_FAILED) {\n        munmap(archiveAddr, archiveSize);\n        archiveAddr = MAP_FAILED;\n    }\n    for (int i = 0; i < MAX_PARALLEL_DECOMP; ++i) {\n        free(decode_buffer[i]);\n        decode_buffer[i] = NULL;\n    }\n    max_file_size = 0;\n    if (entries) {\n        for (int i = 0; i < entryCount; ++i) {\n            free((void *) entries[i].filename);\n        }\n        free(entries);\n        entries = NULL;\n    }\n}\n\nJNIEXPORT jboolean JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {\n    EH_UNUSED(env);\n    EH_UNUSED(thiz);\n    return need_encrypt;\n}\n\nJNIEXPORT jboolean JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz, jstring str) {\n    EH_UNUSED(thiz);\n    struct archive_entry *entry;\n    archive_ctx *ctx;\n    jboolean ret = true;\n    int len = (*env)->GetStringUTFLength(env, str);\n    passwd = realloc(passwd, len + 1);\n    (*env)->GetStringUTFRegion(env, str, 0, len, passwd);\n    passwd[len] = 0;\n    ctx = archive_alloc_ctx();\n    char tmpBuf[4096];\n    while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {\n        if (!archive_entry_is_playable(entry))\n            continue;\n        if (!archive_entry_is_encrypted(entry))\n            continue;\n        if (archive_read_data(ctx->arc, tmpBuf, 4096) < ARCHIVE_OK) {\n            LOGE(\"%s%s\", \"Archive read failed: \", archive_error_string(ctx->arc));\n            ret = false;\n        }\n        break;\n    }\n    archive_release_ctx(ctx);\n    return ret;\n}\n\nJNIEXPORT jstring JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz, jint index) {\n    EH_UNUSED(env);\n    EH_UNUSED(thiz);\n    index = entries[index].index;\n    archive_ctx *ctx = NULL;\n    int ret;\n    ret = archive_get_ctx(&ctx, index);\n    if (ret)\n        return NULL;\n    jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));\n    ctx->using = 0;\n    return str;\n}\n\nJNIEXPORT jboolean JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz, jint index, jint fd) {\n    EH_UNUSED(env);\n    EH_UNUSED(thiz);\n    index = entries[index].index;\n    archive_ctx *ctx = NULL;\n    int ret;\n    ret = archive_get_ctx(&ctx, index);\n    if (!ret) {\n        ret = archive_read_data_into_fd(ctx->arc, fd);\n        ctx->using = 0;\n    }\n    return ret == ARCHIVE_OK;\n}\n\nJNIEXPORT void JNICALL\nJava_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz, jobject buffer) {\n    EH_UNUSED(thiz);\n    void *addr = (*env)->GetDirectBufferAddress(env, buffer);\n    if (!ADDR_IN_FILE_MAPPING(addr)) {\n        release_decode_buffer(addr);\n    }\n}\n"
  },
  {
    "path": "app/src/main/cpp/ehviewer.h",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for\n * more details.\n *\n * You should have received a copy of the GNU General Public License along with\n * EhViewer. If not, see <https://www.gnu.org/licenses/>.\n */\n\n#ifndef EHVIEWER_EHVIEWER_H\n#define EHVIEWER_EHVIEWER_H\n\n#define EH_UNUSED(x) (void)x\n#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG ,__VA_ARGS__)\n#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG ,__VA_ARGS__)\n#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG ,__VA_ARGS__)\n#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG ,__VA_ARGS__)\n#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG ,__VA_ARGS__)\n#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG ,__VA_ARGS__)\n\n#define madvise_log_if_error(addr, len, advice) \\\nif (madvise(addr, len, advice))                 \\\n    LOGE(\"%s%p%s%zu%s%d%s%s%s\", \"madvise addr:\", addr, \"len:\", len, \"with advice \", advice, \" failed with error: \", strerror(errno), \", Ignored\")\n\n#endif /* EHVIEWER_EHVIEWER_H */\n"
  },
  {
    "path": "app/src/main/cpp/gifutils.c",
    "content": "/*\n * Copyright 2023 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for\n * more details.\n *\n * You should have received a copy of the GNU General Public License along with\n * EhViewer. If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <jni.h>\n#include <string.h>\n#include <stdbool.h>\n#include <unistd.h>\n#include <android/log.h>\n#include <sys/stat.h>\n#include <sys/mman.h>\n\n#define LOG_TAG \"gifUtils\"\n\n#include \"ehviewer.h\"\n\n#define GIF_HEADER_87A \"GIF87a\"\n#define GIF_HEADER_89A \"GIF89a\"\n#define GIF_HEADER_LENGTH 6\n\nstatic int FRAME_DELAY_START_MARKER = 0x0021F904;\n\ntypedef signed char byte;\n\n#define FRAME_DELAY_START_MARKER ((byte*)(&FRAME_DELAY_START_MARKER))\n#define MINIMUM_FRAME_DELAY 2\n#define DEFAULT_FRAME_DELAY 10\n\nstatic inline bool isGif(void *addr) {\n    return !memcmp(addr, GIF_HEADER_87A, GIF_HEADER_LENGTH) ||\n           !memcmp(addr, GIF_HEADER_89A, GIF_HEADER_LENGTH);\n}\n\nstatic void doRewrite(byte *addr, size_t size) {\n    if (size < 7 || !isGif(addr)) return;\n    for (size_t i = 0; i < size - 8; i++) {\n        // TODO: Optimize this hex find with SIMD?\n        if (addr[i] == FRAME_DELAY_START_MARKER[3] && addr[i + 1] == FRAME_DELAY_START_MARKER[2] &&\n            addr[i + 2] == FRAME_DELAY_START_MARKER[1] &&\n            addr[i + 3] == FRAME_DELAY_START_MARKER[0]) {\n            byte *end = addr + i + 4;\n            if (end[4] != 0) continue;\n            int frameDelay = end[2] << 8 | end[1];\n            if (frameDelay >= MINIMUM_FRAME_DELAY)\n                break; // Quit if the first block looks normal, for performance\n            end[1] = DEFAULT_FRAME_DELAY;\n            end[2] = 0;\n        }\n    }\n}\n\nJNIEXPORT jboolean JNICALL\nJava_com_hippo_ehviewer_jni_GifUtilsKt_isGif(JNIEnv *env, jclass clazz, jint fd) {\n    byte buffer[GIF_HEADER_LENGTH];\n    return read(fd, buffer, GIF_HEADER_LENGTH) == GIF_HEADER_LENGTH &&\n           isGif(buffer);\n}\n\nJNIEXPORT void JNICALL\nJava_com_hippo_ehviewer_jni_GifUtilsKt_rewriteGifSource(JNIEnv *env, jclass clazz, jobject buffer) {\n    byte *addr = (*env)->GetDirectBufferAddress(env, buffer);\n    size_t size = (*env)->GetDirectBufferCapacity(env, buffer);\n    doRewrite(addr, size);\n}\n\nJNIEXPORT jobject JNICALL\nJava_com_hippo_ehviewer_jni_GifUtilsKt_mmap(JNIEnv *env, jclass clazz, jint fd) {\n    struct stat64 st;\n    fstat64(fd, &st);\n    size_t size = st.st_size;\n    byte *addr = mmap64(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);\n    if (addr == MAP_FAILED) return NULL;\n    return (*env)->NewDirectByteBuffer(env, addr, size);\n}\n\nJNIEXPORT void JNICALL\nJava_com_hippo_ehviewer_jni_GifUtilsKt_munmap(JNIEnv *env, jclass clazz, jobject buffer) {\n    byte *addr = (*env)->GetDirectBufferAddress(env, buffer);\n    size_t size = (*env)->GetDirectBufferCapacity(env, buffer);\n    munmap(addr, size);\n}\n"
  },
  {
    "path": "app/src/main/cpp/hash.c",
    "content": "/*\n * Copyright 2022-2024 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for\n * more details.\n *\n * You should have received a copy of the GNU General Public License along with\n * EhViewer. If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <unistd.h>\n\n#include <jni.h>\n#include <nettle/sha1.h>\n\n#include \"ehviewer.h\"\n\n#define BUFFER_SIZE 8192\n\ntypedef uint8_t byte;\n\nconst char hex_digits[] = \"0123456789abcdef\";\n\nJNIEXPORT jstring JNICALL\nJava_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {\n    EH_UNUSED(clazz);\n    struct sha1_ctx ctx;\n    sha1_init(&ctx);\n\n    size_t bytes_read;\n    byte buffer[BUFFER_SIZE];\n    while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {\n        sha1_update(&ctx, bytes_read, buffer);\n    }\n\n    byte digest[SHA1_DIGEST_SIZE];\n    sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);\n\n    byte byte;\n    char hex_digest[2 * SHA1_DIGEST_SIZE + 1];\n    for (int i = 0; i < SHA1_DIGEST_SIZE; i++) {\n        byte = digest[i];\n        hex_digest[2 * i] = hex_digits[byte >> 4 & 0xF];\n        hex_digest[2 * i + 1] = hex_digits[byte & 0xF];\n    }\n    hex_digest[2 * SHA1_DIGEST_SIZE] = '\\0';\n\n    return (*env)->NewStringUTF(env, hex_digest);\n}\n"
  },
  {
    "path": "app/src/main/cpp/image.c",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for\n * more details.\n *\n * You should have received a copy of the GNU General Public License along with\n * EhViewer. If not, see <https://www.gnu.org/licenses/>.\n */\n\n#include <stdlib.h>\n#include <string.h>\n\n#include <android/bitmap.h>\n#include <android/data_space.h>\n#include <android/log.h>\n\n#include <GLES3/gl3.h>\n#include <jni.h>\n\n#define TAG \"ImageDecoder_wrapper\"\n\n#include \"ehviewer.h\"\n\n#define IMAGE_TILE_MAX_SIZE (512 * 512)\n\nstatic char tile_buffer[IMAGE_TILE_MAX_SIZE * 8];\n\nbool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y,\n                 void *dst, int dst_w, int dst_h, int dst_x, int dst_y,\n                 int width, int height, int stride) {\n    int left;\n    int line;\n    size_t line_stride;\n    int src_stride;\n    int src_pos;\n    int dst_pos;\n    size_t dst_blank_length;\n\n    // Sanitize\n    if (src_x < 0) {\n        width -= src_x;\n        dst_x -= src_x;\n        src_x = 0;\n    }\n    if (dst_x < 0) {\n        width -= dst_x;\n        src_x -= dst_x;\n        dst_x = 0;\n    }\n    if (width <= 0) {\n        return false;\n    }\n    if (src_y < 0) {\n        height -= src_y;\n        dst_y -= src_y;\n        src_y = 0;\n    }\n    if (dst_y < 0) {\n        height -= dst_y;\n        src_y -= dst_y;\n        dst_y = 0;\n    }\n    if (height <= 0) {\n        return false;\n    }\n    left = src_x + width - src_w;\n    if (left > 0) {\n        width -= left;\n    }\n    left = dst_x + width - dst_w;\n    if (left > 0) {\n        width -= left;\n    }\n    if (width <= 0) {\n        return false;\n    }\n    left = src_y + height - src_h;\n    if (left > 0) {\n        height -= left;\n    }\n    left = dst_y + height - dst_h;\n    if (left > 0) {\n        height -= left;\n    }\n    if (height <= 0) {\n        return false;\n    }\n\n    // Init\n    line_stride = (size_t) (width * stride);\n    src_stride = src_w * stride;\n    src_pos = src_y * src_stride + src_x * stride;\n    dst_pos = 0;\n\n    dst_blank_length = (size_t) (dst_y * dst_w + dst_x) * stride;\n\n    // First line\n    dst_pos += (int) dst_blank_length;\n    memcpy(dst + dst_pos, src + src_pos, line_stride);\n    dst_pos += (int) line_stride;\n    src_pos += src_stride;\n\n    // Other lines\n    dst_blank_length = (size_t) ((dst_w - width) * stride);\n    for (line = 1; line < height; line++) {\n        dst_pos += (int) dst_blank_length;\n        memcpy(dst + dst_pos, src + src_pos, line_stride);\n        dst_pos += (int) line_stride;\n        src_pos += src_stride;\n    }\n\n    return true;\n}\n\nJNIEXPORT void JNICALL\nJava_com_hippo_ehviewer_jni_ImageKt_nativeTexImage(JNIEnv *env, jclass clazz, jobject bitmap, jboolean init,\n                                          jint offset_x, jint offset_y, jint width, jint height) {\n    if (width * height > IMAGE_TILE_MAX_SIZE)\n        return;\n    AndroidBitmapInfo info;\n    void *pixels = NULL;\n    AndroidBitmap_lockPixels(env, bitmap, &pixels);\n    AndroidBitmap_getInfo(env, bitmap, &info);\n    bool is_f16 = info.format == ANDROID_BITMAP_FORMAT_RGBA_F16;\n    copy_pixels(pixels, info.width, info.height, offset_x, offset_y, tile_buffer, width, height, 0, 0, width, height, is_f16 ? 8 : 4);\n    AndroidBitmap_unlockPixels(env, bitmap);\n    if (init) {\n        glTexImage2D(GL_TEXTURE_2D, 0, is_f16 ? GL_RGBA16F : GL_RGBA8, width, height, 0, GL_RGBA,\n                     is_f16 ? GL_HALF_FLOAT : GL_UNSIGNED_BYTE, tile_buffer);\n    } else {\n        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA,\n                        is_f16 ? GL_HALF_FLOAT : GL_UNSIGNED_BYTE, tile_buffer);\n    }\n}\n"
  },
  {
    "path": "app/src/main/cpp/natsort/strnatcmp.c",
    "content": "/* -*- mode: c; c-file-style: \"k&r\" -*-\n\n  strnatcmp.c -- Perform 'natural order' comparisons of strings in C.\n  Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>\n\n  This software is provided 'as-is', without any express or implied\n  warranty.  In no event will the authors be held liable for any damages\n  arising from the use of this software.\n\n  Permission is granted to anyone to use this software for any purpose,\n  including commercial applications, and to alter it and redistribute it\n  freely, subject to the following restrictions:\n\n  1. The origin of this software must not be misrepresented; you must not\n     claim that you wrote the original software. If you use this software\n     in a product, an acknowledgment in the product documentation would be\n     appreciated but is not required.\n  2. Altered source versions must be plainly marked as such, and must not be\n     misrepresented as being the original software.\n  3. This notice may not be removed or altered from any source distribution.\n*/\n\n\n/* partial change history:\n *\n * 2004-10-10 mbp: Lift out character type dependencies into macros.\n *\n * Eric Sosman pointed out that ctype functions take a parameter whose\n * value must be that of an unsigned int, even on platforms that have\n * negative chars in their default char type.\n */\n\n#include <ctype.h>\n\n#include \"strnatcmp.h\"\n\n\n/* These are defined as macros to make it easier to adapt this code to\n * different characters types or comparison functions. */\nstatic inline int\nnat_isdigit(const char *a)\n{\n     return isdigit((unsigned char) *a);\n}\n\n\nstatic inline int\nnat_isspace(const char *a)\n{\n     return isspace((unsigned char) *a) || *a == '0' && nat_isdigit(a + 1);\n}\n\n\nstatic int\ncompare_right(const char *a, const char *b)\n{\n     int bias = 0;\n\n     /* The longest run of digits wins.  That aside, the greatest\n        value wins, but we can't know that it will until we've scanned\n        both numbers to know that they have the same magnitude, so we\n        remember it in BIAS. */\n     for (;; a++, b++) {\n          if (!nat_isdigit(a)  &&  !nat_isdigit(b))\n               return bias;\n          if (!nat_isdigit(a))\n               return -1;\n          if (!nat_isdigit(b))\n               return +1;\n          if (*a < *b) {\n               if (!bias)\n                    bias = -1;\n          } else if (*a > *b) {\n               if (!bias)\n                    bias = +1;\n          } else if (!*a  &&  !*b)\n               return bias;\n     }\n}\n\n\nint\nstrnatcmp(const char *a, const char *b)\n{\n    int result;\n\n    while (1) {\n         /* skip over leading spaces or zeros */\n          while (nat_isspace(a))\n               a++;\n\n          while (nat_isspace(b))\n               b++;\n\n          /* process run of digits */\n          if (nat_isdigit(a)  &&  nat_isdigit(b)) {\n               if ((result = compare_right(a, b)) != 0)\n                    return result;\n          }\n\n          if (!*a && !*b) {\n               /* The strings compare the same.  Perhaps the caller\n                  will want to call strcmp to break the tie. */\n               return 0;\n          }\n\n          if (*a < *b)\n               return -1;\n\n          if (*a > *b)\n               return +1;\n\n          a++; b++;\n     }\n}\n\n"
  },
  {
    "path": "app/src/main/cpp/natsort/strnatcmp.h",
    "content": "/* -*- mode: c; c-file-style: \"k&r\" -*-\n\n  strnatcmp.c -- Perform 'natural order' comparisons of strings in C.\n  Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>\n\n  This software is provided 'as-is', without any express or implied\n  warranty.  In no event will the authors be held liable for any damages\n  arising from the use of this software.\n\n  Permission is granted to anyone to use this software for any purpose,\n  including commercial applications, and to alter it and redistribute it\n  freely, subject to the following restrictions:\n\n  1. The origin of this software must not be misrepresented; you must not\n     claim that you wrote the original software. If you use this software\n     in a product, an acknowledgment in the product documentation would be\n     appreciated but is not required.\n  2. Altered source versions must be plainly marked as such, and must not be\n     misrepresented as being the original software.\n  3. This notice may not be removed or altered from any source distribution.\n*/\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\nint strnatcmp(const char *a, const char *b);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "app/src/main/cpp/nettle/.gitignore",
    "content": "/nettle"
  },
  {
    "path": "app/src/main/cpp/nettle/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.4.1)\nproject(nettle C)\n\nset(LIBNETTLE_DEFINITIONS\n        -DHAVE_CONFIG_H)\n\nset(LIBNETTLE_SOURCES\n        nettle/aes-decrypt-internal.c\n        nettle/aes-decrypt.c\n        nettle/aes-decrypt-table.c\n        nettle/aes128-decrypt.c\n        nettle/aes192-decrypt.c\n        nettle/aes256-decrypt.c\n        nettle/aes-encrypt-internal.c\n        nettle/aes-encrypt.c\n        nettle/aes-encrypt-table.c\n        nettle/aes128-encrypt.c\n        nettle/aes192-encrypt.c\n        nettle/aes256-encrypt.c\n        nettle/aes-invert-internal.c\n        nettle/aes-set-key-internal.c\n        nettle/aes-set-encrypt-key.c\n        nettle/aes-set-decrypt-key.c\n        nettle/aes128-set-encrypt-key.c\n        nettle/aes128-set-decrypt-key.c\n        nettle/aes128-meta.c\n        nettle/aes192-set-encrypt-key.c\n        nettle/aes192-set-decrypt-key.c\n        nettle/aes192-meta.c\n        nettle/aes256-set-encrypt-key.c\n        nettle/aes256-set-decrypt-key.c\n        nettle/aes256-meta.c\n        nettle/nist-keywrap.c\n        nettle/arcfour.c\n        nettle/arctwo.c\n        nettle/arctwo-meta.c\n        nettle/blowfish.c\n        nettle/blowfish-bcrypt.c\n        nettle/base16-encode.c\n        nettle/base16-decode.c\n        nettle/base16-meta.c\n        nettle/base64-encode.c\n        nettle/base64-decode.c\n        nettle/base64-meta.c\n        nettle/base64url-encode.c\n        nettle/base64url-decode.c\n        nettle/base64url-meta.c\n        nettle/buffer.c\n        nettle/buffer-init.c\n        nettle/camellia-crypt-internal.c\n        nettle/camellia-table.c\n        nettle/camellia-absorb.c\n        nettle/camellia-invert-key.c\n        nettle/camellia128-set-encrypt-key.c\n        nettle/camellia128-crypt.c\n        nettle/camellia128-set-decrypt-key.c\n        nettle/camellia128-meta.c\n        nettle/camellia192-meta.c\n        nettle/camellia256-set-encrypt-key.c\n        nettle/camellia256-crypt.c\n        nettle/camellia256-set-decrypt-key.c\n        nettle/camellia256-meta.c\n        nettle/cast128.c\n        nettle/cast128-meta.c\n        nettle/cbc.c\n        nettle/cbc-aes128-encrypt.c\n        nettle/cbc-aes192-encrypt.c\n        nettle/cbc-aes256-encrypt.c\n        nettle/ccm.c\n        nettle/ccm-aes128.c\n        nettle/ccm-aes192.c\n        nettle/ccm-aes256.c\n        nettle/cfb.c\n        nettle/siv-cmac.c\n        nettle/siv-cmac-aes128.c\n        nettle/siv-cmac-aes256.c\n        nettle/cnd-memcpy.c\n        nettle/chacha-crypt.c\n        nettle/chacha-core-internal.c\n        nettle/chacha-poly1305.c\n        nettle/chacha-poly1305-meta.c\n        nettle/chacha-set-key.c\n        nettle/chacha-set-nonce.c\n        nettle/ctr.c\n        nettle/ctr16.c\n        nettle/des.c\n        nettle/des3.c\n        nettle/eax.c\n        nettle/eax-aes128.c\n        nettle/eax-aes128-meta.c\n        nettle/ghash-set-key.c\n        nettle/ghash-update.c\n        nettle/gcm.c\n        nettle/gcm-aes.c\n        nettle/gcm-aes128.c\n        nettle/gcm-aes128-meta.c\n        nettle/gcm-aes192.c\n        nettle/gcm-aes192-meta.c\n        nettle/gcm-aes256.c\n        nettle/gcm-aes256-meta.c\n        nettle/gcm-camellia128.c\n        nettle/gcm-camellia128-meta.c\n        nettle/gcm-camellia256.c\n        nettle/gcm-camellia256-meta.c\n        nettle/cmac.c\n        nettle/cmac64.c\n        nettle/cmac-aes128.c\n        nettle/cmac-aes256.c\n        nettle/cmac-des3.c\n        nettle/cmac-aes128-meta.c\n        nettle/cmac-aes256-meta.c\n        nettle/cmac-des3-meta.c\n        nettle/gost28147.c\n        nettle/gosthash94.c\n        nettle/gosthash94-meta.c\n        nettle/hmac.c\n        nettle/hmac-gosthash94.c\n        nettle/hmac-md5.c\n        nettle/hmac-ripemd160.c\n        nettle/hmac-sha1.c\n        nettle/hmac-sha224.c\n        nettle/hmac-sha256.c\n        nettle/hmac-sha384.c\n        nettle/hmac-sha512.c\n        nettle/hmac-streebog.c\n        nettle/hmac-sm3.c\n        nettle/hmac-gosthash94-meta.c\n        nettle/hmac-md5-meta.c\n        nettle/hmac-ripemd160-meta.c\n        nettle/hmac-sha1-meta.c\n        nettle/hmac-sha224-meta.c\n        nettle/hmac-sha256-meta.c\n        nettle/hmac-sha384-meta.c\n        nettle/hmac-sha512-meta.c\n        nettle/hmac-streebog-meta.c\n        nettle/hmac-sm3-meta.c\n        nettle/knuth-lfib.c\n        nettle/hkdf.c\n        nettle/md2.c\n        nettle/md2-meta.c\n        nettle/md4.c\n        nettle/md4-meta.c\n        nettle/md5.c\n        nettle/md5-compat.c\n        nettle/md5-meta.c\n        nettle/memeql-sec.c\n        nettle/memxor.c\n        nettle/memxor3.c\n        nettle/nettle-lookup-hash.c\n        nettle/nettle-meta-aeads.c\n        nettle/nettle-meta-armors.c\n        nettle/nettle-meta-ciphers.c\n        nettle/nettle-meta-hashes.c\n        nettle/nettle-meta-macs.c\n        nettle/pbkdf2.c\n        nettle/pbkdf2-hmac-gosthash94.c\n        nettle/pbkdf2-hmac-sha1.c\n        nettle/pbkdf2-hmac-sha256.c\n        nettle/pbkdf2-hmac-sha384.c\n        nettle/pbkdf2-hmac-sha512.c\n        nettle/poly1305-aes.c\n        nettle/poly1305-internal.c\n        nettle/realloc.c\n        nettle/ripemd160.c\n        nettle/ripemd160-compress.c\n        nettle/ripemd160-meta.c\n        nettle/salsa20-core-internal.c\n        nettle/salsa20-crypt-internal.c\n        nettle/salsa20-crypt.c\n        nettle/salsa20r12-crypt.c\n        nettle/salsa20-set-key.c\n        nettle/salsa20-set-nonce.c\n        nettle/salsa20-128-set-key.c\n        nettle/salsa20-256-set-key.c\n        nettle/sha1.c\n        nettle/sha1-compress.c\n        nettle/sha1-meta.c\n        nettle/sha256.c\n        nettle/sha256-compress-n.c\n        nettle/sha224-meta.c\n        nettle/sha256-meta.c\n        nettle/sha512.c\n        nettle/sha512-compress.c\n        nettle/sha384-meta.c\n        nettle/sha512-meta.c\n        nettle/sha512-224-meta.c\n        nettle/sha512-256-meta.c\n        nettle/sha3.c\n        nettle/sha3-permute.c\n        nettle/sha3-shake.c\n        nettle/sha3-224.c\n        nettle/sha3-224-meta.c\n        nettle/sha3-256.c\n        nettle/sha3-256-meta.c\n        nettle/sha3-384.c\n        nettle/sha3-384-meta.c\n        nettle/sha3-512.c\n        nettle/sha3-512-meta.c\n        nettle/shake128.c\n        nettle/shake256.c\n        nettle/sm3.c\n        nettle/sm3-meta.c\n        nettle/serpent-set-key.c\n        nettle/serpent-encrypt.c\n        nettle/serpent-decrypt.c\n        nettle/serpent-meta.c\n        nettle/streebog.c\n        nettle/streebog-meta.c\n        nettle/twofish.c\n        nettle/twofish-meta.c\n        nettle/umac-nh.c\n        nettle/umac-nh-n.c\n        nettle/umac-l2.c\n        nettle/umac-l3.c\n        nettle/umac-poly64.c\n        nettle/umac-poly128.c\n        nettle/umac-set-key.c\n        nettle/umac32.c\n        nettle/umac64.c\n        nettle/umac96.c\n        nettle/umac128.c\n        nettle/version.c\n        nettle/write-be32.c\n        nettle/write-le32.c\n        nettle/write-le64.c\n        nettle/yarrow256.c\n        nettle/yarrow_key_event.c\n        nettle/xts.c\n        nettle/xts-aes128.c\n        nettle/xts-aes256.c\n        )\n\nset(LIBNETTLE_INCLUDES\n        ${CMAKE_CURRENT_SOURCE_DIR}\n        ${CMAKE_CURRENT_SOURCE_DIR}/nettle\n        )\n\nadd_library(nettle STATIC ${LIBNETTLE_SOURCES})\ntarget_include_directories(nettle PUBLIC ${LIBNETTLE_INCLUDES})\ntarget_compile_definitions(nettle PRIVATE ${LIBNETTLE_DEFINITIONS})\n"
  },
  {
    "path": "app/src/main/cpp/nettle/config.h",
    "content": "/* config.h.  Generated from config.h.in by configure.  */\n/* config.h.in.  Generated from configure.ac by autoheader.  */\n\n/* Define if building universal (internal helper macro) */\n/* #undef AC_APPLE_UNIVERSAL_BUILD */\n\n/* Define to 1 if using 'alloca.c'. */\n/* #undef C_ALLOCA */\n\n/* Define to 1 if you have 'alloca', as a function or macro. */\n#define HAVE_ALLOCA 1\n\n/* Define to 1 if <alloca.h> works. */\n#define HAVE_ALLOCA_H 1\n\n/* Define if __builtin_bswap64 is available */\n#define HAVE_BUILTIN_BSWAP64 1\n\n/* Define if clock_gettime is available */\n#define HAVE_CLOCK_GETTIME 1\n\n/* Define to 1 if you have the <dlfcn.h> header file. */\n#define HAVE_DLFCN_H 1\n\n/* Define to 1 if you have the `elf_aux_info' function. */\n/* #undef HAVE_ELF_AUX_INFO */\n\n/* Define if fcntl file locking is available */\n#define HAVE_FCNTL_LOCKING 1\n\n/* Define if the compiler understands __attribute__ */\n#define HAVE_GCC_ATTRIBUTE 1\n\n/* Define to 1 if you have the `getline' function. */\n#define HAVE_GETLINE 1\n\n/* Define to 1 if you have the <inttypes.h> header file. */\n#define HAVE_INTTYPES_H 1\n\n/* Define to 1 if you have dlopen (with -ldl). */\n#define HAVE_LIBDL 1\n\n/* Define to 1 if you have the `gmp' library (-lgmp). */\n/* #undef HAVE_LIBGMP */\n\n/* Define if compiler and linker supports __attribute__ ifunc */\n#define HAVE_LINK_IFUNC 1\n\n/* Define to 1 if you have the <malloc.h> header file. */\n#define HAVE_MALLOC_H 1\n\n/* Define to 1 each of the following for which a native (ie. CPU specific)\n    implementation of the corresponding routine exists.  */\n/* #undef HAVE_NATIVE_memxor3 */\n/* #undef HAVE_NATIVE_aes_decrypt */\n/* #undef HAVE_NATIVE_aes_encrypt */\n/* #undef HAVE_NATIVE_aes_invert */\n/* #undef HAVE_NATIVE_aes128_decrypt */\n/* #undef HAVE_NATIVE_aes128_encrypt */\n/* #undef HAVE_NATIVE_aes128_invert_key */\n/* #undef HAVE_NATIVE_aes128_set_decrypt_key */\n/* #undef HAVE_NATIVE_aes128_set_encrypt_key */\n/* #undef HAVE_NATIVE_aes192_decrypt */\n/* #undef HAVE_NATIVE_aes192_encrypt */\n/* #undef HAVE_NATIVE_aes192_invert_key */\n/* #undef HAVE_NATIVE_aes192_set_decrypt_key */\n/* #undef HAVE_NATIVE_aes192_set_encrypt_key */\n/* #undef HAVE_NATIVE_aes256_decrypt */\n/* #undef HAVE_NATIVE_aes256_encrypt */\n/* #undef HAVE_NATIVE_aes256_invert_key */\n/* #undef HAVE_NATIVE_aes256_set_decrypt_key */\n/* #undef HAVE_NATIVE_aes256_set_encrypt_key */\n/* #undef HAVE_NATIVE_cbc_aes128_encrypt */\n/* #undef HAVE_NATIVE_cbc_aes192_encrypt */\n/* #undef HAVE_NATIVE_cbc_aes256_encrypt */\n/* #undef HAVE_NATIVE_chacha_core */\n/* #undef HAVE_NATIVE_chacha_2core */\n#define HAVE_NATIVE_chacha_3core 1\n/* #undef HAVE_NATIVE_chacha_4core */\n/* #undef HAVE_NATIVE_fat_chacha_2core */\n/* #undef HAVE_NATIVE_fat_chacha_3core */\n/* #undef HAVE_NATIVE_fat_chacha_4core */\n/* #undef HAVE_NATIVE_ecc_curve25519_modp */\n/* #undef HAVE_NATIVE_ecc_curve448_modp */\n/* #undef HAVE_NATIVE_ecc_secp192r1_modp */\n/* #undef HAVE_NATIVE_ecc_secp192r1_redc */\n/* #undef HAVE_NATIVE_ecc_secp224r1_modp */\n/* #undef HAVE_NATIVE_ecc_secp224r1_redc */\n/* #undef HAVE_NATIVE_ecc_secp256r1_modp */\n/* #undef HAVE_NATIVE_ecc_secp256r1_redc */\n/* #undef HAVE_NATIVE_ecc_secp384r1_modp */\n/* #undef HAVE_NATIVE_ecc_secp384r1_redc */\n/* #undef HAVE_NATIVE_ecc_secp521r1_modp */\n/* #undef HAVE_NATIVE_ecc_secp521r1_redc */\n/* #undef HAVE_NATIVE_poly1305_set_key */\n/* #undef HAVE_NATIVE_poly1305_block */\n/* #undef HAVE_NATIVE_poly1305_digest */\n/* #undef HAVE_NATIVE_poly1305_blocks */\n/* #undef HAVE_NATIVE_fat_poly1305_blocks */\n/* #undef HAVE_NATIVE_ghash_set_key */\n/* #undef HAVE_NATIVE_ghash_update */\n/* #undef HAVE_NATIVE_gcm_aes_encrypt */\n/* #undef HAVE_NATIVE_gcm_aes_decrypt */\n/* #undef HAVE_NATIVE_salsa20_core */\n#define HAVE_NATIVE_salsa20_2core 1\n/* #undef HAVE_NATIVE_fat_salsa20_2core */\n/* #undef HAVE_NATIVE_sha1_compress */\n/* #undef HAVE_NATIVE_sha256_compress_n */\n/* #undef HAVE_NATIVE_sha512_compress */\n/* #undef HAVE_NATIVE_sha3_permute */\n/* #undef HAVE_NATIVE_umac_nh */\n/* #undef HAVE_NATIVE_umac_nh_n */\n\n/* Define to 1 if you have the <openssl/ec.h> header file. */\n/* #undef HAVE_OPENSSL_EC_H */\n\n/* Define to 1 if you have the <openssl/evp.h> header file. */\n/* #undef HAVE_OPENSSL_EVP_H */\n\n/* Define to 1 if you have the <openssl/rsa.h> header file. */\n/* #undef HAVE_OPENSSL_RSA_H */\n\n/* Define to 1 if you have the `secure_getenv' function. */\n#define HAVE_SECURE_GETENV 1\n\n/* Define to 1 if you have the <stdint.h> header file. */\n#define HAVE_STDINT_H 1\n\n/* Define to 1 if you have the <stdio.h> header file. */\n#define HAVE_STDIO_H 1\n\n/* Define to 1 if you have the <stdlib.h> header file. */\n#define HAVE_STDLIB_H 1\n\n/* Define to 1 if you have the <strings.h> header file. */\n#define HAVE_STRINGS_H 1\n\n/* Define to 1 if you have the <string.h> header file. */\n#define HAVE_STRING_H 1\n\n/* Define to 1 if you have the <sys/stat.h> header file. */\n#define HAVE_SYS_STAT_H 1\n\n/* Define to 1 if you have the <sys/types.h> header file. */\n#define HAVE_SYS_TYPES_H 1\n\n/* Define to 1 if you have the <unistd.h> header file. */\n#define HAVE_UNISTD_H 1\n\n/* Define to 1 if you have the <valgrind/memcheck.h> header file. */\n/* #undef HAVE_VALGRIND_MEMCHECK_H */\n\n/* Define to the address where bug reports for this package should be sent. */\n#define PACKAGE_BUGREPORT \"nettle-bugs@lists.lysator.liu.se\"\n\n/* Define to the full name of this package. */\n#define PACKAGE_NAME \"nettle\"\n\n/* Define to the full name and version of this package. */\n#define PACKAGE_STRING \"nettle 3.10.2\"\n\n/* Define to the one symbol short name of this package. */\n#define PACKAGE_TARNAME \"nettle\"\n\n/* Define to the home page for this package. */\n#define PACKAGE_URL \"\"\n\n/* Define to the version of this package. */\n#define PACKAGE_VERSION \"3.10.2\"\n\n/* The size of `long', as computed by sizeof. */\n#define SIZEOF_LONG __SIZEOF_LONG__\n\n/* The size of `size_t', as computed by sizeof. */\n#define SIZEOF_SIZE_T __SIZEOF_SIZE_T__\n\n/* If using the C implementation of alloca, define if you know the\n   direction of stack growth for your system; otherwise it will be\n   automatically deduced at runtime.\n\tSTACK_DIRECTION > 0 => grows toward higher addresses\n\tSTACK_DIRECTION < 0 => grows toward lower addresses\n\tSTACK_DIRECTION = 0 => direction of growth unknown */\n/* #undef STACK_DIRECTION */\n\n/* Define to 1 if all of the C90 standard headers exist (not just the ones\n   required in a freestanding environment). This macro is provided for\n   backward compatibility; new code need not use it. */\n#define STDC_HEADERS 1\n\n/* Defined to enable additional asserts */\n/* #undef WITH_EXTRA_ASSERTS */\n\n/* Defined if public key features are enabled */\n/* #undef WITH_HOGWEED */\n\n/* Define if you have openssl libcrypto (used for benchmarking) */\n/* #undef WITH_OPENSSL */\n\n/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most\n   significant byte first (like Motorola and SPARC, unlike Intel). */\n#if defined AC_APPLE_UNIVERSAL_BUILD\n# if defined __BIG_ENDIAN__\n#  define WORDS_BIGENDIAN 1\n# endif\n#else\n# ifndef WORDS_BIGENDIAN\n/* #  undef WORDS_BIGENDIAN */\n# endif\n#endif\n\n/* Define to empty if `const' does not conform to ANSI C. */\n/* #undef const */\n\n/* Define to `int' if <sys/types.h> doesn't define. */\n/* #undef gid_t */\n\n/* Define to `__inline__' or `__inline' if that's what the C compiler\n   calls it, or to nothing if 'inline' is not supported under any name.  */\n#ifndef __cplusplus\n/* #undef inline */\n#endif\n\n/* Define to `unsigned int' if <sys/types.h> does not define. */\n/* #undef size_t */\n\n/* Define to `int' if <sys/types.h> doesn't define. */\n/* #undef uid_t */\n\n/* AIX requires this to be the first thing in the file.  */\n#ifndef __GNUC__\n# if HAVE_ALLOCA_H\n#  include <alloca.h>\n# else\n#  ifdef _AIX\n #pragma alloca\n#  else\n#   ifndef alloca /* predefined by HP cc +Olibcalls */\nchar *alloca ();\n#   endif\n#  endif\n/* Needed for alloca on windows */\n#  if HAVE_MALLOC_H\n#   include <malloc.h>\n#  endif\n# endif\n#else /* defined __GNUC__ */\n# if HAVE_ALLOCA_H\n#  include <alloca.h>\n# else\n/* Needed for alloca on windows, also with gcc */\n#  if HAVE_MALLOC_H\n#   include <malloc.h>\n#  endif\n# endif\n#endif\n\n\n#if __GNUC__ && HAVE_GCC_ATTRIBUTE\n# define NORETURN __attribute__ ((__noreturn__))\n# define PRINTF_STYLE(f, a) __attribute__ ((__format__ (__printf__, f, a)))\n# define UNUSED __attribute__ ((__unused__))\n#else\n# define NORETURN\n# define PRINTF_STYLE(f, a)\n# define UNUSED\n#endif\n\n\n#if defined(__x86_64__) || defined(__arch64__)\n# define HAVE_NATIVE_64_BIT 1\n#else\n/* Needs include of <limits.h> before use. */\n# define HAVE_NATIVE_64_BIT (SIZEOF_LONG * CHAR_BIT >= 64)\n#endif\n\n"
  },
  {
    "path": "app/src/main/cpp/nettle/keymap.h",
    "content": " \n"
  },
  {
    "path": "app/src/main/cpp/nettle/rotors.h",
    "content": " \n"
  },
  {
    "path": "app/src/main/cpp/nettle/version.h",
    "content": "/* version.h\n\n   Information about library version.\n\n   Copyright (C) 2015 Red Hat, Inc.\n   Copyright (C) 2015 Niels Möller\n\n   This file is part of GNU Nettle.\n\n   GNU Nettle is free software: you can redistribute it and/or\n   modify it under the terms of either:\n\n     * the GNU Lesser General Public License as published by the Free\n       Software Foundation; either version 3 of the License, or (at your\n       option) any later version.\n\n   or\n\n     * the GNU General Public License as published by the Free\n       Software Foundation; either version 2 of the License, or (at your\n       option) any later version.\n\n   or both in parallel, as here.\n\n   GNU Nettle 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 GNU\n   General Public License for more details.\n\n   You should have received copies of the GNU General Public License and\n   the GNU Lesser General Public License along with this program.  If\n   not, see http://www.gnu.org/licenses/.\n*/\n\n#ifndef NETTLE_VERSION_H_INCLUDED\n#define NETTLE_VERSION_H_INCLUDED\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n/* Individual version numbers in decimal */\n#define NETTLE_VERSION_MAJOR 3\n#define NETTLE_VERSION_MINOR 10\n\n#define NETTLE_USE_MINI_GMP 0\n\n/* We need a preprocessor constant for GMP_NUMB_BITS, simply using\n   sizeof(mp_limb_t) * CHAR_BIT is not good enough. */\n#if NETTLE_USE_MINI_GMP\n# define GMP_NUMB_BITS n/a\n#endif\n\nint\nnettle_version_major (void);\n\nint\nnettle_version_minor (void);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif /* NETTLE_VERSION_H_INCLUDED */\n"
  },
  {
    "path": "app/src/main/java/com/hippo/app/CheckBoxDialogBuilder.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.app\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.widget.CheckBox\nimport androidx.appcompat.app.AlertDialog\nimport com.hippo.ehviewer.R\n\n@SuppressLint(\"InflateParams\")\nclass CheckBoxDialogBuilder(\n    context: Context,\n    message: String?,\n    checkText: String?,\n    checked: Boolean,\n) : AlertDialog.Builder(\n    context,\n) {\n    private val mCheckBox: CheckBox\n    val isChecked: Boolean\n        get() = mCheckBox.isChecked\n\n    init {\n        val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_checkbox_builder, null)\n        setView(view)\n        setMessage(message)\n        mCheckBox = view.findViewById(R.id.checkbox)\n        mCheckBox.text = checkText\n        mCheckBox.isChecked = checked\n        view.setOnClickListener { mCheckBox.toggle() }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/app/EditTextCheckBoxDialogBuilder.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.app\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.widget.CheckBox\nimport android.widget.EditText\nimport android.widget.TextView\nimport android.widget.TextView.OnEditorActionListener\nimport androidx.appcompat.app.AlertDialog\nimport com.google.android.material.textfield.TextInputLayout\nimport com.hippo.ehviewer.R\n\n@SuppressLint(\"InflateParams\")\nclass EditTextCheckBoxDialogBuilder(\n    context: Context,\n    text: String?,\n    hint: String?,\n    checkText: String?,\n    checked: Boolean,\n) : AlertDialog.Builder(\n    context,\n),\n    OnEditorActionListener {\n    private val mCheckBox: CheckBox\n    val isChecked: Boolean\n        get() = mCheckBox.isChecked\n    private val mTextInputLayout: TextInputLayout\n    private val editText: EditText\n    private var mDialog: AlertDialog? = null\n    val text: String\n        get() = editText.text.toString()\n\n    fun setError(error: CharSequence?) {\n        mTextInputLayout.error = error\n    }\n\n    override fun create(): AlertDialog {\n        mDialog = super.create()\n        return mDialog as AlertDialog\n    }\n\n    override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean = if (mDialog != null) {\n        val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)\n        button?.performClick()\n        true\n    } else {\n        false\n    }\n\n    init {\n        val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittextcheckbox_builder, null)\n        setView(view)\n        mCheckBox = view.findViewById(R.id.checkbox)\n        mCheckBox.text = checkText\n        mCheckBox.isChecked = checked\n        view.setOnClickListener { mCheckBox.toggle() }\n        editText = view.findViewById(R.id.edit_text)\n        editText.setText(text)\n        editText.setSelection(editText.text.length)\n        editText.setOnEditorActionListener(this)\n        mTextInputLayout = view.findViewById(R.id.text_input_layout)\n        mTextInputLayout.hint = hint\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/app/EditTextDialogBuilder.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.app\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.widget.EditText\nimport android.widget.TextView\nimport android.widget.TextView.OnEditorActionListener\nimport androidx.appcompat.app.AlertDialog\nimport com.google.android.material.textfield.TextInputLayout\nimport com.hippo.ehviewer.R\n\n@SuppressLint(\"InflateParams\")\nclass EditTextDialogBuilder(\n    context: Context,\n    text: String?,\n    hint: String?,\n) : AlertDialog.Builder(\n    context,\n),\n    OnEditorActionListener {\n    private val mTextInputLayout: TextInputLayout\n    val editText: EditText\n    private var mDialog: AlertDialog? = null\n    val text: String\n        get() = editText.text.toString()\n\n    fun setError(error: CharSequence?) {\n        mTextInputLayout.error = error\n    }\n\n    override fun create(): AlertDialog {\n        mDialog = super.create()\n        return mDialog as AlertDialog\n    }\n\n    override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean = if (mDialog != null) {\n        val button = mDialog!!.getButton(DialogInterface.BUTTON_POSITIVE)\n        button?.performClick()\n        true\n    } else {\n        false\n    }\n\n    init {\n        val view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_edittext_builder, null)\n        setView(view)\n        mTextInputLayout = view as TextInputLayout\n        editText = view.findViewById(R.id.edit_text)\n        editText.setText(text)\n        editText.setSelection(editText.text.length)\n        editText.setOnEditorActionListener(this)\n        mTextInputLayout.hint = hint\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/app/ListCheckBoxDialogBuilder.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.app\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.AdapterView\nimport android.widget.ArrayAdapter\nimport android.widget.CheckBox\nimport android.widget.ListView\nimport androidx.appcompat.app.AlertDialog\nimport com.hippo.ehviewer.R\nimport com.hippo.yorozuya.ViewUtils\n\n@SuppressLint(\"InflateParams\")\nclass ListCheckBoxDialogBuilder(\n    context: Context,\n    items: List<CharSequence>,\n    listener: (ListCheckBoxDialogBuilder?, AlertDialog?, Int) -> Unit,\n    checkText: String?,\n    checked: Boolean,\n) : AlertDialog.Builder(\n    context,\n) {\n    private val mCheckBox: CheckBox\n    private var mDialog: AlertDialog? = null\n    val isChecked: Boolean\n        get() = mCheckBox.isChecked\n\n    override fun create(): AlertDialog {\n        mDialog = super.create()\n        return mDialog as AlertDialog\n    }\n\n    init {\n        val view =\n            LayoutInflater.from(getContext()).inflate(R.layout.dialog_list_checkbox_builder, null)\n        setView(view)\n        val listView = ViewUtils.`$$`(view, R.id.list_view) as ListView\n        mCheckBox = ViewUtils.`$$`(view, R.id.checkbox) as CheckBox\n        listView.adapter = ArrayAdapter(getContext(), R.layout.item_select_dialog, items)\n        listView.onItemClickListener =\n            AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->\n                listener(this@ListCheckBoxDialogBuilder, mDialog, position)\n                mDialog?.dismiss()\n            }\n        mCheckBox.text = checkText\n        mCheckBox.isChecked = checked\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/database/MSQLiteBuilder.kt",
    "content": "/*\n * Copyright 2017 Hippo Seven\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.hippo.database\n\nimport android.content.Context\nimport android.database.sqlite.SQLiteOpenHelper\nimport android.util.SparseArray\n\nclass MSQLiteBuilder {\n    companion object {\n        const val COLUMN_ID = \"_id\"\n        private val JAVA_TYPE_TO_SQLITE_TYPE: MutableMap<Class<*>?, String> = HashMap()\n        private fun javaTypeToSQLiteType(clazz: Class<*>?): String = JAVA_TYPE_TO_SQLITE_TYPE[clazz]\n            ?: throw IllegalStateException(\"Unknown type: $clazz\")\n\n        init {\n            JAVA_TYPE_TO_SQLITE_TYPE[Boolean::class.javaPrimitiveType] =\n                \"INTEGER NOT NULL DEFAULT 0\"\n            JAVA_TYPE_TO_SQLITE_TYPE[Byte::class.javaPrimitiveType] =\n                \"INTEGER NOT NULL DEFAULT 0\"\n            JAVA_TYPE_TO_SQLITE_TYPE[Short::class.javaPrimitiveType] =\n                \"INTEGER NOT NULL DEFAULT 0\"\n            JAVA_TYPE_TO_SQLITE_TYPE[Int::class.javaPrimitiveType] = \"INTEGER NOT NULL DEFAULT 0\"\n            JAVA_TYPE_TO_SQLITE_TYPE[Long::class.javaPrimitiveType] =\n                \"INTEGER NOT NULL DEFAULT 0\"\n            JAVA_TYPE_TO_SQLITE_TYPE[Float::class.javaPrimitiveType] =\n                \"REAL NOT NULL DEFAULT 0\"\n            JAVA_TYPE_TO_SQLITE_TYPE[Double::class.javaPrimitiveType] = \"REAL NOT NULL DEFAULT 0\"\n            JAVA_TYPE_TO_SQLITE_TYPE[String::class.java] = \"TEXT\"\n        }\n    }\n\n    private val statementsMap = SparseArray<List<String>?>()\n    private var version = 0\n    private var statements: MutableList<String>? = null\n\n    /**\n     * Bump database version.\n     */\n    fun version(version: Int): MSQLiteBuilder {\n        check(version > this.version) {\n            (\n                \"New version must be bigger than current version. \" +\n                    \"current version: \" + this.version + \", new version: \" + version + \".\"\n                )\n        }\n        this.version = version\n        statements = ArrayList()\n        statementsMap.put(version, statements)\n        return this\n    }\n\n    /**\n     * Creates a table with int [.COLUMN_ID] primary key.\n     */\n    fun createTable(\n        table: String,\n        column: String = COLUMN_ID,\n        clazz: Class<*>? = Int::class.javaPrimitiveType,\n    ): MSQLiteBuilder = statement(\"CREATE TABLE \" + table + \" (\" + column + \" \" + javaTypeToSQLiteType(clazz) + \" PRIMARY KEY);\")\n\n    /**\n     * Drops a table.\n     */\n    fun dropTable(table: String): MSQLiteBuilder = statement(\"DROP TABLE $table;\")\n\n    /**\n     * Inserts a column to the table.\n     */\n    fun insertColumn(table: String, column: String, clazz: Class<*>?): MSQLiteBuilder = statement(\n        \"ALTER TABLE $table ADD COLUMN $column \" + javaTypeToSQLiteType(\n            clazz,\n        ) + \";\",\n    )\n\n    /**\n     * Add a statement.\n     */\n    fun statement(statement: String): MSQLiteBuilder {\n        check(!(version == 0 || statements == null)) { \"Call version() first!\" }\n        statements!!.add(statement)\n        return this\n    }\n\n    /**\n     * Build a SQLiteOpenHelper from it.\n     */\n    fun build(context: Context?, name: String?, version: Int): SQLiteOpenHelper = MSQLiteOpenHelper(context, name, version, this)\n\n    fun getStatements(oldVersion: Int, newVersion: Int): List<String> {\n        val result: MutableList<String> = ArrayList()\n        for (i in oldVersion + 1..newVersion) {\n            val list = statementsMap[i]\n            if (list != null) {\n                result.addAll(list)\n            }\n        }\n        return result\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/database/MSQLiteOpenHelper.kt",
    "content": "/*\n * Copyright 2017 Hippo Seven\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.hippo.database\n\nimport android.content.Context\nimport android.database.sqlite.SQLiteDatabase\nimport android.database.sqlite.SQLiteOpenHelper\nimport com.hippo.util.SqlUtils\n\ninternal class MSQLiteOpenHelper(\n    context: Context?,\n    name: String?,\n    private val version: Int,\n    private val builder: MSQLiteBuilder,\n) : SQLiteOpenHelper(context, name, null, version) {\n    override fun onCreate(db: SQLiteDatabase) {\n        onUpgrade(db, 0, version)\n    }\n\n    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {\n        for (command in builder.getStatements(oldVersion, newVersion)) {\n            db.execSQL(command)\n        }\n    }\n\n    override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {\n        SqlUtils.dropAllTable(db)\n        onCreate(db)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/drawable/AddDeleteDrawable.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.drawable\n\nimport android.animation.ObjectAnimator\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.ColorFilter\nimport android.graphics.Paint\nimport android.graphics.Path\nimport android.graphics.PixelFormat\nimport android.graphics.drawable.Drawable\nimport androidx.annotation.Keep\nimport androidx.core.graphics.withTranslation\nimport com.hippo.ehviewer.R\nimport com.hippo.yorozuya.MathUtils\nimport kotlin.math.roundToInt\n\n/**\n * @param context used to get the configuration for the drawable from\n */\nclass AddDeleteDrawable(context: Context, color: Int) : Drawable() {\n    private val mPaint = Paint()\n    private val mPath = Path()\n    private val mSize: Int\n    private var mProgress = 0f\n    private var mAutoUpdateMirror = false\n    private var mVerticalMirror = false\n\n    init {\n        val resources = context.resources\n        mSize = resources.getDimensionPixelSize(R.dimen.add_size)\n        val barThickness = resources.getDimension(R.dimen.add_thickness).roundToInt().toFloat()\n        mPaint.setColor(color)\n        mPaint.style = Paint.Style.STROKE\n        mPaint.strokeJoin = Paint.Join.MITER\n        mPaint.strokeCap = Paint.Cap.BUTT\n        mPaint.strokeWidth = barThickness\n        val halfSize = (mSize / 2).toFloat()\n        mPath.moveTo(0f, -halfSize)\n        mPath.lineTo(0f, halfSize)\n        mPath.moveTo(-halfSize, 0f)\n        mPath.lineTo(halfSize, 0f)\n    }\n\n    override fun draw(canvas: Canvas) {\n        val bounds = getBounds()\n        val canvasRotate: Float = if (mVerticalMirror) {\n            MathUtils.lerp(270f, 135f, mProgress)\n        } else {\n            MathUtils.lerp(0f, 135f, mProgress)\n        }\n        canvas.withTranslation(bounds.centerX().toFloat(), bounds.centerY().toFloat()) {\n            rotate(canvasRotate)\n            drawPath(mPath, mPaint)\n        }\n    }\n\n    fun setColor(color: Int) {\n        mPaint.setColor(color)\n        invalidateSelf()\n    }\n\n    override fun setAlpha(alpha: Int) {\n        mPaint.setAlpha(alpha)\n    }\n\n    override fun setColorFilter(cf: ColorFilter?) {\n        mPaint.setColorFilter(cf)\n    }\n\n    override fun getIntrinsicHeight(): Int = mSize * 6 / 5\n\n    override fun getIntrinsicWidth(): Int = mSize * 6 / 5\n\n    /**\n     * If set, canvas is flipped when progress reached to end and going back to start.\n     */\n    private fun setVerticalMirror(verticalMirror: Boolean) {\n        mVerticalMirror = verticalMirror\n    }\n\n    @get:Keep\n    @set:Keep\n    var progress: Float\n        get() = mProgress\n        set(progress) {\n            if (mAutoUpdateMirror) {\n                if (progress == 1f) {\n                    setVerticalMirror(true)\n                } else if (progress == 0f) {\n                    setVerticalMirror(false)\n                }\n            }\n            mProgress = progress\n            invalidateSelf()\n        }\n\n    fun setAdd(duration: Long) {\n        setShape(false, duration)\n    }\n\n    fun setDelete(duration: Long) {\n        setShape(true, duration)\n    }\n\n    private fun setShape(delete: Boolean, duration: Long) {\n        if (!(!delete && mProgress == 0f || delete && mProgress == 1f)) {\n            val endProgress = if (delete) 1f else 0f\n            if (duration <= 0) {\n                progress = endProgress\n            } else {\n                val oa = ObjectAnimator.ofFloat(this, \"progress\", endProgress)\n                oa.setDuration(duration)\n                oa.setAutoCancel(true)\n                oa.start()\n            }\n        }\n    }\n\n    @Deprecated(\n        \"Deprecated in Java\",\n        ReplaceWith(\"PixelFormat.TRANSLUCENT\", \"android.graphics.PixelFormat\"),\n    )\n    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/drawable/BatteryDrawable.kt",
    "content": "/*\n * Copyright (C) 2014 Hippo Seven\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.hippo.drawable\n\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.ColorFilter\nimport android.graphics.Paint\nimport android.graphics.PixelFormat\nimport android.graphics.Rect\nimport android.graphics.drawable.Drawable\nimport com.hippo.yorozuya.MathUtils\nimport kotlin.math.sqrt\n\nclass BatteryDrawable : Drawable() {\n    private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG)\n    private val mTopRect: Rect\n    private val mBottomRect: Rect\n    private val mRightRect: Rect\n    private val mHeadRect: Rect\n    private val mElectRect: Rect\n    private var mColor = Color.WHITE\n    private var mWarningColor = Color.RED\n    private var mElect = -1\n    private var mStart = 0\n    private var mStop = 0\n\n    init {\n        mPaint.style = Paint.Style.FILL\n        mTopRect = Rect()\n        mBottomRect = Rect()\n        mRightRect = Rect()\n        mHeadRect = Rect()\n        mElectRect = Rect()\n        updatePaint()\n    }\n\n    override fun onBoundsChange(bounds: Rect) {\n        val width = bounds.width()\n        val height = bounds.height()\n        val strokeWidth = (sqrt((width * width + height * height).toDouble()) * 0.06f).toInt()\n        val turn1 = width * 6 / 7\n        val turn2 = height / 3\n        val secBottom = height - strokeWidth\n        mStart = strokeWidth\n        mStop = turn1 - strokeWidth\n        mTopRect[0, 0, turn1] = strokeWidth\n        mBottomRect[0, secBottom, turn1] = height\n        mRightRect[turn1 - strokeWidth, strokeWidth, turn1] = secBottom\n        mHeadRect[turn1, turn2, width] = height - turn2\n        mElectRect[0, strokeWidth, mStop] = secBottom\n    }\n\n    /**\n     * How to draw:\n     * |------------------------------|\n     * |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|\n     * |------------------------------|---|\n     * |/////////////////|         |//|\\\\\\|\n     * |/////////////////|         |//|\\\\\\|\n     * |------------------------------|---|\n     * |\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\|\n     * |------------------------------|\n     */\n    override fun draw(canvas: Canvas) {\n        if (mElect == -1) {\n            return\n        }\n        mElectRect.right = MathUtils.lerp(mStart, mStop, mElect / 100.0f)\n        canvas.drawRect(mTopRect, mPaint)\n        canvas.drawRect(mBottomRect, mPaint)\n        canvas.drawRect(mRightRect, mPaint)\n        canvas.drawRect(mHeadRect, mPaint)\n        canvas.drawRect(mElectRect, mPaint)\n    }\n\n    private val isWarn: Boolean\n        get() = mElect <= WARN_LIMIT\n\n    fun setColor(color: Int) {\n        if (mColor == color) {\n            return\n        }\n        mColor = color\n        if (!isWarn) {\n            mPaint.setColor(mColor)\n            invalidateSelf()\n        }\n    }\n\n    fun setWarningColor(color: Int) {\n        if (mWarningColor == color) {\n            return\n        }\n        mWarningColor = color\n        if (isWarn) {\n            mPaint.setColor(mWarningColor)\n            invalidateSelf()\n        }\n    }\n\n    fun setElect(elect: Int) {\n        if (mElect == elect) {\n            return\n        }\n        mElect = elect\n        updatePaint()\n    }\n\n    fun setElect(elect: Int, warn: Boolean) {\n        if (mElect == elect) {\n            return\n        }\n        mElect = elect\n        updatePaint(warn)\n    }\n\n    private fun updatePaint(warn: Boolean = isWarn) {\n        if (warn) {\n            mPaint.setColor(mWarningColor)\n        } else {\n            mPaint.setColor(mColor)\n        }\n        invalidateSelf()\n    }\n\n    override fun setAlpha(alpha: Int) {\n        mPaint.setAlpha(alpha)\n    }\n\n    override fun setColorFilter(cf: ColorFilter?) {\n        mPaint.setColorFilter(cf)\n    }\n\n    @Deprecated(\n        \"Deprecated in Java\",\n        ReplaceWith(\"PixelFormat.TRANSLUCENT\", \"android.graphics.PixelFormat\"),\n    )\n    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT\n\n    companion object {\n        const val WARN_LIMIT = 15\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/drawable/DrawerArrowDrawable.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.drawable\n\nimport android.animation.ObjectAnimator\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.ColorFilter\nimport android.graphics.Paint\nimport android.graphics.Path\nimport android.graphics.PixelFormat\nimport android.graphics.drawable.Drawable\nimport androidx.annotation.ColorInt\nimport androidx.annotation.Keep\nimport androidx.core.graphics.withTranslation\nimport com.hippo.ehviewer.R\nimport com.hippo.yorozuya.MathUtils\nimport kotlin.math.cos\nimport kotlin.math.roundToInt\nimport kotlin.math.sin\n\n/**\n * A drawable that can draw a \"Drawer hamburger\" menu or an Arrow and animate between them.\n * @param context used to get the configuration for the drawable from\n*/\nclass DrawerArrowDrawable(context: Context, color: Int) : Drawable() {\n    private val mPaint = Paint()\n    private val mBarThickness: Float\n\n    // The length of top and bottom bars when they merge into an arrow\n    private val mTopBottomArrowSize: Float\n\n    // The length of middle bar\n    private val mBarSize: Float\n\n    // The length of the middle bar when arrow is shaped\n    private val mMiddleArrowSize: Float\n\n    // The space between bars when they are parallel\n    private val mBarGap: Float\n\n    // Whether bars should spin or not during progress\n    private val mSpin: Boolean\n\n    // Use Path instead of canvas operations so that if color has transparency, overlapping sections\n    // wont look different\n    private val mPath = Path()\n\n    // The reported intrinsic size of the drawable.\n    private val mSize: Int\n\n    // the amount that overlaps w/ bar size when rotation is max\n    private val mMaxCutForBarSize: Float\n\n    // Whether we should mirror animation when animation is reversed.\n    private var mVerticalMirror = false\n\n    // The interpolated version of the original progress\n    private var mProgress = 0f\n\n    init {\n        val resources = context.resources\n        mPaint.isAntiAlias = true\n        mPaint.setColor(color)\n        mSize = resources.getDimensionPixelSize(R.dimen.dad_drawable_size)\n        // round this because having this floating may cause bad measurements\n        mBarSize = resources.getDimension(R.dimen.dad_bar_size).roundToInt().toFloat()\n        // round this because having this floating may cause bad measurements\n        mTopBottomArrowSize =\n            resources.getDimension(R.dimen.dad_top_bottom_bar_arrow_size).roundToInt().toFloat()\n        mBarThickness = resources.getDimension(R.dimen.dad_thickness)\n        // round this because having this floating may cause bad measurements\n        mBarGap = resources.getDimension(R.dimen.dad_gap_between_bars).roundToInt().toFloat()\n        mSpin = resources.getBoolean(R.bool.dad_spin_bars)\n        mMiddleArrowSize = resources.getDimension(R.dimen.dad_middle_bar_arrow_size)\n        mPaint.style = Paint.Style.STROKE\n        mPaint.strokeJoin = Paint.Join.MITER\n        mPaint.strokeCap = Paint.Cap.BUTT\n        mPaint.strokeWidth = mBarThickness\n        mMaxCutForBarSize = (mBarThickness / 2 * cos(ARROW_HEAD_ANGLE.toDouble())).toFloat()\n    }\n\n    /**\n     * If set, canvas is flipped when progress reached to end and going back to start.\n     */\n    private fun setVerticalMirror(verticalMirror: Boolean) {\n        mVerticalMirror = verticalMirror\n    }\n\n    override fun draw(canvas: Canvas) {\n        val bounds = getBounds()\n        // Interpolated widths of arrow bars\n        val arrowSize = MathUtils.lerp(mBarSize, mTopBottomArrowSize, mProgress)\n        val middleBarSize = MathUtils.lerp(mBarSize, mMiddleArrowSize, mProgress)\n        // Interpolated size of middle bar\n        val middleBarCut = MathUtils.lerp(0f, mMaxCutForBarSize, mProgress).roundToInt().toFloat()\n        // The rotation of the top and bottom bars (that make the arrow head)\n        val rotation = MathUtils.lerp(0f, ARROW_HEAD_ANGLE, mProgress)\n\n        // The whole canvas rotates as the transition happens\n        val canvasRotate = MathUtils.lerp(-180, 0, mProgress).toFloat()\n        val arrowWidth = (arrowSize * cos(rotation.toDouble())).roundToInt().toFloat()\n        val arrowHeight = (arrowSize * sin(rotation.toDouble())).roundToInt().toFloat()\n        mPath.rewind()\n        val topBottomBarOffset = MathUtils.lerp(\n            mBarGap + mBarThickness,\n            -mMaxCutForBarSize,\n            mProgress,\n        )\n        val arrowEdge = -middleBarSize / 2\n        // draw middle bar\n        mPath.moveTo(arrowEdge + middleBarCut, 0f)\n        mPath.rLineTo(middleBarSize - middleBarCut * 2, 0f)\n\n        // bottom bar\n        mPath.moveTo(arrowEdge, topBottomBarOffset)\n        mPath.rLineTo(arrowWidth, arrowHeight)\n\n        // top bar\n        mPath.moveTo(arrowEdge, -topBottomBarOffset)\n        mPath.rLineTo(arrowWidth, -arrowHeight)\n        mPath.close()\n        canvas.withTranslation(bounds.centerX().toFloat(), bounds.centerY().toFloat()) {\n            // Rotate the whole canvas if spinning, if not, rotate it 180 to get\n            // the arrow pointing the other way for RTL.\n            if (mSpin) {\n                rotate(canvasRotate * if (mVerticalMirror) -1 else 1)\n            }\n            drawPath(mPath, mPaint)\n        }\n    }\n\n    fun setColor(@ColorInt color: Int) {\n        mPaint.setColor(color)\n        invalidateSelf()\n    }\n\n    override fun setAlpha(i: Int) {\n        mPaint.setAlpha(i)\n    }\n\n    override fun setColorFilter(colorFilter: ColorFilter?) {\n        mPaint.setColorFilter(colorFilter)\n    }\n\n    override fun getIntrinsicHeight(): Int = mSize\n\n    override fun getIntrinsicWidth(): Int = mSize\n\n    @Deprecated(\n        \"Deprecated in Java\",\n        ReplaceWith(\"PixelFormat.TRANSLUCENT\", \"android.graphics.PixelFormat\"),\n    )\n    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT\n\n    @get:Keep\n    @set:Keep\n    var progress: Float\n        get() = mProgress\n        set(progress) {\n            if (progress == 1f) {\n                setVerticalMirror(true)\n            } else if (progress == 0f) {\n                setVerticalMirror(false)\n            }\n            mProgress = progress\n            invalidateSelf()\n        }\n\n    fun setMenu(duration: Long) {\n        setShape(false, duration)\n    }\n\n    fun setArrow(duration: Long) {\n        setShape(true, duration)\n    }\n\n    private fun setShape(arrow: Boolean, duration: Long) {\n        if (!(!arrow && mProgress == 0f || arrow && mProgress == 1f)) {\n            val endProgress = if (arrow) 1f else 0f\n            if (duration <= 0) {\n                progress = endProgress\n            } else {\n                val oa = ObjectAnimator.ofFloat(this, \"progress\", endProgress)\n                oa.setDuration(duration)\n                oa.setAutoCancel(true)\n                oa.start()\n            }\n        }\n    }\n\n    companion object {\n        // The angle in degrees that the arrow head is inclined at.\n        private val ARROW_HEAD_ANGLE = Math.toRadians(45.0).toFloat()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/drawable/PreciselyClipDrawable.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.drawable\n\nimport android.graphics.Canvas\nimport android.graphics.Rect\nimport android.graphics.RectF\nimport android.graphics.drawable.Drawable\nimport android.graphics.drawable.DrawableWrapper\nimport androidx.core.graphics.withClip\n\n/**\n * Show a part of the original drawable\n */\nclass PreciselyClipDrawable(\n    drawable: Drawable,\n    offsetX: Int,\n    offsetY: Int,\n    width: Int,\n    height: Int,\n) : DrawableWrapper(drawable) {\n    private val mScale: RectF\n    private val mTemp = Rect()\n\n    init {\n        val originWidth = drawable.intrinsicWidth.toFloat()\n        val originHeight = drawable.intrinsicHeight.toFloat()\n        mScale = RectF(\n            (offsetX / originWidth).coerceIn(0.0f, 1.0f),\n            (offsetY / originHeight).coerceIn(0.0f, 1.0f),\n            ((offsetX + width) / originWidth).coerceIn(0.0f, 1.0f),\n            ((offsetY + height) / originHeight).coerceIn(0.0f, 1.0f),\n        )\n    }\n\n    override fun onBoundsChange(bounds: Rect) {\n        mTemp.left = ((mScale.left * bounds.right - mScale.right * bounds.left) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()\n        mTemp.right = (((1 - mScale.right) * bounds.left - (1 - mScale.left) * bounds.right) / (mScale.left * (1 - mScale.right) - mScale.right * (1 - mScale.left))).toInt()\n        mTemp.top = ((mScale.top * bounds.bottom - mScale.bottom * bounds.top) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()\n        mTemp.bottom = (((1 - mScale.bottom) * bounds.top - (1 - mScale.top) * bounds.bottom) / (mScale.top * (1 - mScale.bottom) - mScale.bottom * (1 - mScale.top))).toInt()\n        super.onBoundsChange(mTemp)\n    }\n\n    override fun getIntrinsicWidth(): Int = (super.intrinsicWidth * mScale.width()).toInt()\n\n    override fun getIntrinsicHeight(): Int = (super.intrinsicHeight * mScale.height()).toInt()\n\n    override fun draw(canvas: Canvas) {\n        canvas.withClip(bounds) { super.draw(canvas) }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/drawable/TriangleDrawable.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.drawable\n\nimport android.graphics.Canvas\nimport android.graphics.ColorFilter\nimport android.graphics.Paint\nimport android.graphics.Path\nimport android.graphics.PixelFormat\nimport android.graphics.Rect\nimport android.graphics.drawable.Drawable\n\nclass TriangleDrawable(color: Int) : Drawable() {\n    private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG)\n    private val mPath: Path\n\n    init {\n        mPaint.setColor(color)\n        mPath = Path()\n    }\n\n    fun setColor(color: Int) {\n        mPaint.setColor(color)\n        invalidateSelf()\n    }\n\n    override fun onBoundsChange(bounds: Rect) {\n        super.onBoundsChange(bounds)\n        mPath.reset()\n        mPath.moveTo(bounds.left.toFloat(), bounds.top.toFloat())\n        mPath.lineTo(bounds.right.toFloat(), bounds.top.toFloat())\n        mPath.lineTo(bounds.right.toFloat(), bounds.bottom.toFloat())\n        mPath.close()\n    }\n\n    override fun draw(canvas: Canvas) {\n        canvas.drawPath(mPath, mPaint)\n    }\n\n    override fun setAlpha(alpha: Int) {\n        mPaint.setAlpha(alpha)\n    }\n\n    override fun setColorFilter(colorFilter: ColorFilter?) {\n        mPaint.setColorFilter(colorFilter)\n    }\n\n    @Deprecated(\n        \"Deprecated in Java\",\n        ReplaceWith(\"PixelFormat.OPAQUE\", \"android.graphics.PixelFormat\"),\n    )\n    override fun getOpacity(): Int = PixelFormat.OPAQUE\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/drawable/UnikeryDrawable.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.drawable\n\nimport android.graphics.drawable.Animatable\nimport android.graphics.drawable.Drawable\nimport coil3.Image\nimport coil3.asDrawable\nimport coil3.imageLoader\nimport coil3.request.Disposable\nimport coil3.request.ImageRequest\nimport com.hippo.widget.ObservedTextView\n\nclass UnikeryDrawable(private val mTextView: ObservedTextView) :\n    WrapDrawable(),\n    ObservedTextView.OnWindowAttachListener {\n    private var mUrl: String? = null\n    private var task: Disposable? = null\n\n    init {\n        mTextView.setOnWindowAttachListener(this)\n    }\n\n    override fun onAttachedToWindow() {\n        load(mUrl)\n    }\n\n    override fun onDetachedFromWindow() {\n        if (task != null && !task!!.isDisposed) task!!.dispose()\n        clearDrawable()\n    }\n\n    fun load(url: String?) {\n        if (url != null) {\n            mUrl = url\n            val request =\n                ImageRequest.Builder(mTextView.context).data(url)\n                    .memoryCacheKey(url)\n                    .diskCacheKey(url)\n                    .target(onSuccess = ::onGetValue)\n                    .build()\n            task = mTextView.context.imageLoader.enqueue(request)\n        }\n    }\n\n    private fun clearDrawable() {\n        drawable = null\n    }\n\n    override var drawable: Drawable?\n        get() = super.drawable\n        set(newDrawable) {\n            // Remove old callback\n            val oldDrawable = drawable\n            oldDrawable?.callback = null\n            super.drawable = newDrawable\n            newDrawable?.callback = mTextView\n            updateBounds()\n            if (newDrawable != null) {\n                invalidateSelf()\n            }\n        }\n\n    override fun invalidateSelf() {\n        val cs = mTextView.getText()\n        mTextView.text = cs\n    }\n\n    private fun onGetValue(image: Image) {\n        clearDrawable()\n        drawable = image.asDrawable(mTextView.resources)\n        (drawable as? Animatable)?.start()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/drawable/WrapDrawable.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.drawable\n\nimport android.graphics.Canvas\nimport android.graphics.ColorFilter\nimport android.graphics.PixelFormat\nimport android.graphics.Rect\nimport android.graphics.drawable.Drawable\n\nopen class WrapDrawable : Drawable() {\n    open var drawable: Drawable? = null\n\n    fun updateBounds() {\n        setBounds(0, 0, intrinsicWidth, intrinsicHeight)\n    }\n\n    override fun draw(canvas: Canvas) {\n        drawable?.draw(canvas)\n    }\n\n    override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {\n        super.setBounds(left, top, right, bottom)\n        drawable?.setBounds(left, top, right, bottom)\n    }\n\n    override fun setBounds(bounds: Rect) {\n        super.bounds = bounds\n        drawable?.bounds = bounds\n    }\n\n    override fun getChangingConfigurations(): Int = drawable?.changingConfigurations ?: super.changingConfigurations\n\n    override fun setChangingConfigurations(configs: Int) {\n        super.changingConfigurations = configs\n        drawable?.changingConfigurations = configs\n    }\n\n    override fun setFilterBitmap(filter: Boolean) {\n        super.setFilterBitmap(filter)\n        drawable?.setFilterBitmap(filter)\n    }\n\n    override fun setAlpha(alpha: Int) {\n        drawable?.alpha = alpha\n    }\n\n    override fun setColorFilter(cf: ColorFilter?) {\n        drawable?.colorFilter = cf\n    }\n\n    @Deprecated(\n        \"Deprecated in Java\",\n        ReplaceWith(\"PixelFormat.TRANSLUCENT\", \"android.graphics.PixelFormat\"),\n    )\n    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT\n\n    override fun getIntrinsicWidth(): Int = drawable?.intrinsicWidth ?: super.intrinsicWidth\n\n    override fun getIntrinsicHeight(): Int = drawable?.intrinsicHeight ?: super.intrinsicHeight\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/EasyRecyclerView.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.content.Context\nimport android.os.Parcel\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.util.SparseBooleanArray\nimport android.view.ActionMode\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.widget.Checkable\nimport androidx.collection.LongSparseArray\nimport androidx.core.util.isEmpty\nimport androidx.core.util.size\nimport androidx.recyclerview.widget.RecyclerView\nimport com.hippo.util.readParcelableCompat\nimport com.hippo.yorozuya.NumberUtils\n\n/**\n * Add setChoiceMode for RecyclerView\n */\n// Get some code from twoway-view and AbsListView.\nopen class EasyRecyclerView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0,\n) : RecyclerView(\n    context,\n    attrs,\n    defStyle,\n) {\n    /**\n     * Wrapper for the multiple choice mode callback; AbsListView needs to perform\n     * a few extra actions around what application code does.\n     */\n    private var mMultiChoiceModeCallback: MultiChoiceModeWrapper? = null\n\n    /**\n     * Controls if/how the user may choose/check items in the list\n     */\n    private var mChoiceMode = CHOICE_MODE_NONE\n\n    /**\n     * Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive.\n     */\n    private var mChoiceActionMode: ActionMode? = null\n\n    /**\n     * Listener for custom multiple choices\n     */\n    private var mCustomChoiceListener: CustomChoiceListener? = null\n    var isInCustomChoice = false\n        private set\n\n    /**\n     * A lock, avoid OutOfCustomChoiceMode when doing OutOfCustomChoiceMode\n     */\n    private var mOutOfCustomChoiceModing = false\n    private var mTempCheckStates: SparseBooleanArray? = null\n\n    /**\n     * Running count of how many items are currently checked\n     */\n    var checkedItemCount = 0\n        private set\n\n    /**\n     * Running state of which positions are currently checked\n     */\n    private var mCheckStates: SparseBooleanArray? = null\n\n    /**\n     * Running state of which IDs are currently checked.\n     * If there is a value for a given key, the checked state for that ID is true\n     * and the value holds the last known position in the adapter for that id.\n     */\n    private var mCheckedIdStates: LongSparseArray<Int>? = null\n    private var mAdapter: Adapter<*>? = null\n\n    /**\n     * {@inheritDoc}\n     */\n    override fun setAdapter(adapter: Adapter<*>?) {\n        super.setAdapter(adapter)\n        mAdapter = adapter\n        if (adapter != null &&\n            adapter.hasStableIds() &&\n            mChoiceMode != CHOICE_MODE_NONE &&\n            mCheckedIdStates == null\n        ) {\n            mCheckedIdStates = LongSparseArray()\n        }\n        mCheckStates?.clear()\n        mCheckedIdStates?.clear()\n    }\n\n    override fun onChildAttachedToWindow(child: View) {\n        super.onChildAttachedToWindow(child)\n        if (mCheckStates != null) {\n            val position = getChildAdapterPosition(child)\n            if (position >= 0) {\n                setViewChecked(child, mCheckStates!![position])\n            }\n        }\n    }\n\n    /**\n     * Returns the checked state of the specified position. The result is only\n     * valid if the choice mode has been set to [.CHOICE_MODE_SINGLE]\n     * or [.CHOICE_MODE_MULTIPLE].\n     *\n     * @param position The item whose checked state to return\n     * @return The item's checked state or `false` if choice mode\n     * is invalid\n     * @see .setChoiceMode\n     */\n    private fun isItemChecked(position: Int): Boolean = if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {\n        mCheckStates!![position]\n    } else {\n        false\n    }\n\n    /**\n     * Returns the set of checked items in the list. The result is only valid if\n     * the choice mode has not been set to [.CHOICE_MODE_NONE].\n     *\n     * @return A SparseBooleanArray which will return true for each call to\n     * get(int position) where position is a checked position in the\n     * list and false otherwise, or `null` if the choice\n     * mode is set to [.CHOICE_MODE_NONE].\n     */\n    val checkedItemPositions: SparseBooleanArray?\n        get() = if (mChoiceMode != CHOICE_MODE_NONE) {\n            mCheckStates\n        } else {\n            null\n        }\n\n    fun intoCustomChoiceMode() {\n        if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !isInCustomChoice) {\n            isInCustomChoice = true\n            mCustomChoiceListener!!.onIntoCustomChoice(this)\n        }\n    }\n\n    fun outOfCustomChoiceMode() {\n        if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && isInCustomChoice && !mOutOfCustomChoiceModing) {\n            mOutOfCustomChoiceModing = true\n            // Copy mCheckStates\n            mTempCheckStates!!.clear()\n            run {\n                for (i in 0 until mCheckStates!!.size) {\n                    mTempCheckStates!!.put(mCheckStates!!.keyAt(i), mCheckStates!!.valueAt(i))\n                }\n            }\n            // Uncheck remain checked items\n            for (i in 0 until mTempCheckStates!!.size) {\n                if (mTempCheckStates!!.valueAt(i)) {\n                    setItemChecked(mTempCheckStates!!.keyAt(i), false)\n                }\n            }\n            isInCustomChoice = false\n            mCustomChoiceListener!!.onOutOfCustomChoice(this)\n            mOutOfCustomChoiceModing = false\n        }\n    }\n\n    /**\n     * Clear any choices previously set\n     */\n    private fun clearChoices() {\n        mCheckStates?.clear()\n        mCheckedIdStates?.clear()\n        checkedItemCount = 0\n        updateOnScreenCheckedViews()\n    }\n\n    /**\n     * Sets all items checked.\n     */\n    fun checkAll() {\n        if (mChoiceMode == CHOICE_MODE_NONE || mChoiceMode == CHOICE_MODE_SINGLE) {\n            return\n        }\n\n        // Check is intoCheckMode\n        check(!(mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !isInCustomChoice)) { \"Call intoCheckMode first\" }\n\n        // Start selection mode if needed. We don't need to if we're unchecking something.\n        if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {\n            check(\n                !(\n                    mMultiChoiceModeCallback == null ||\n                        !mMultiChoiceModeCallback!!.hasWrappedCallback()\n                    ),\n            ) {\n                \"EasyRecyclerView: attempted to start selection mode \" +\n                    \"for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was \" +\n                    \"supplied. Call setMultiChoiceModeListener to set a callback.\"\n            }\n            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)\n        }\n        for (i in 0 until mAdapter!!.itemCount) {\n            val oldValue = mCheckStates!![i]\n            mCheckStates!!.put(i, true)\n            if (mCheckedIdStates != null && mAdapter!!.hasStableIds()) {\n                mCheckedIdStates!!.put(mAdapter!!.getItemId(i), i)\n            }\n            if (!oldValue) {\n                checkedItemCount++\n            }\n            if (mChoiceActionMode != null) {\n                val id = mAdapter!!.getItemId(i)\n                mMultiChoiceModeCallback!!.onItemCheckedStateChanged(\n                    mChoiceActionMode!!,\n                    i,\n                    id,\n                    true,\n                )\n            }\n            if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {\n                val id = mAdapter!!.getItemId(i)\n                mCustomChoiceListener!!.onItemCheckedStateChanged(this, i, id, true)\n            }\n        }\n        updateOnScreenCheckedViews()\n    }\n\n    fun toggleItemChecked(position: Int) {\n        if (mCheckStates != null) {\n            setItemChecked(position, !mCheckStates!![position])\n        }\n    }\n\n    /**\n     * Sets the checked state of the specified position. The is only valid if\n     * the choice mode has been set to [.CHOICE_MODE_SINGLE] or\n     * [.CHOICE_MODE_MULTIPLE].\n     *\n     * @param position The item whose checked state is to be checked\n     * @param value    The new checked state for the item\n     */\n    private fun setItemChecked(position: Int, value: Boolean) {\n        if (mChoiceMode == CHOICE_MODE_NONE) {\n            return\n        }\n\n        // Check is intoCheckMode\n        check(!(mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM && !isInCustomChoice)) { \"Call intoCheckMode first\" }\n\n        // Start selection mode if needed. We don't need to if we're unchecking something.\n        if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {\n            check(\n                !(\n                    mMultiChoiceModeCallback == null ||\n                        !mMultiChoiceModeCallback!!.hasWrappedCallback()\n                    ),\n            ) {\n                \"EasyRecyclerView: attempted to start selection mode \" +\n                    \"for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was \" +\n                    \"supplied. Call setMultiChoiceModeListener to set a callback.\"\n            }\n            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)\n        }\n        if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL || mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {\n            val oldValue = mCheckStates!![position]\n            mCheckStates!!.put(position, value)\n            if (mCheckedIdStates != null && mAdapter!!.hasStableIds()) {\n                if (value) {\n                    mCheckedIdStates!!.put(mAdapter!!.getItemId(position), position)\n                } else {\n                    mCheckedIdStates!!.remove(mAdapter!!.getItemId(position))\n                }\n            }\n            if (oldValue != value) {\n                if (value) {\n                    checkedItemCount++\n                } else {\n                    checkedItemCount--\n                }\n            }\n            if (mChoiceActionMode != null) {\n                val id = mAdapter!!.getItemId(position)\n                mMultiChoiceModeCallback!!.onItemCheckedStateChanged(\n                    mChoiceActionMode!!,\n                    position,\n                    id,\n                    value,\n                )\n            }\n            if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {\n                val id = mAdapter!!.getItemId(position)\n                mCustomChoiceListener!!.onItemCheckedStateChanged(this, position, id, value)\n            }\n        } else {\n            val updateIds = mCheckedIdStates != null && mAdapter!!.hasStableIds()\n            // Clear all values if we're checking something, or unchecking the currently\n            // selected item\n            if (value || isItemChecked(position)) {\n                mCheckStates!!.clear()\n                if (updateIds) {\n                    mCheckedIdStates!!.clear()\n                }\n            }\n            // this may end up selecting the value we just cleared but this way\n            // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on\n            if (value) {\n                mCheckStates!!.put(position, true)\n                if (updateIds) {\n                    mCheckedIdStates!!.put(mAdapter!!.getItemId(position), position)\n                }\n                checkedItemCount = 1\n            } else if (mCheckStates!!.isEmpty() || !mCheckStates!!.valueAt(0)) {\n                checkedItemCount = 0\n            }\n        }\n        updateOnScreenCheckedViews()\n    }\n\n    /**\n     * Defines the choice behavior for the List. By default, Lists do not have any choice behavior\n     * ([.CHOICE_MODE_NONE]). By setting the choiceMode to [.CHOICE_MODE_SINGLE], the\n     * List allows up to one item to  be in a chosen state. By setting the choiceMode to\n     * [.CHOICE_MODE_MULTIPLE], the list allows any number of items to be chosen.\n     *\n     * @param choiceMode One of [.CHOICE_MODE_NONE], [.CHOICE_MODE_SINGLE], or\n     * [.CHOICE_MODE_MULTIPLE]\n     */\n    fun setChoiceMode(choiceMode: Int) {\n        mChoiceMode = choiceMode\n        if (mChoiceActionMode != null) {\n            mChoiceActionMode!!.finish()\n            mChoiceActionMode = null\n        }\n        if (mChoiceMode != CHOICE_MODE_NONE) {\n            if (mCheckStates == null) {\n                mCheckStates = SparseBooleanArray(0)\n            }\n            if (mCheckedIdStates == null && mAdapter != null && mAdapter!!.hasStableIds()) {\n                mCheckedIdStates = LongSparseArray(0)\n            }\n            // Modal multi-choice mode only has choices when the mode is active. Clear them.\n            if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {\n                clearChoices()\n                isLongClickable = true\n            } else if (mChoiceMode == CHOICE_MODE_MULTIPLE_CUSTOM) {\n                if (mTempCheckStates == null) {\n                    mTempCheckStates = SparseBooleanArray(0)\n                }\n                clearChoices()\n            }\n        }\n    }\n\n    fun setCustomCheckedListener(listener: CustomChoiceListener?) {\n        mCustomChoiceListener = listener\n    }\n\n    /**\n     * Perform a quick, in-place update of the checked or activated state\n     * on all visible item views. This should only be called when a valid\n     * choice mode is active.\n     */\n    private fun updateOnScreenCheckedViews() {\n        for (i in 0 until childCount) {\n            val child = getChildAt(i)\n            val position = getChildAdapterPosition(child)\n            setViewChecked(child, mCheckStates!![position])\n        }\n    }\n\n    public override fun onSaveInstanceState(): Parcelable {\n        val ss = SavedState(super.onSaveInstanceState())\n        ss.choiceMode = mChoiceMode\n        ss.customChoice = isInCustomChoice\n        ss.checkedItemCount = checkedItemCount\n        ss.checkState = mCheckStates\n        ss.checkIdState = mCheckedIdStates\n        return ss\n    }\n\n    public override fun onRestoreInstanceState(state: Parcelable) {\n        val ss = state as SavedState\n        super.onRestoreInstanceState(ss.superState)\n        setChoiceMode(ss.choiceMode)\n        isInCustomChoice = ss.customChoice\n        checkedItemCount = ss.checkedItemCount\n        if (ss.checkState != null) {\n            mCheckStates = ss.checkState\n        }\n        if (ss.checkIdState != null) {\n            mCheckedIdStates = ss.checkIdState\n        }\n        if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && checkedItemCount > 0) {\n            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)\n        }\n        updateOnScreenCheckedViews()\n    }\n\n    /**\n     * A MultiChoiceModeListener receives events for [android.widget.AbsListView.CHOICE_MODE_MULTIPLE_MODAL].\n     * It acts as the [android.view.ActionMode.Callback] for the selection mode and also receives\n     * [.onItemCheckedStateChanged] events when the user\n     * selects and deselects list items.\n     */\n    interface MultiChoiceModeListener : ActionMode.Callback {\n        /**\n         * Called when an item is checked or unchecked during selection mode.\n         *\n         * @param mode     The [android.view.ActionMode] providing the selection mode\n         * @param position Adapter position of the item that was checked or unchecked\n         * @param id       Adapter ID of the item that was checked or unchecked\n         * @param checked  `true` if the item is now checked, `false`\n         * if the item is now unchecked.\n         */\n        fun onItemCheckedStateChanged(\n            mode: ActionMode,\n            position: Int,\n            id: Long,\n            checked: Boolean,\n        )\n    }\n\n    /**\n     * Custom checked\n     */\n    interface CustomChoiceListener {\n        fun onIntoCustomChoice(view: EasyRecyclerView)\n        fun onOutOfCustomChoice(view: EasyRecyclerView)\n        fun onItemCheckedStateChanged(\n            view: EasyRecyclerView,\n            position: Int,\n            id: Long,\n            checked: Boolean,\n        )\n    }\n\n    /**\n     * This saved state class is a Parcelable and should not extend\n     * [android.view.View.BaseSavedState] nor [android.view.AbsSavedState]\n     * because its super class AbsSavedState's constructor\n     * currently passes null\n     * as a class loader to read its superstate from Parcelable.\n     * This causes [android.os.BadParcelableException] when restoring saved states.\n     *\n     *\n     * The super class \"RecyclerView\" is a part of the support library,\n     * and restoring its saved state requires the class loader that loaded the RecyclerView.\n     * It seems that the class loader is not required when restoring from RecyclerView itself,\n     * but it is required when restoring from RecyclerView's subclasses.\n     */\n    internal open class SavedState : Parcelable {\n        var choiceMode = 0\n        var customChoice = false\n        var checkedItemCount = 0\n        var checkState: SparseBooleanArray? = null\n        var checkIdState: LongSparseArray<Int>? = null\n\n        // This keeps the parent(RecyclerView)'s state\n        var superState: Parcelable?\n\n        constructor() {\n            superState = null\n        }\n\n        /**\n         * Constructor called from [.onSaveInstanceState]\n         */\n        constructor(superState: Parcelable?) {\n            this.superState = if (superState !== EMPTY_STATE) superState else null\n        }\n\n        /**\n         * Constructor called from [.CREATOR]\n         */\n        private constructor(`in`: Parcel) {\n            // Parcel 'in' has its parent(RecyclerView)'s saved state.\n            // To restore it, class loader that loaded RecyclerView is required.\n            val superState =\n                `in`.readParcelableCompat<Parcelable>(RecyclerView::class.java.getClassLoader())\n            this.superState = superState ?: EMPTY_STATE\n            choiceMode = `in`.readInt()\n            customChoice = NumberUtils.int2boolean(`in`.readInt())\n            checkedItemCount = `in`.readInt()\n            checkState = `in`.readSparseBooleanArray()\n            val n = `in`.readInt()\n            if (n > 0) {\n                checkIdState = LongSparseArray()\n                (0 until n).forEach { i ->\n                    val key = `in`.readLong()\n                    val value = `in`.readInt()\n                    checkIdState!!.put(key, value)\n                }\n            }\n        }\n\n        override fun describeContents(): Int = 0\n\n        override fun writeToParcel(out: Parcel, flags: Int) {\n            out.writeParcelable(superState, flags)\n            out.writeInt(choiceMode)\n            out.writeInt(NumberUtils.boolean2int(customChoice))\n            out.writeInt(checkedItemCount)\n            out.writeSparseBooleanArray(checkState)\n            val n = if (checkIdState != null) checkIdState!!.size() else 0\n            out.writeInt(n)\n            for (i in 0 until n) {\n                out.writeLong(checkIdState!!.keyAt(i))\n                out.writeInt(checkIdState!!.valueAt(i))\n            }\n        }\n\n        companion object CREATOR : Parcelable.Creator<SavedState> {\n            override fun createFromParcel(`in`: Parcel): SavedState = SavedState(`in`)\n\n            override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)\n        }\n    }\n\n    inner class MultiChoiceModeWrapper : MultiChoiceModeListener {\n        private val mWrapped: MultiChoiceModeListener? = null\n        fun hasWrappedCallback(): Boolean = mWrapped != null\n\n        override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {\n            if (mWrapped!!.onCreateActionMode(mode, menu)) {\n                // Initialize checked graphic state?\n                isLongClickable = false\n                return true\n            }\n            return false\n        }\n\n        override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = mWrapped!!.onPrepareActionMode(mode, menu)\n\n        override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = mWrapped!!.onActionItemClicked(mode, item)\n\n        override fun onDestroyActionMode(mode: ActionMode) {\n            mWrapped!!.onDestroyActionMode(mode)\n            mChoiceActionMode = null\n\n            // Ending selection mode means deselecting everything.\n            clearChoices()\n            requestLayout()\n            isLongClickable = true\n        }\n\n        override fun onItemCheckedStateChanged(\n            mode: ActionMode,\n            position: Int,\n            id: Long,\n            checked: Boolean,\n        ) {\n            mWrapped!!.onItemCheckedStateChanged(mode, position, id, checked)\n\n            // If there are no items selected we no longer need the selection mode.\n            if (checkedItemCount == 0) {\n                mode.finish()\n            }\n        }\n    }\n\n    companion object {\n        /**\n         * Normal list that does not indicate choices\n         */\n        const val CHOICE_MODE_NONE = 0\n\n        /**\n         * The list allows up to one choice\n         */\n        const val CHOICE_MODE_SINGLE = 1\n\n        /**\n         * The list allows multiple choices\n         */\n        const val CHOICE_MODE_MULTIPLE = 2\n\n        /**\n         * The list allows multiple choices in a modal selection mode\n         */\n        const val CHOICE_MODE_MULTIPLE_MODAL = 3\n\n        /**\n         * The list allows multiple choices in custom action\n         */\n        const val CHOICE_MODE_MULTIPLE_CUSTOM = 4\n\n        private val EMPTY_STATE: SavedState = object : SavedState() {}\n\n        private fun setViewChecked(view: View, checked: Boolean) {\n            if (view is Checkable) {\n                (view as Checkable).isChecked = checked\n            } else {\n                view.isActivated = checked\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/FastScroller.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.drawable.Drawable\nimport android.os.Handler\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewConfiguration\nimport androidx.core.content.withStyledAttributes\nimport androidx.core.graphics.withTranslation\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.RecyclerView.AdapterDataObserver\nimport com.hippo.ehviewer.R\nimport com.hippo.yorozuya.AnimationUtils\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.SimpleAnimatorListener\nimport com.hippo.yorozuya.SimpleHandler\nimport kotlin.math.abs\nimport kotlin.math.max\nimport kotlin.math.min\n\nclass FastScroller @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : View(\n    context,\n    attrs,\n    defStyleAttr,\n) {\n    private var mSimpleHandler: Handler? = null\n    private var mDraggable = false\n    private var mMinHandlerHeight = 0\n    private var mRecyclerView: RecyclerView? = null\n    private var mOnScrollChangeListener: RecyclerView.OnScrollListener? = null\n    private var mAdapter: RecyclerView.Adapter<*>? = null\n    private var mAdapterDataObserver: AdapterDataObserver? = null\n    private var mHandler: Drawable? = null\n    private var mHandlerOffset = INVALID\n    private var mHandlerHeight = INVALID\n    private var mDownX = INVALID.toFloat()\n    private var mDownY = INVALID.toFloat()\n    private var mLastMotionY = INVALID.toFloat()\n    private var mDragged = false\n    private var mCantDrag = false\n    private var mTouchSlop = 0\n    private var mListener: OnDragHandlerListener? = null\n    private var mShowAnimator: ObjectAnimator? = null\n    private var mHideAnimator: ObjectAnimator? = null\n    private val mHideRunnable = Runnable { mHideAnimator!!.start() }\n\n    init {\n        init(context, attrs, defStyleAttr)\n    }\n\n    private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {\n        mSimpleHandler = SimpleHandler.getInstance()\n        context.withStyledAttributes(attrs, R.styleable.FastScroller, defStyleAttr, 0) {\n            mHandler = getDrawable(R.styleable.FastScroller_handler)\n            mDraggable = getBoolean(R.styleable.FastScroller_draggable, true)\n        }\n        setAlpha(0.0f)\n        visibility = INVISIBLE\n        mMinHandlerHeight = LayoutUtils.dp2pix(context, MIN_HANDLER_HEIGHT_DP.toFloat())\n        mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop\n        mShowAnimator = ObjectAnimator.ofFloat(this, \"alpha\", 1.0f)\n        mShowAnimator!!.interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR\n        mShowAnimator!!.setDuration(SCROLL_BAR_FADE_DURATION.toLong())\n        mHideAnimator = ObjectAnimator.ofFloat(this, \"alpha\", 0.0f)\n        mHideAnimator!!.interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR\n        mHideAnimator!!.setDuration(SCROLL_BAR_FADE_DURATION.toLong())\n        mHideAnimator!!.addListener(object : SimpleAnimatorListener() {\n            private var mCancel = false\n            override fun onAnimationCancel(animation: Animator) {\n                mCancel = true\n            }\n\n            override fun onAnimationEnd(animation: Animator) {\n                if (mCancel) {\n                    mCancel = false\n                } else {\n                    visibility = INVISIBLE\n                }\n            }\n        })\n    }\n\n    fun setOnDragHandlerListener(listener: OnDragHandlerListener?) {\n        mListener = listener\n    }\n\n    private fun updatePosition(show: Boolean) {\n        if (mRecyclerView == null) {\n            return\n        }\n        val paddingTop = paddingTop\n        val paddingBottom = paddingBottom\n        val height = height - paddingTop - paddingBottom\n        val offset = mRecyclerView!!.computeVerticalScrollOffset()\n        val extent = mRecyclerView!!.computeVerticalScrollExtent()\n        val range = mRecyclerView!!.computeVerticalScrollRange()\n        if (height <= 0 || extent >= range || extent <= 0) {\n            return\n        }\n        var endOffset = height.toLong() * offset / range\n        var endHeight = height * extent / range\n        endHeight = max(endHeight.toDouble(), mMinHandlerHeight.toDouble()).toInt()\n        endOffset = min(endOffset.toDouble(), (height - endHeight).toDouble()).toLong()\n        mHandlerOffset = (endOffset + paddingTop).toInt()\n        mHandlerHeight = endHeight\n        if (show) {\n            if (mHideAnimator!!.isRunning) {\n                mHideAnimator!!.cancel()\n                mShowAnimator!!.start()\n            } else if (visibility != VISIBLE && !mShowAnimator!!.isRunning) {\n                visibility = VISIBLE\n                mShowAnimator!!.start()\n            }\n            val handler = mSimpleHandler\n            handler!!.removeCallbacks(mHideRunnable)\n            if (!mDragged) {\n                handler.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())\n            }\n        }\n    }\n\n    fun setHandlerDrawable(drawable: Drawable?) {\n        mHandler = drawable\n        invalidate()\n    }\n\n    var isDraggable: Boolean\n        get() = mDraggable\n        set(draggable) {\n            mDraggable = draggable\n            if (mDragged) {\n                mDragged = false\n            }\n            mSimpleHandler!!.removeCallbacks(mHideRunnable)\n            mHideRunnable.run()\n        }\n\n    val isAttached: Boolean\n        get() = mRecyclerView != null\n\n    fun attachToRecyclerView(recyclerView: RecyclerView?) {\n        if (recyclerView == null) {\n            return\n        }\n        check(mRecyclerView == null) {\n            \"The FastScroller is already attached to a RecyclerView, \" +\n                \"call detachedFromRecyclerView first\"\n        }\n        mRecyclerView = recyclerView\n        mOnScrollChangeListener = object : RecyclerView.OnScrollListener() {\n            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {\n                updatePosition(true)\n                invalidate()\n            }\n        }\n        recyclerView.addOnScrollListener(mOnScrollChangeListener!!)\n        mAdapter = recyclerView.adapter\n        if (mAdapter != null) {\n            mAdapterDataObserver = object : AdapterDataObserver() {\n                override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {\n                    super.onItemRangeChanged(positionStart, itemCount)\n                    updatePosition(false)\n                    invalidate()\n                }\n\n                override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {\n                    super.onItemRangeChanged(positionStart, itemCount, payload)\n                    updatePosition(false)\n                    invalidate()\n                }\n\n                override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {\n                    super.onItemRangeInserted(positionStart, itemCount)\n                    updatePosition(false)\n                    invalidate()\n                }\n\n                override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {\n                    super.onItemRangeRemoved(positionStart, itemCount)\n                    updatePosition(false)\n                    invalidate()\n                }\n\n                override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {\n                    super.onItemRangeMoved(fromPosition, toPosition, itemCount)\n                    updatePosition(false)\n                    invalidate()\n                }\n            }\n            mAdapter!!.registerAdapterDataObserver(mAdapterDataObserver!!)\n        }\n    }\n\n    fun detachedFromRecyclerView() {\n        if (mRecyclerView != null && mOnScrollChangeListener != null) {\n            mRecyclerView!!.removeOnScrollListener(mOnScrollChangeListener!!)\n        }\n        mRecyclerView = null\n        mOnScrollChangeListener = null\n        if (mAdapter != null && mAdapterDataObserver != null) {\n            mAdapter!!.unregisterAdapterDataObserver(mAdapterDataObserver!!)\n        }\n        mAdapter = null\n        mAdapterDataObserver = null\n        setAlpha(0.0f)\n        visibility = INVISIBLE\n    }\n\n    override fun onDraw(canvas: Canvas) {\n        if (mRecyclerView == null || mHandler == null) {\n            return\n        }\n        if (mHandlerHeight == INVALID) {\n            updatePosition(false)\n        }\n        if (mHandlerHeight == INVALID) {\n            return\n        }\n        val paddingLeft = getPaddingLeft()\n        canvas.withTranslation(paddingLeft.toFloat(), mHandlerOffset.toFloat()) {\n            mHandler!!.setBounds(0, 0, width - paddingLeft - getPaddingRight(), mHandlerHeight)\n            mHandler!!.draw(canvas)\n        }\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onTouchEvent(event: MotionEvent): Boolean {\n        if (!mDraggable || visibility != VISIBLE || mRecyclerView == null || mHandlerHeight == INVALID) {\n            return false\n        }\n        val action = event.action\n        if (action == MotionEvent.ACTION_DOWN) {\n            mCantDrag = false\n        }\n        if (mCantDrag) {\n            return false\n        }\n        when (action) {\n            MotionEvent.ACTION_DOWN -> {\n                mDragged = false\n                mDownX = event.x\n                mDownY = event.y\n                if (mDownY < mHandlerOffset || mDownY > mHandlerOffset + mHandlerHeight) {\n                    mCantDrag = true\n                    return false\n                }\n            }\n            MotionEvent.ACTION_MOVE -> {\n                if (!mDragged) {\n                    val x = event.x\n                    val y = event.y\n                    // Check touch slop\n                    if (MathUtils.dist(x, y, mDownX, mDownY) < mTouchSlop) {\n                        return true\n                    }\n                    if (abs((x - mDownX).toDouble()) > abs((y - mDownY).toDouble()) || y < mHandlerOffset || y > mHandlerOffset + mHandlerHeight) {\n                        mCantDrag = true\n                        return false\n                    } else {\n                        mDragged = true\n                        mSimpleHandler!!.removeCallbacks(mHideRunnable)\n                        // Update mLastMotionY\n                        mLastMotionY =\n                            if (mDownY < mHandlerOffset || mDownY >= mHandlerOffset + mHandlerHeight) {\n                                // the point out of handler, make the point in handler center\n                                mHandlerOffset + mHandlerHeight.toFloat() / 2\n                            } else {\n                                mDownY\n                            }\n                        // Notify\n                        if (mListener != null) {\n                            mListener!!.onStartDragHandler()\n                        }\n                    }\n                }\n                val range = mRecyclerView!!.computeVerticalScrollRange()\n                if (range <= 0) {\n                    return true\n                }\n                val y = event.y\n                val scroll =\n                    (range * (y - mLastMotionY) / (height - paddingTop - paddingBottom)).toInt()\n                mRecyclerView!!.scrollBy(0, scroll)\n                mLastMotionY = y\n            }\n            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {\n                // Notify\n                if (mDragged && mListener != null) {\n                    mListener!!.onEndDragHandler()\n                }\n                mDragged = false\n                mSimpleHandler!!.postDelayed(mHideRunnable, SCROLL_BAR_DELAY.toLong())\n            }\n        }\n        return true\n    }\n\n    interface OnDragHandlerListener {\n        fun onStartDragHandler()\n        fun onEndDragHandler()\n    }\n\n    companion object {\n        private const val INVALID = -1\n        private const val SCROLL_BAR_FADE_DURATION = 500\n        private const val SCROLL_BAR_DELAY = 1500\n        private const val MIN_HANDLER_HEIGHT_DP = 48\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/HandlerDrawable.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.ColorFilter\nimport android.graphics.Paint\nimport android.graphics.PixelFormat\nimport android.graphics.RectF\nimport android.graphics.drawable.Drawable\n\nclass HandlerDrawable : Drawable() {\n    private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)\n    private val mTemp = RectF()\n    private var mColor = Color.BLACK\n\n    init {\n        mPaint.setColor(mColor)\n        mPaint.style = Paint.Style.FILL\n    }\n\n    override fun draw(canvas: Canvas) {\n        val width = getBounds().width()\n        val height = getBounds().height()\n        if (width > height) {\n            canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), mPaint)\n        } else {\n            mTemp[0f, 0f, width.toFloat()] = width.toFloat()\n            canvas.drawArc(mTemp, -180f, 180f, true, mPaint)\n            mTemp[0f, (height - width).toFloat(), width.toFloat()] = height.toFloat()\n            canvas.drawArc(mTemp, 0f, 180f, true, mPaint)\n            val halfWidth = width.toFloat() / 2.0f\n            canvas.drawRect(0f, halfWidth, width.toFloat(), height - halfWidth, mPaint)\n        }\n    }\n\n    fun setColor(color: Int) {\n        if (mColor != color) {\n            mColor = color\n            mPaint.setColor(color)\n        }\n    }\n\n    override fun setAlpha(alpha: Int) {\n        mPaint.setAlpha(alpha)\n    }\n\n    override fun setColorFilter(colorFilter: ColorFilter?) {\n        mPaint.setColorFilter(colorFilter)\n    }\n\n    @Deprecated(\"Deprecated in Java\")\n    override fun getOpacity(): Int {\n        val alpha = Color.alpha(mColor)\n        return when (alpha) {\n            0xff -> {\n                PixelFormat.OPAQUE\n            }\n            0x00 -> {\n                PixelFormat.TRANSPARENT\n            }\n            else -> {\n                PixelFormat.TRANSLUCENT\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/LayoutManagerUtils.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.content.Context\nimport android.graphics.PointF\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.SimpleHandler\nimport java.lang.reflect.InvocationTargetException\nimport java.lang.reflect.Method\nimport kotlin.math.abs\n\nobject LayoutManagerUtils {\n    private var sCsdfp: Method? = null\n\n    init {\n        try {\n            sCsdfp = StaggeredGridLayoutManager::class.java.getDeclaredMethod(\n                \"calculateScrollDirectionForPosition\",\n                Int::class.javaPrimitiveType,\n            )\n            sCsdfp!!.isAccessible = true\n        } catch (e: NoSuchMethodException) {\n            // Ignore\n            e.printStackTrace()\n        }\n    }\n\n    fun scrollToPositionWithOffset(\n        layoutManager: RecyclerView.LayoutManager,\n        position: Int,\n        offset: Int,\n    ) {\n        when (layoutManager) {\n            is LinearLayoutManager -> {\n                layoutManager.scrollToPositionWithOffset(position, offset)\n            }\n            is StaggeredGridLayoutManager -> {\n                layoutManager.scrollToPositionWithOffset(position, offset)\n            }\n            else -> {\n                throw IllegalStateException(\n                    \"Can't do scrollToPositionWithOffset for \" +\n                        layoutManager.javaClass.getName(),\n                )\n            }\n        }\n    }\n\n    fun smoothScrollToPosition(\n        layoutManager: RecyclerView.LayoutManager,\n        context: Context?,\n        position: Int,\n        millisecondsPerInch: Int = -1,\n    ) {\n        val smoothScroller: SimpleSmoothScroller\n        when (layoutManager) {\n            is LinearLayoutManager -> {\n                smoothScroller =\n                    object : SimpleSmoothScroller(context!!, millisecondsPerInch.toFloat()) {\n                        override fun computeScrollVectorForPosition(targetPosition: Int): PointF? = layoutManager.computeScrollVectorForPosition(targetPosition)\n                    }\n            }\n            is StaggeredGridLayoutManager -> {\n                smoothScroller =\n                    object : SimpleSmoothScroller(context!!, millisecondsPerInch.toFloat()) {\n                        override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {\n                            var direction = 0\n                            try {\n                                direction = sCsdfp!!.invoke(layoutManager, targetPosition) as Int\n                            } catch (e: IllegalAccessException) {\n                                e.printStackTrace()\n                            } catch (e: InvocationTargetException) {\n                                e.printStackTrace()\n                            }\n                            if (direction == 0) {\n                                return null\n                            }\n                            return if (layoutManager.orientation == StaggeredGridLayoutManager.HORIZONTAL) {\n                                PointF(direction.toFloat(), 0f)\n                            } else {\n                                PointF(0f, direction.toFloat())\n                            }\n                        }\n                    }\n            }\n            else -> {\n                throw IllegalStateException(\n                    \"Can't do smoothScrollToPosition for \" +\n                        layoutManager.javaClass.getName(),\n                )\n            }\n        }\n        smoothScroller.targetPosition = position\n        layoutManager.startSmoothScroll(smoothScroller)\n    }\n\n    fun scrollToPositionProperly(\n        layoutManager: RecyclerView.LayoutManager,\n        context: Context?,\n        position: Int,\n        listener: OnScrollToPositionListener?,\n    ) {\n        SimpleHandler.getInstance().postDelayed({\n            val first = getFirstVisibleItemPosition(layoutManager)\n            val last = getLastVisibleItemPosition(layoutManager)\n            val offset = abs((position - first).toDouble()).toInt()\n            val max = last - first\n            if (offset < max && max > 0) {\n                smoothScrollToPosition(\n                    layoutManager,\n                    context,\n                    position,\n                    MathUtils.lerp(100, 25, (offset / max).toFloat()),\n                )\n            } else {\n                scrollToPositionWithOffset(layoutManager, position, 0)\n                listener?.onScrollToPosition(position)\n            }\n        }, 200)\n    }\n\n    fun getFirstVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int = when (layoutManager) {\n        is LinearLayoutManager -> {\n            layoutManager.findFirstVisibleItemPosition()\n        }\n        is StaggeredGridLayoutManager -> {\n            val positions =\n                layoutManager.findFirstVisibleItemPositions(null)\n            MathUtils.min(*positions)\n        }\n        else -> {\n            throw IllegalStateException(\n                \"Can't do getFirstVisibleItemPosition for \" +\n                    layoutManager.javaClass.getName(),\n            )\n        }\n    }\n\n    fun getLastVisibleItemPosition(layoutManager: RecyclerView.LayoutManager): Int = when (layoutManager) {\n        is LinearLayoutManager -> {\n            layoutManager.findLastVisibleItemPosition()\n        }\n        is StaggeredGridLayoutManager -> {\n            val positions =\n                layoutManager.findLastVisibleItemPositions(null)\n            MathUtils.max(*positions)\n        }\n        else -> {\n            throw IllegalStateException(\n                \"Can't do getLastVisibleItemPosition for \" +\n                    layoutManager.javaClass.getName(),\n            )\n        }\n    }\n\n    fun interface OnScrollToPositionListener {\n        fun onScrollToPosition(position: Int)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/LinearDividerItemDecoration.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.Rect\nimport android.view.View\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.RecyclerView.ItemDecoration\n\n/**\n * Only work for [androidx.recyclerview.widget.LinearLayoutManager].\n * Show divider between item, just like\n * [android.widget.ListView.setDivider]\n */\nclass LinearDividerItemDecoration(orientation: Int, color: Int, thickness: Int) : ItemDecoration() {\n    private val mRect: Rect = Rect()\n    private val mPaint: Paint = Paint()\n    private var mShowFirstDivider = false\n    private var mShowLastDivider = false\n    private var mOrientation = 0\n    private var mThickness = 0\n    private var mPaddingStart = 0\n    private var mPaddingEnd = 0\n    private var mOverlap = false\n    private var mShowDividerHelper: ShowDividerHelper? = null\n\n    init {\n        mPaint.style = Paint.Style.FILL\n        setOrientation(orientation)\n        setColor(color)\n        setThickness(thickness)\n    }\n\n    fun setShowDividerHelper(showDividerHelper: ShowDividerHelper?) {\n        mShowDividerHelper = showDividerHelper\n    }\n\n    fun setOrientation(orientation: Int) {\n        require(!(orientation != HORIZONTAL && orientation != VERTICAL)) { \"invalid orientation\" }\n        mOrientation = orientation\n    }\n\n    fun setColor(color: Int) {\n        mPaint.setColor(color)\n    }\n\n    fun setThickness(thickness: Int) {\n        mThickness = thickness\n    }\n\n    fun setShowFirstDivider(showFirstDivider: Boolean) {\n        mShowFirstDivider = showFirstDivider\n    }\n\n    fun setShowLastDivider(showLastDivider: Boolean) {\n        mShowLastDivider = showLastDivider\n    }\n\n    fun setPadding(padding: Int) {\n        setPaddingStart(padding)\n        setPaddingEnd(padding)\n    }\n\n    fun setPaddingStart(paddingStart: Int) {\n        mPaddingStart = paddingStart\n    }\n\n    fun setPaddingEnd(paddingEnd: Int) {\n        mPaddingEnd = paddingEnd\n    }\n\n    fun setOverlap(overlap: Boolean) {\n        mOverlap = overlap\n    }\n\n    override fun getItemOffsets(\n        outRect: Rect,\n        view: View,\n        parent: RecyclerView,\n        state: RecyclerView.State,\n    ) {\n        if (parent.adapter == null) {\n            outRect[0, 0, 0] = 0\n            return\n        }\n        if (mOverlap) {\n            outRect[0, 0, 0] = 0\n            return\n        }\n        val position = parent.getChildLayoutPosition(view)\n        val itemCount = parent.adapter!!.itemCount\n        if (mShowDividerHelper != null) {\n            if (mOrientation == VERTICAL) {\n                if (position == 0 && mShowDividerHelper!!.showDivider(0)) {\n                    outRect.top = mThickness\n                }\n                if (mShowDividerHelper!!.showDivider(position + 1)) {\n                    outRect.bottom = mThickness\n                }\n            } else {\n                if (position == 0 && mShowDividerHelper!!.showDivider(0)) {\n                    outRect.left = mThickness\n                }\n                if (mShowDividerHelper!!.showDivider(position + 1)) {\n                    outRect.right = mThickness\n                }\n            }\n        } else {\n            if (mOrientation == VERTICAL) {\n                if (position == 0 && mShowFirstDivider) {\n                    outRect.top = mThickness\n                }\n                outRect.bottom = mThickness\n                if (position == itemCount - 1 && !mShowLastDivider) {\n                    outRect.bottom = 0\n                }\n            } else {\n                if (position == 0 && mShowFirstDivider) {\n                    outRect.left = mThickness\n                }\n                outRect.right = mThickness\n                if (position == itemCount - 1 && !mShowLastDivider) {\n                    outRect.right = 0\n                }\n            }\n        }\n    }\n\n    override fun onDrawOver(\n        c: Canvas,\n        parent: RecyclerView,\n        state: RecyclerView.State,\n    ) {\n        val adapter = parent.adapter ?: return\n        val itemCount = adapter.itemCount\n        val overlap = mOverlap\n        if (mOrientation == VERTICAL) {\n            val isRtl = parent.layoutDirection == View.LAYOUT_DIRECTION_RTL\n            val mPaddingLeft: Int\n            val mPaddingRight: Int\n            if (isRtl) {\n                mPaddingLeft = mPaddingEnd\n                mPaddingRight = mPaddingStart\n            } else {\n                mPaddingLeft = mPaddingStart\n                mPaddingRight = mPaddingEnd\n            }\n            val left = parent.getPaddingLeft() + mPaddingLeft\n            val right = parent.width - parent.getPaddingRight() - mPaddingRight\n            val childCount = parent.childCount\n            for (i in 0 until childCount) {\n                val child = parent.getChildAt(i)\n                val lp = child.layoutParams as RecyclerView.LayoutParams\n                val position = parent.getChildLayoutPosition(child)\n                var show: Boolean\n                show = if (mShowDividerHelper != null) {\n                    mShowDividerHelper!!.showDivider(position + 1)\n                } else {\n                    position != itemCount - 1 || mShowLastDivider\n                }\n                if (show) {\n                    var top = child.bottom + lp.bottomMargin\n                    if (overlap) {\n                        top -= mThickness\n                    }\n                    val bottom = top + mThickness\n                    mRect[left, top, right] = bottom\n                    c.drawRect(mRect, mPaint)\n                }\n                if (position == 0) {\n                    show = if (mShowDividerHelper != null) {\n                        mShowDividerHelper!!.showDivider(0)\n                    } else {\n                        mShowFirstDivider\n                    }\n                    if (show) {\n                        var bottom = child.top + lp.topMargin\n                        if (overlap) {\n                            bottom += mThickness\n                        }\n                        val top = bottom - mThickness\n                        mRect[left, top, right] = bottom\n                        c.drawRect(mRect, mPaint)\n                    }\n                }\n            }\n        } else {\n            val top = parent.paddingTop + mPaddingStart\n            val bottom = parent.height - parent.paddingBottom - mPaddingEnd\n            val childCount = parent.childCount\n            for (i in 0 until childCount) {\n                val child = parent.getChildAt(i)\n                val lp = child.layoutParams as RecyclerView.LayoutParams\n                val position = parent.getChildLayoutPosition(child)\n                var show: Boolean\n                show = if (mShowDividerHelper != null) {\n                    mShowDividerHelper!!.showDivider(position + 1)\n                } else {\n                    position != itemCount - 1 || mShowLastDivider\n                }\n                if (show) {\n                    var left = child.right + lp.rightMargin\n                    if (overlap) {\n                        left -= mThickness\n                    }\n                    val right = left + mThickness\n                    mRect[left, top, right] = bottom\n                    c.drawRect(mRect, mPaint)\n                }\n                if (position == 0) {\n                    show = if (mShowDividerHelper != null) {\n                        mShowDividerHelper!!.showDivider(0)\n                    } else {\n                        mShowFirstDivider\n                    }\n                    if (show) {\n                        var right = child.left + lp.leftMargin\n                        if (overlap) {\n                            right += mThickness\n                        }\n                        val left = right - mThickness\n                        mRect[left, top, right] = bottom\n                        c.drawRect(mRect, mPaint)\n                    }\n                }\n            }\n        }\n    }\n\n    interface ShowDividerHelper {\n        fun showDivider(index: Int): Boolean\n    }\n\n    companion object {\n        const val HORIZONTAL = LinearLayoutManager.HORIZONTAL\n        const val VERTICAL = LinearLayoutManager.VERTICAL\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/MarginItemDecoration.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.graphics.Rect\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.RecyclerView.ItemDecoration\n\n/**\n * @param margin        gap between two item\n * @param paddingLeft   gap between RecyclerView left and left item left\n * @param paddingTop    gap between RecyclerView top and top item top\n * @param paddingRight  gap between RecyclerView right and right item right\n * @param paddingBottom gap between RecyclerView bottom and bottom item bottom\n */\nclass MarginItemDecoration(\n    margin: Int,\n    paddingLeft: Int,\n    paddingTop: Int,\n    paddingRight: Int,\n    paddingBottom: Int,\n) : ItemDecoration() {\n    private var mMargin = 0\n    private var mPaddingLeft = 0\n    private var mPaddingTop = 0\n    private var mPaddingRight = 0\n    private var mPaddingBottom = 0\n\n    init {\n        val halfMargin = margin / 2\n        mMargin = halfMargin\n        mPaddingLeft = paddingLeft - halfMargin\n        mPaddingTop = paddingTop - halfMargin\n        mPaddingRight = paddingRight - halfMargin\n        mPaddingBottom = paddingBottom - halfMargin\n    }\n\n    override fun getItemOffsets(\n        outRect: Rect,\n        view: View,\n        parent: RecyclerView,\n        state: RecyclerView.State,\n    ) {\n        outRect[mMargin, mMargin, mMargin] = mMargin\n    }\n\n    fun applyPaddings(view: View) {\n        view.setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/SimpleHolder.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.view.View\nimport androidx.recyclerview.widget.RecyclerView\n\nclass SimpleHolder(itemView: View) : RecyclerView.ViewHolder(itemView)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/easyrecyclerview/SimpleSmoothScroller.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.easyrecyclerview\n\nimport android.content.Context\nimport androidx.recyclerview.widget.LinearSmoothScroller\nimport kotlin.math.abs\nimport kotlin.math.ceil\n\nabstract class SimpleSmoothScroller(context: Context, millisecondsPerInch: Float) : LinearSmoothScroller(context) {\n    private val mMillisecondsPerPx: Float = millisecondsPerInch / context.resources.displayMetrics.densityDpi\n\n    override fun calculateTimeForScrolling(dx: Int): Int = if (mMillisecondsPerPx <= 0) {\n        super.calculateTimeForScrolling(dx)\n    } else {\n        ceil(abs(dx.toDouble()) * mMillisecondsPerPx).toInt()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/AppConfig.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.ehviewer;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.Environment;\n\nimport androidx.annotation.Nullable;\n\nimport com.hippo.ehviewer.client.exception.ParseException;\nimport com.hippo.util.ReadableTime;\nimport com.hippo.yorozuya.FileUtils;\nimport com.hippo.yorozuya.IOUtils;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.nio.charset.StandardCharsets;\n\npublic class AppConfig {\n    public static final String APP_DIRNAME = \"EhViewer\";\n\n    private static final String DOWNLOAD = \"download\";\n    private static final String TEMP = \"temp\";\n    private static final String PARSE_ERROR = \"parse_error\";\n    private static final String CRASH = \"crash\";\n\n    @SuppressLint(\"StaticFieldLeak\")\n    private static Context sContext;\n\n    public static void initialize(Context context) {\n        sContext = context.getApplicationContext();\n    }\n\n    @Nullable\n    public static File getExternalAppDir() {\n        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {\n            File dir = sContext.getExternalFilesDir(null);\n            return FileUtils.ensureDirectory(dir) ? dir : null;\n        }\n        return null;\n    }\n\n    /**\n     * mkdirs and get\n     */\n    @Nullable\n    public static File getDirInExternalAppDir(String filename) {\n        File appFolder = getExternalAppDir();\n        if (appFolder != null) {\n            File dir = new File(appFolder, filename);\n            return FileUtils.ensureDirectory(dir) ? dir : null;\n        }\n        return null;\n    }\n\n    @Nullable\n    public static File getFileInExternalAppDir(String filename) {\n        File appFolder = getExternalAppDir();\n        if (appFolder != null) {\n            File file = new File(appFolder, filename);\n            return FileUtils.ensureFile(file) ? file : null;\n        }\n        return null;\n    }\n\n    @Nullable\n    public static File getDefaultDownloadDir() {\n        return getDirInExternalAppDir(DOWNLOAD);\n    }\n\n    @Nullable\n    public static File getExternalTempDir() {\n        File dir = sContext.getExternalCacheDir();\n        File file;\n        if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {\n            return file;\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    public static File getExternalCopyTempDir() {\n        File dir = sContext.getExternalCacheDir();\n        File file;\n        if (null != dir && FileUtils.ensureDirectory(file = new File(dir, \"copy\"))) {\n            return file;\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    public static File getExternalParseErrorDir() {\n        return getDirInExternalAppDir(PARSE_ERROR);\n    }\n\n    @Nullable\n    public static File getExternalCrashDir() {\n        return getDirInExternalAppDir(CRASH);\n    }\n\n    @Nullable\n    public static File getTempDir() {\n        File dir = sContext.getCacheDir();\n        File file;\n        if (null != dir && FileUtils.ensureDirectory(file = new File(dir, TEMP))) {\n            return file;\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    public static File createTempFile() {\n        return FileUtils.createTempFile(getTempDir(), null);\n    }\n\n    public static void saveParseErrorBody(ParseException e, String body) {\n        File dir = getExternalParseErrorDir();\n        if (null == dir) {\n            return;\n        }\n\n        File file = new File(dir, ReadableTime.INSTANCE.getFilenamableTime() + \".txt\");\n        OutputStream os = null;\n        try {\n            os = new FileOutputStream(file);\n            String message = e.getMessage();\n            if (null != message) {\n                os.write(message.getBytes(StandardCharsets.UTF_8));\n                os.write('\\n');\n            }\n            if (null != body) {\n                os.write(body.getBytes(StandardCharsets.UTF_8));\n            }\n            os.flush();\n        } catch (IOException e1) {\n            // Ignore\n        } finally {\n            IOUtils.closeQuietly(os);\n        }\n    }\n\n    @Nullable\n    public static File getFilesDir(String name) {\n        File dir = sContext.getFilesDir();\n        if (dir == null) {\n            return null;\n        }\n\n        dir = new File(dir, name);\n        if (dir.isDirectory() || dir.mkdirs()) {\n            return dir;\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/Crash.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer\n\nimport android.os.Build\nimport android.os.Debug\nimport com.hippo.util.ReadableTime\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.OSUtils\nimport java.io.File\nimport java.io.FileWriter\nimport java.io.PrintWriter\n\nprivate fun joinIfStringArray(any: Any?): String = if (any is Array<*>) any.joinToString() else any.toString()\n\nprivate fun collectClassStaticInfo(clazz: Class<*>): String = clazz.declaredFields.joinToString(\"\\n\") {\n    \"${it.name}=${joinIfStringArray(it.get(null))}\"\n}\n\nobject Crash {\n    private fun collectInfo(fw: FileWriter) {\n        fw.write(\"======== PackageInfo ========\\n\")\n        fw.write(\"PackageName=${BuildConfig.APPLICATION_ID}\\n\")\n        fw.write(\"VersionName=${BuildConfig.VERSION_NAME}\\n\")\n        fw.write(\"VersionCode=${BuildConfig.VERSION_CODE}\\n\")\n        fw.write(\"CommitSha=${BuildConfig.COMMIT_SHA}\\n\")\n        fw.write(\"BuildTime=${BuildConfig.BUILD_TIME}\\n\")\n        fw.write(\"\\n\")\n\n        // Runtime\n        fw.write(\"======== Runtime ========\\n\")\n        fw.write(\"TopActivity=${EhApplication.application.topActivity?.javaClass?.name}\\n\")\n        fw.write(\"\\n\")\n\n        // Device info\n        fw.write(\"======== DeviceInfo ========\\n\")\n        fw.write(\"${collectClassStaticInfo(Build::class.java)}\\n\")\n        fw.write(\"${collectClassStaticInfo(Build.VERSION::class.java)}\\n\")\n        fw.write(\"MEMORY=\")\n        fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppAllocatedMemory(), false))\n        fw.write(\"\\n\")\n        fw.write(\"MEMORY_NATIVE=\")\n        fw.write(FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false))\n        fw.write(\"\\n\")\n        fw.write(\"MEMORY_MAX=\")\n        fw.write(FileUtils.humanReadableByteCount(OSUtils.getAppMaxMemory(), false))\n        fw.write(\"\\n\")\n        fw.write(\"MEMORY_TOTAL=\")\n        fw.write(FileUtils.humanReadableByteCount(OSUtils.getTotalMemory(), false))\n        fw.write(\"\\n\")\n        fw.write(\"\\n\")\n    }\n\n    private fun getThrowableInfo(t: Throwable, fw: FileWriter) {\n        val printWriter = PrintWriter(fw)\n        t.printStackTrace(printWriter)\n        var cause = t.cause\n        while (cause != null) {\n            cause.printStackTrace(printWriter)\n            cause = cause.cause\n        }\n    }\n\n    fun saveCrashLog(t: Throwable) {\n        val dir = AppConfig.getExternalCrashDir() ?: return\n        val nowString = ReadableTime.getFilenamableTime()\n        val fileName = \"crash-$nowString.log\"\n        val file = File(dir, fileName)\n        runCatching {\n            FileWriter(file).use { fw ->\n                fw.write(\"TIME=${nowString}\\n\")\n                fw.write(\"\\n\")\n                collectInfo(fw)\n                fw.write(\"======== CrashInfo ========\\n\")\n                getThrowableInfo(t, fw)\n                fw.write(\"\\n\")\n                fw.flush()\n            }\n        }.onFailure {\n            it.printStackTrace()\n            file.delete()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/EhApplication.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer\n\nimport android.app.Activity\nimport android.content.Context\nimport android.os.StrictMode\nimport android.text.method.LinkMovementMethod\nimport android.view.View\nimport android.widget.TextView\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.collection.LruCache\nimport coil3.ImageLoader\nimport coil3.SingletonImageLoader\nimport coil3.disk.DiskCache\nimport coil3.gif.AnimatedImageDecoder\nimport coil3.gif.GifDecoder\nimport coil3.network.ConnectivityChecker\nimport coil3.network.NetworkFetcher\nimport coil3.network.okhttp.asNetworkClient\nimport coil3.request.crossfade\nimport coil3.serviceLoaderEnabled\nimport coil3.util.DebugLogger\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhEngine\nimport com.hippo.ehviewer.client.EhTagDatabase\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.coil.DownloadThumbInterceptor\nimport com.hippo.ehviewer.coil.MergeInterceptor\nimport com.hippo.ehviewer.dao.buildMainDB\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.ehviewer.ui.EhActivity\nimport com.hippo.ehviewer.ui.keepNoMediaFileStatus\nimport com.hippo.ehviewer.widget.SearchDatabase\nimport com.hippo.scene.SceneApplication\nimport com.hippo.util.ReadableTime\nimport com.hippo.util.isAtLeastP\nimport com.hippo.util.launchIO\nimport com.hippo.util.loadHtml\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.IntIdGenerator\nimport eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor\nimport java.security.cert.CertificateFactory\nimport java.security.cert.X509Certificate\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport okhttp3.Cache\nimport okhttp3.OkHttpClient\nimport okhttp3.tls.HandshakeCertificates\nimport okio.FileSystem\nimport okio.Path.Companion.toOkioPath\n\nclass EhApplication :\n    SceneApplication(),\n    SingletonImageLoader.Factory {\n    private val mIdGenerator = IntIdGenerator()\n    private val mGlobalStuffMap = HashMap<Int, Any>()\n    private val mActivityList = ArrayList<Activity>()\n    val topActivity: EhActivity?\n        get() = if (mActivityList.isNotEmpty()) {\n            mActivityList[mActivityList.size - 1] as EhActivity\n        } else {\n            null\n        }\n\n    fun recreateAllActivity() {\n        mActivityList.forEach { it.recreate() }\n    }\n\n    @OptIn(DelicateCoroutinesApi::class)\n    override fun onCreate() {\n        application = this\n        val handler = Thread.getDefaultUncaughtExceptionHandler()\n        Thread.setDefaultUncaughtExceptionHandler { t, e ->\n            try {\n                if (Settings.saveCrashLog) {\n                    Crash.saveCrashLog(e)\n                }\n            } catch (_: Throwable) {\n            }\n            handler?.uncaughtException(t, e)\n        }\n        super.onCreate()\n        System.loadLibrary(\"ehviewer\")\n        Settings.initialize()\n        ReadableTime.initialize(this)\n        AppConfig.initialize(this)\n        AppCompatDelegate.setDefaultNightMode(Settings.theme)\n\n        launchIO {\n            launchIO {\n                nonCacheOkHttpClient\n            }\n            launchIO {\n                EhTagDatabase.read()\n            }\n            launchIO {\n                ehDatabase\n            }\n            launchIO {\n                DownloadManager.isIdle\n            }\n            launchIO {\n                cleanupDownload()\n            }\n            launchIO {\n                theDawnOfNewDay()\n            }\n        }\n        mIdGenerator.setNextId(Settings.getInt(KEY_GLOBAL_STUFF_NEXT_ID, 0))\n        if (BuildConfig.DEBUG) {\n            StrictMode.enableDefaults()\n        }\n    }\n\n    private suspend fun cleanupDownload() {\n        runCatching {\n            keepNoMediaFileStatus()\n        }.onFailure {\n            it.printStackTrace()\n        }\n        runCatching {\n            clearTempDir()\n        }.onFailure {\n            it.printStackTrace()\n        }\n    }\n\n    private suspend fun theDawnOfNewDay() {\n        runCatching {\n            if (Settings.requestNews && EhCookieStore.hasSignedIn()) {\n                EhEngine.getNews(true)?.let { showEventPane(it) }\n            }\n        }.onFailure {\n            it.printStackTrace()\n        }\n    }\n\n    fun showEventPane(html: String) {\n        if (Settings.hideHvEvents && html.contains(\"You have encountered a monster!\")) {\n            return\n        }\n        val activity = topActivity\n        activity?.runOnUiThread {\n            val dialog = AlertDialog.Builder(activity)\n                .setMessage(loadHtml(html))\n                .setPositiveButton(android.R.string.ok, null)\n                .create()\n            dialog.setOnShowListener {\n                val messageView = dialog.findViewById<View>(android.R.id.message)\n                if (messageView is TextView) {\n                    messageView.movementMethod = LinkMovementMethod.getInstance()\n                }\n            }\n            try {\n                dialog.show()\n            } catch (_: Throwable) {\n                // ignore\n            }\n        }\n    }\n\n    private fun clearTempDir() {\n        var dir = AppConfig.getTempDir()\n        if (null != dir) {\n            FileUtils.deleteContent(dir)\n        }\n        dir = AppConfig.getExternalTempDir()\n        if (null != dir) {\n            FileUtils.deleteContent(dir)\n        }\n    }\n\n    fun putGlobalStuff(o: Any): Int {\n        val id = mIdGenerator.nextId()\n        mGlobalStuffMap[id] = o\n        Settings.putInt(KEY_GLOBAL_STUFF_NEXT_ID, mIdGenerator.nextId())\n        return id\n    }\n\n    fun containGlobalStuff(id: Int): Boolean = mGlobalStuffMap.containsKey(id)\n\n    fun removeGlobalStuff(id: Int): Any? = mGlobalStuffMap.remove(id)\n\n    fun removeGlobalStuff(o: Any) {\n        mGlobalStuffMap.values.removeAll(setOf(o))\n    }\n\n    fun registerActivity(activity: Activity) {\n        mActivityList.add(activity)\n    }\n\n    fun unregisterActivity(activity: Activity) {\n        mActivityList.remove(activity)\n    }\n\n    override fun newImageLoader(context: Context) = ImageLoader.Builder(context).apply {\n        serviceLoaderEnabled(false)\n        components {\n            if (isAtLeastP) {\n                add(AnimatedImageDecoder.Factory(false))\n            } else {\n                add(GifDecoder.Factory())\n            }\n            add(\n                NetworkFetcher.Factory(\n                    networkClient = { coilOkHttpClient.asNetworkClient() },\n                    connectivityChecker = { ConnectivityChecker.ONLINE },\n                ),\n            )\n            add(MergeInterceptor)\n            add(DownloadThumbInterceptor)\n        }\n        crossfade(300)\n        diskCache(thumbCache)\n        if (BuildConfig.DEBUG) logger(DebugLogger())\n    }.build()\n\n    companion object {\n        private const val KEY_GLOBAL_STUFF_NEXT_ID = \"global_stuff_next_id\"\n\n        lateinit var application: EhApplication\n            private set\n\n        val cacheDir by lazy { application.cacheDir.toOkioPath() }\n\n        val ehProxySelector by lazy { EhProxySelector() }\n\n        val nonCacheOkHttpClient by lazy {\n            val cf = CertificateFactory.getInstance(\"X.509\")\n            val cert = application.resources.openRawResource(R.raw.isrgrootx1).use {\n                cf.generateCertificates(it).first() as X509Certificate\n            }\n            val certs = HandshakeCertificates.Builder()\n                .addPlatformTrustedCertificates()\n                .addTrustedCertificate(cert)\n                .build()\n            OkHttpClient.Builder().apply {\n                cookieJar(EhCookieStore)\n                proxySelector(ehProxySelector)\n                sslSocketFactory(certs.sslSocketFactory(), certs.trustManager)\n                addInterceptor(CloudflareInterceptor(application))\n            }.build()\n        }\n\n        val noRedirectOkHttpClient by lazy {\n            nonCacheOkHttpClient.newBuilder()\n                .followRedirects(false)\n                .build()\n        }\n\n        val coilOkHttpClient by lazy {\n            nonCacheOkHttpClient.newBuilder()\n                .addInterceptor { chain ->\n                    val request = chain.request()\n                    val newRequest = request.newBuilder()\n                        .header(\"User-Agent\", Settings.userAgent!!)\n                        .build()\n                    chain.proceed(newRequest)\n                }\n                .build()\n        }\n\n        // Never use this okhttp client to download large blobs!!!\n        val okHttpClient by lazy {\n            nonCacheOkHttpClient.newBuilder()\n                .cache(Cache(FileSystem.SYSTEM, cacheDir / \"http_cache\", 20 * 1024 * 1024))\n                .build()\n        }\n\n        val galleryDetailCache by lazy {\n            LruCache<Long, GalleryDetail>(25).also {\n                favouriteStatusRouter.addListener { gid, slot ->\n                    it[gid]?.favoriteSlot = slot\n                }\n            }\n        }\n\n        val favouriteStatusRouter by lazy { FavouriteStatusRouter() }\n\n        val ehDatabase by lazy { buildMainDB(application) }\n\n        val searchDatabase by lazy { SearchDatabase.getInstance(application)!! }\n\n        val thumbCache by lazy {\n            DiskCache.Builder()\n                .directory(cacheDir / \"thumb\")\n                .maxSizeBytes((Settings.readCacheSize / 5).coerceIn(64, 1024).toLong() * 1024 * 1024)\n                .build()\n        }\n\n        val imageCache by lazy {\n            DiskCache.Builder()\n                .directory(cacheDir / \"image\")\n                .maxSizeBytes((Settings.readCacheSize / 5 * 4).coerceIn(256, 4096).toLong() * 1024 * 1024)\n                .build()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/EhDB.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer\n\nimport android.content.Context\nimport android.net.Uri\nimport androidx.paging.PagingSource\nimport androidx.room.Room.databaseBuilder\nimport com.hippo.ehviewer.EhApplication.Companion.ehDatabase\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.dao.BasicDao\nimport com.hippo.ehviewer.dao.DownloadDirname\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.dao.DownloadLabel\nimport com.hippo.ehviewer.dao.EhDatabase\nimport com.hippo.ehviewer.dao.Filter\nimport com.hippo.ehviewer.dao.HistoryInfo\nimport com.hippo.ehviewer.dao.LocalFavoriteInfo\nimport com.hippo.ehviewer.dao.QuickSearch\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.sendTo\n\nobject EhDB {\n    private val db = ehDatabase\n\n    // Fix state\n    @get:Synchronized\n    val allDownloadInfo: List<DownloadInfo>\n        get() = db.downloadsDao().list().onEach {\n            if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {\n                it.state = DownloadInfo.STATE_NONE\n            }\n        }\n\n    @Synchronized\n    fun updateDownloadInfo(downloadInfos: List<DownloadInfo>) {\n        val dao = db.downloadsDao()\n        dao.update(downloadInfos)\n    }\n\n    @Synchronized\n    fun putDownloadInfo(downloadInfo: DownloadInfo) {\n        db.downloadsDao().run {\n            if (load(downloadInfo.gid) != null) {\n                update(downloadInfo)\n            } else {\n                insert(downloadInfo)\n            }\n        }\n    }\n\n    @Synchronized\n    fun removeDownloadInfo(downloadInfo: DownloadInfo) {\n        db.downloadsDao().delete(downloadInfo)\n    }\n\n    @get:Synchronized\n    val allDownloadDirname: List<DownloadDirname>\n        get() = db.downloadDirnameDao().list()\n\n    @Synchronized\n    fun getDownloadDirname(gid: Long): String? {\n        val dao = db.downloadDirnameDao()\n        val raw = dao.load(gid)\n        return raw?.dirname\n    }\n\n    @Synchronized\n    fun putDownloadDirname(gid: Long, dirname: String?) {\n        val dao = db.downloadDirnameDao()\n        var raw = dao.load(gid)\n        if (raw != null) {\n            raw.dirname = dirname\n            dao.update(raw)\n        } else {\n            raw = DownloadDirname(gid, dirname)\n            dao.insert(raw)\n        }\n    }\n\n    @Synchronized\n    fun removeDownloadDirname(gid: Long) {\n        val dao = db.downloadDirnameDao()\n        dao.deleteByKey(gid)\n    }\n\n    @get:Synchronized\n    val allDownloadLabelList: List<DownloadLabel>\n        get() = db.downloadLabelDao().list()\n\n    @Synchronized\n    fun addDownloadLabel(label: String): DownloadLabel {\n        val dao = db.downloadLabelDao()\n        val raw = DownloadLabel()\n        raw.label = label\n        raw.time = System.currentTimeMillis()\n        raw.id = dao.insert(raw)\n        return raw\n    }\n\n    @Synchronized\n    fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {\n        // Reset id\n        raw.id = null\n        val dao = db.downloadLabelDao()\n        raw.id = dao.insert(raw)\n        return raw\n    }\n\n    @Synchronized\n    fun updateDownloadLabel(raw: DownloadLabel?) {\n        val dao = db.downloadLabelDao()\n        dao.update(raw!!)\n    }\n\n    @Synchronized\n    fun moveDownloadLabel(fromPosition: Int, toPosition: Int) {\n        if (fromPosition == toPosition) {\n            return\n        }\n        val reverse = fromPosition > toPosition\n        val offset = if (reverse) toPosition else fromPosition\n        val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1\n        val dao = db.downloadLabelDao()\n        val list = dao.list(offset, limit)\n        val step = if (reverse) 1 else -1\n        val start = if (reverse) limit - 1 else 0\n        val end = if (reverse) 0 else limit - 1\n        val toTime = list[end].time\n        var i = end\n        while (if (reverse) i < start else i > 0) {\n            val aTime = list[i].time\n            val bTime = list[i + step].time\n            list[i].time = if (aTime == bTime) bTime + step else bTime\n            i += step\n        }\n        list[start].time = toTime\n        dao.update(list)\n    }\n\n    @Synchronized\n    fun removeDownloadLabel(raw: DownloadLabel?) {\n        val dao = db.downloadLabelDao()\n        dao.delete(raw!!)\n    }\n\n    @get:Synchronized\n    val allLocalFavorites: List<GalleryInfo>\n        get() {\n            val dao = db.localFavoritesDao()\n            val list = dao.list()\n            return ArrayList<GalleryInfo>(list)\n        }\n\n    @Synchronized\n    fun searchLocalFavorites(query: String): List<GalleryInfo> {\n        val dao = db.localFavoritesDao()\n        val list = dao.list(\"%$query%\")\n        return ArrayList<GalleryInfo>(list)\n    }\n\n    @Synchronized\n    fun removeLocalFavorites(gid: Long) {\n        db.localFavoritesDao().deleteByKey(gid)\n    }\n\n    @Synchronized\n    fun removeLocalFavorites(gidArray: LongArray) {\n        val dao = db.localFavoritesDao()\n        for (gid in gidArray) {\n            dao.deleteByKey(gid)\n        }\n    }\n\n    @Synchronized\n    fun containLocalFavorites(gid: Long): Boolean {\n        val dao = db.localFavoritesDao()\n        return dao.contains(gid)\n    }\n\n    @Synchronized\n    fun putLocalFavorites(galleryInfo: GalleryInfo) {\n        val dao = db.localFavoritesDao()\n        if (null == dao.load(galleryInfo.gid)) {\n            val info: LocalFavoriteInfo\n            if (galleryInfo is LocalFavoriteInfo) {\n                info = galleryInfo\n            } else {\n                info = LocalFavoriteInfo(galleryInfo)\n                info.time = System.currentTimeMillis()\n            }\n            dao.insert(info)\n        }\n    }\n\n    @Synchronized\n    fun putLocalFavorites(galleryInfoList: List<GalleryInfo>) {\n        for (gi in galleryInfoList) {\n            putLocalFavorites(gi)\n        }\n    }\n\n    @get:Synchronized\n    val allQuickSearch: List<QuickSearch>\n        get() {\n            val dao = db.quickSearchDao()\n            return dao.list()\n        }\n\n    @Synchronized\n    fun insertQuickSearch(quickSearch: QuickSearch) {\n        val dao = db.quickSearchDao()\n        quickSearch.id = null\n        quickSearch.time = System.currentTimeMillis()\n        quickSearch.id = dao.insert(quickSearch)\n    }\n\n    @Synchronized\n    fun importQuickSearch(quickSearchList: List<QuickSearch?>) {\n        val dao = db.quickSearchDao()\n        for (quickSearch in quickSearchList) {\n            dao.insert(quickSearch!!)\n        }\n    }\n\n    @Synchronized\n    fun deleteQuickSearch(quickSearch: QuickSearch?) {\n        quickSearch ?: return\n        val dao = db.quickSearchDao()\n        dao.delete(quickSearch)\n    }\n\n    @Synchronized\n    fun moveQuickSearch(fromPosition: Int, toPosition: Int) {\n        if (fromPosition == toPosition) {\n            return\n        }\n        val reverse = fromPosition > toPosition\n        val offset = if (reverse) toPosition else fromPosition\n        val limit = if (reverse) fromPosition - toPosition + 1 else toPosition - fromPosition + 1\n        val dao = db.quickSearchDao()\n        val list = dao.list(offset, limit)\n        val step = if (reverse) 1 else -1\n        val start = if (reverse) limit - 1 else 0\n        val end = if (reverse) 0 else limit - 1\n        val toTime = list[end].time\n        var i = end\n        while (if (reverse) i < start else i > 0) {\n            val aTime = list[i].time\n            val bTime = list[i + step].time\n            list[i].time = if (aTime == bTime) bTime + step else bTime\n            i += step\n        }\n        list[start].time = toTime\n        dao.update(list)\n    }\n\n    @get:Synchronized\n    val historyLazyList: PagingSource<Int, HistoryInfo>\n        get() = db.historyDao().listLazy()\n\n    @Synchronized\n    fun putHistoryInfo(galleryInfo: GalleryInfo) {\n        val dao = db.historyDao()\n        val info = galleryInfo as? HistoryInfo ?: HistoryInfo(galleryInfo)\n        info.time = System.currentTimeMillis()\n        if (null != dao.load(info.gid)) {\n            dao.update(info)\n        } else {\n            dao.insert(info)\n        }\n    }\n\n    @Synchronized\n    fun updateHistoryFavSlot(gid: Long, slot: Int) {\n        val dao = db.historyDao()\n        val info = dao.load(gid)\n        if (null != info) {\n            info.favoriteSlot = slot\n            dao.update(info)\n        }\n    }\n\n    @Synchronized\n    fun putHistoryInfo(historyInfoList: List<HistoryInfo>) {\n        val dao = db.historyDao()\n        for (info in historyInfoList) {\n            if (null == dao.load(info.gid)) {\n                dao.insert(info)\n            }\n        }\n    }\n\n    @Synchronized\n    fun deleteHistoryInfo(info: HistoryInfo?) {\n        val dao = db.historyDao()\n        dao.delete(info!!)\n    }\n\n    @Synchronized\n    fun clearHistoryInfo() {\n        val dao = db.historyDao()\n        dao.deleteAll()\n    }\n\n    @get:Synchronized\n    val allFilter: List<Filter>\n        get() = db.filterDao().list()\n\n    @Synchronized\n    fun addFilter(filter: Filter): Boolean {\n        val existFilter: Filter? = try {\n            db.filterDao().load(filter.text!!, filter.mode)\n        } catch (_: Exception) {\n            null\n        }\n        return if (existFilter == null) {\n            filter.id = null\n            filter.id = db.filterDao().insert(filter)\n            true\n        } else {\n            false\n        }\n    }\n\n    @Synchronized\n    fun deleteFilter(filter: Filter) {\n        db.filterDao().delete(filter)\n    }\n\n    @Synchronized\n    fun triggerFilter(filter: Filter) {\n        filter.enable = filter.enable?.not() == true\n        db.filterDao().update(filter)\n    }\n\n    private fun <T> copyDao(from: BasicDao<T>, to: BasicDao<T>) {\n        val list = from.list()\n        for (item in list) to.insert(item)\n    }\n\n    @Synchronized\n    fun exportDB(context: Context, uri: Uri): Boolean {\n        val ehExportName = \"eh.export.db\"\n        runCatching {\n            // Delete old export db\n            context.deleteDatabase(ehExportName)\n            val newDb =\n                databaseBuilder(context, EhDatabase::class.java, ehExportName).build()\n\n            // Copy data to a export db\n            copyDao(db.downloadsDao(), newDb.downloadsDao())\n            copyDao(db.downloadLabelDao(), newDb.downloadLabelDao())\n            copyDao(db.downloadDirnameDao(), newDb.downloadDirnameDao())\n            copyDao(db.historyDao(), newDb.historyDao())\n            copyDao(db.quickSearchDao(), newDb.quickSearchDao())\n            copyDao(db.localFavoritesDao(), newDb.localFavoritesDao())\n            copyDao(db.filterDao(), newDb.filterDao())\n\n            // Close export db so we can copy it\n            newDb.close()\n\n            // Copy export db to data dir\n            val dbFile = context.getDatabasePath(ehExportName)\n            UniFile.fromFile(dbFile)!! sendTo UniFile.fromUri(context, uri)!!\n            return true\n        }.onFailure {\n            it.printStackTrace()\n        }\n        return false\n    }\n\n    /**\n     * @return error string, null for no error\n     */\n    @Synchronized\n    fun importDB(context: Context, uri: Uri): String? {\n        val tmpDBName = \"tmp.db\"\n        runCatching {\n            val oldDB = databaseBuilder(context, EhDatabase::class.java, tmpDBName)\n                .createFromInputStream { context.contentResolver.openInputStream(uri) }.build()\n            // Download label\n            val manager = DownloadManager\n            runCatching {\n                val downloadLabelList = oldDB.downloadLabelDao().list()\n                manager.addDownloadLabel(downloadLabelList)\n            }\n            // Downloads\n            runCatching {\n                val downloadInfoList = oldDB.downloadsDao().list()\n                manager.addDownload(downloadInfoList, false)\n            }\n            // Download dirname\n            runCatching {\n                oldDB.downloadDirnameDao().list().forEach {\n                    putDownloadDirname(it.gid, it.dirname)\n                }\n            }\n            // History\n            runCatching {\n                val historyInfoList = oldDB.historyDao().list()\n                putHistoryInfo(historyInfoList)\n            }\n            // QuickSearch\n            runCatching {\n                val quickSearchList = oldDB.quickSearchDao().list()\n                val currentQuickSearchList = db.quickSearchDao().list()\n                val importList = quickSearchList.mapNotNull { newQS ->\n                    newQS.takeIf { currentQuickSearchList.find { it.name == newQS.name } == null }\n                }\n                importQuickSearch(importList)\n            }\n            // LocalFavorites\n            runCatching {\n                oldDB.localFavoritesDao().list().forEach {\n                    putLocalFavorites(it)\n                }\n            }\n            // Filter\n            runCatching {\n                val filterList = oldDB.filterDao().list()\n                val currentFilterList = db.filterDao().list()\n                filterList.forEach {\n                    if (it !in currentFilterList) addFilter(it)\n                }\n            }\n            oldDB.close()\n            context.deleteDatabase(tmpDBName)\n        }.onFailure {\n            it.printStackTrace()\n            return context.getString(R.string.settings_advanced_import_data_cant_read)\n        }\n        return null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/EhProxySelector.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer\n\nimport android.text.TextUtils\nimport com.hippo.ehviewer.Settings.proxyIp\nimport com.hippo.ehviewer.Settings.proxyPort\nimport com.hippo.ehviewer.Settings.proxyType\nimport com.hippo.network.InetValidator.isValidInetPort\nimport com.hippo.util.ExceptionUtils\nimport java.io.IOException\nimport java.net.InetAddress\nimport java.net.InetSocketAddress\nimport java.net.Proxy\nimport java.net.ProxySelector\nimport java.net.SocketAddress\nimport java.net.URI\n\nclass EhProxySelector internal constructor() : ProxySelector() {\n    private var delegation: ProxySelector? = null\n    private var alternative: ProxySelector?\n\n    init {\n        alternative = getDefault() ?: NullProxySelector()\n        updateProxy()\n    }\n\n    fun updateProxy() {\n        delegation = when (proxyType) {\n            TYPE_DIRECT -> NullProxySelector()\n            TYPE_SYSTEM -> alternative\n            TYPE_HTTP, TYPE_SOCKS -> null\n            else -> alternative\n        }\n    }\n\n    override fun select(uri: URI): List<Proxy> {\n        val type = proxyType\n        if (type == TYPE_HTTP || type == TYPE_SOCKS) {\n            runCatching {\n                val ip = proxyIp\n                val port = proxyPort\n                if (!TextUtils.isEmpty(ip) && isValidInetPort(port)) {\n                    val inetAddress = InetAddress.getByName(ip)\n                    val socketAddress = InetSocketAddress(inetAddress, port)\n                    return listOf(\n                        Proxy(\n                            if (type == TYPE_HTTP) Proxy.Type.HTTP else Proxy.Type.SOCKS,\n                            socketAddress,\n                        ),\n                    )\n                }\n            }.onFailure {\n                ExceptionUtils.throwIfFatal(it)\n                it.printStackTrace()\n            }\n        }\n        return delegation?.select(uri) ?: alternative!!.select(uri)\n    }\n\n    override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {\n        delegation?.select(uri)\n    }\n\n    private class NullProxySelector : ProxySelector() {\n        override fun select(uri: URI): List<Proxy> = listOf(Proxy.NO_PROXY)\n\n        override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {}\n    }\n\n    companion object {\n        const val TYPE_DIRECT = 0\n        const val TYPE_SYSTEM = 1\n        const val TYPE_HTTP = 2\n        const val TYPE_SOCKS = 3\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/FavouriteStatusRouter.java",
    "content": "/*\n * Copyright 2019 Hippo Seven\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\npackage com.hippo.ehviewer;\n\nimport android.annotation.SuppressLint;\n\nimport com.hippo.ehviewer.client.data.GalleryInfo;\nimport com.hippo.yorozuya.IntIdGenerator;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic class FavouriteStatusRouter {\n    private static final String KEY_DATA_MAP_NEXT_ID = \"data_map_next_id\";\n\n    private final IntIdGenerator idGenerator = new IntIdGenerator(Settings.getInt(KEY_DATA_MAP_NEXT_ID, 0));\n    @SuppressLint(\"UseSparseArrays\")\n    private final HashMap<Integer, Map<Long, GalleryInfo>> maps = new HashMap<>();\n\n    private final List<Listener> listeners = new ArrayList<>();\n\n    public int saveDataMap(Map<Long, GalleryInfo> map) {\n        int id = idGenerator.nextId();\n        maps.put(id, map);\n        Settings.putInt(KEY_DATA_MAP_NEXT_ID, idGenerator.nextId());\n        return id;\n    }\n\n    public Map<Long, GalleryInfo> restoreDataMap(int id) {\n        return maps.remove(id);\n    }\n\n    public void modifyFavourites(long gid, int slot) {\n        for (Map<Long, GalleryInfo> map : maps.values()) {\n            GalleryInfo info = map.get(gid);\n            if (info != null) {\n                info.setFavoriteSlot(slot);\n            }\n        }\n\n        for (Listener listener : listeners) {\n            listener.onModifyFavourites(gid, slot);\n        }\n    }\n\n    public void addListener(Listener listener) {\n        listeners.add(listener);\n    }\n\n    public void removeListener(Listener listener) {\n        listeners.remove(listener);\n    }\n\n    public interface Listener {\n        void onModifyFavourites(long gid, int slot);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/GetText.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer\n\nimport androidx.annotation.StringRes\n\nobject GetText {\n    fun getString(@StringRes id: Int): String = EhApplication.application.getString(id)\n\n    fun getString(@StringRes id: Int, vararg formatArgs: Any?): String = EhApplication.application.getString(id, *formatArgs)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/Settings.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer\n\nimport android.content.SharedPreferences\nimport android.net.Uri\nimport android.util.Log\nimport androidx.core.content.edit\nimport androidx.preference.PreferenceManager\nimport com.hippo.ehviewer.EhApplication.Companion.application\nimport com.hippo.ehviewer.client.data.FavListUrlBuilder\nimport com.hippo.ehviewer.ui.scene.GalleryListScene\nimport com.hippo.glgallery.GalleryView\nimport com.hippo.okhttp.CHROME_USER_AGENT\nimport com.hippo.unifile.UniFile\nimport com.hippo.yorozuya.NumberUtils\nimport java.util.Locale\n\n@Suppress(\"SameParameterValue\")\nobject Settings {\n    /********************\n     ****** Eh\n     ********************/\n    const val KEY_ACCOUNT = \"account\"\n    const val KEY_GALLERY_SITE = \"gallery_site\"\n    private const val DEFAULT_GALLERY_SITE = 0\n    private const val KEY_IMAGE_LIMITS = \"image_limits\"\n    private const val KEY_U_CONFIG = \"uconfig\"\n    private const val KEY_MY_TAGS = \"mytags\"\n    const val KEY_THEME = \"theme\"\n    private const val DEFAULT_THEME = -1\n    const val KEY_BLACK_DARK_THEME = \"black_dark_theme\"\n    private const val DEFAULT_BLACK_DARK_THEME = false\n    private const val KEY_LAUNCH_PAGE = \"launch_page\"\n    private const val DEFAULT_LAUNCH_PAGE = 0\n    const val KEY_LIST_MODE = \"list_mode\"\n    private const val DEFAULT_LIST_MODE = 0\n    const val KEY_DETAIL_SIZE = \"detail_size_\"\n    private const val DEFAULT_DETAIL_SIZE = 8\n    const val KEY_LIST_THUMB_SIZE = \"list_cover_size\"\n    private const val DEFAULT_LIST_THUMB_SIZE = 40\n    const val KEY_THUMB_SIZE = \"cover_size\"\n    private const val DEFAULT_THUMB_SIZE = 4\n    const val KEY_THUMB_SHOW_TITLE = \"thumb_show_title\"\n    private const val DEFAULT_THUMB_SHOW_TITLE = true\n    private const val KEY_SHOW_JPN_TITLE = \"show_jpn_title\"\n    private const val DEFAULT_SHOW_JPN_TITLE = false\n    private const val KEY_SHOW_GALLERY_PAGES = \"show_gallery_pages\"\n    private const val DEFAULT_SHOW_GALLERY_PAGES = true\n    private const val KEY_SHOW_COMMENTS = \"show_gallery_comments\"\n    private const val DEFAULT_SHOW_COMMENTS = true\n    private const val KEY_COMMENT_THRESHOLD = \"comment_threshold\"\n    private const val DEFAULT_COMMENT_THRESHOLD = -101\n    private const val KEY_PREVIEW_NUM = \"preview_num\"\n    private const val DEFAULT_PREVIEW_NUM = 60\n    private const val KEY_PREVIEW_SIZE = \"preview_size\"\n    private const val DEFAULT_PREVIEW_SIZE = 3\n    const val KEY_SHOW_TAG_TRANSLATIONS = \"show_tag_translations\"\n    private const val DEFAULT_SHOW_TAG_TRANSLATIONS = false\n    private const val KEY_TRANSLATIONS_LAST_UPDATE = \"translations_last_update\"\n    const val KEY_TAG_TRANSLATIONS_SOURCE = \"tag_translations_source\"\n    private const val KEY_METERED_NETWORK_WARNING = \"cellular_network_warning\"\n    private const val DEFAULT_METERED_NETWORK_WARNING = false\n    private const val KEY_REQUEST_NEWS = \"request_news\"\n    private const val DEFAULT_REQUEST_NEWS = false\n    private const val KEY_HIDE_HV_EVENTS = \"hide_hv_events\"\n    private const val DEFAULT_HIDE_HV_EVENTS = false\n    val SIGN_IN_REQUIRED = arrayOf(\n        KEY_GALLERY_SITE,\n        KEY_IMAGE_LIMITS,\n        KEY_U_CONFIG,\n        KEY_MY_TAGS,\n        KEY_SHOW_JPN_TITLE,\n        KEY_REQUEST_NEWS,\n        KEY_HIDE_HV_EVENTS,\n    )\n\n    /********************\n     ****** Read\n     ********************/\n    private const val KEY_SCREEN_ROTATION = \"screen_rotation\"\n    private const val DEFAULT_SCREEN_ROTATION = 0\n    private const val KEY_READING_DIRECTION = \"reading_direction\"\n    private const val DEFAULT_READING_DIRECTION = GalleryView.LAYOUT_RIGHT_TO_LEFT\n    private const val KEY_PAGE_SCALING = \"page_scaling\"\n    private const val DEFAULT_PAGE_SCALING = GalleryView.SCALE_FIT\n    private const val KEY_START_POSITION = \"start_position\"\n    private const val DEFAULT_START_POSITION = GalleryView.START_POSITION_TOP_RIGHT\n    private const val KEY_READ_THEME = \"read_theme\"\n    private const val DEFAULT_READ_THEME = 1\n    private const val KEY_KEEP_SCREEN_ON = \"keep_screen_on\"\n    private const val DEFAULT_KEEP_SCREEN_ON = false\n    private const val KEY_SHOW_CLOCK = \"gallery_show_clock\"\n    private const val DEFAULT_SHOW_CLOCK = true\n    private const val KEY_SHOW_PROGRESS = \"gallery_show_progress\"\n    private const val DEFAULT_SHOW_PROGRESS = true\n    private const val KEY_SHOW_BATTERY = \"gallery_show_battery\"\n    private const val DEFAULT_SHOW_BATTERY = true\n    private const val KEY_SHOW_PAGE_INTERVAL = \"gallery_show_page_interval\"\n    private const val DEFAULT_SHOW_PAGE_INTERVAL = false\n    private const val KEY_TURN_PAGE_INTERVAL = \"turn_page_interval\"\n    private const val DEFAULT_TURN_PAGE_INTERVAL = 5\n    private const val KEY_VOLUME_PAGE = \"volume_page\"\n    private const val DEFAULT_VOLUME_PAGE = false\n    private const val KEY_VOLUME_PAGE_INTERVAL = \"volume_page_interval\"\n    private const val DEFAULT_VOLUME_PAGE_INTERVAL = 1\n    private const val KEY_REVERSE_VOLUME_PAGE = \"reserve_volume_page\"\n    private const val DEFAULT_REVERSE_VOLUME_PAGE = false\n    private const val KEY_READING_FULLSCREEN = \"reading_fullscreen\"\n    private const val VALUE_READING_FULLSCREEN = true\n    private const val KEY_CUSTOM_SCREEN_LIGHTNESS = \"custom_screen_lightness\"\n    private const val DEFAULT_CUSTOM_SCREEN_LIGHTNESS = false\n    private const val KEY_SCREEN_LIGHTNESS = \"screen_lightness\"\n    private const val DEFAULT_SCREEN_LIGHTNESS = 50\n\n    /********************\n     ****** Download\n     ********************/\n    const val KEY_DOWNLOAD_LOCATION = \"download_location\"\n    private const val KEY_DOWNLOAD_SAVE_SCHEME = \"image_scheme\"\n    private const val KEY_DOWNLOAD_SAVE_AUTHORITY = \"image_authority\"\n    private const val KEY_DOWNLOAD_SAVE_PATH = \"image_path\"\n    private const val KEY_DOWNLOAD_SAVE_QUERY = \"image_query\"\n    private const val KEY_DOWNLOAD_SAVE_FRAGMENT = \"image_fragment\"\n    const val KEY_MEDIA_SCAN = \"media_scan\"\n    private const val DEFAULT_MEDIA_SCAN = false\n    const val KEY_MULTI_THREAD_DOWNLOAD = \"download_thread\"\n    private const val DEFAULT_MULTI_THREAD_DOWNLOAD = 3\n    const val KEY_DOWNLOAD_DELAY = \"download_delay_2\"\n    private const val DEFAULT_DOWNLOAD_DELAY = 1000\n    private const val KEY_DOWNLOAD_TIMEOUT = \"download_timeout\"\n    private const val DEFAULT_DOWNLOAD_TIMEOUT = 60\n    const val KEY_PRELOAD_IMAGE = \"preload_image\"\n    private const val DEFAULT_PRELOAD_IMAGE = 5\n    const val KEY_DOWNLOAD_ORIGIN_IMAGE = \"download_origin_image_\"\n    private const val DEFAULT_DOWNLOAD_ORIGIN_IMAGE = 0\n\n    /********************\n     ****** Privacy and Security\n     ********************/\n    private const val KEY_SECURITY = \"security\"\n    private const val DEFAULT_SECURITY = \"\"\n    private const val KEY_ENABLE_FINGERPRINT = \"enable_fingerprint\"\n    private const val DEFAULT_ENABLE_FINGERPRINT = false\n    private const val KEY_SEC_SECURITY = \"enable_secure\"\n    private const val DEFAULT_SEC_SECURITY = false\n\n    /********************\n     ****** Advanced\n     ********************/\n    private const val KEY_SAVE_PARSE_ERROR_BODY = \"save_parse_error_body\"\n    private const val DEFAULT_SAVE_PARSE_ERROR_BODY = true\n    private const val KEY_SAVE_CRASH_LOG = \"save_crash_log\"\n    private const val DEFAULT_SAVE_CRASH_LOG = true\n    private const val KEY_READ_CACHE_SIZE = \"read_cache_size\"\n    private const val DEFAULT_READ_CACHE_SIZE = 320\n    const val KEY_APP_LANGUAGE = \"app_language\"\n    private const val DEFAULT_APP_LANGUAGE = \"system\"\n    private const val KEY_PROXY_TYPE = \"proxy_type\"\n    private const val DEFAULT_PROXY_TYPE = EhProxySelector.TYPE_SYSTEM\n    private const val KEY_PROXY_IP = \"proxy_ip\"\n    private val DEFAULT_PROXY_IP: String? = null\n    private const val KEY_PROXY_PORT = \"proxy_port\"\n    private const val DEFAULT_PROXY_PORT = -1\n    private const val KEY_USER_AGENT = \"user_agent\"\n    private const val KEY_APP_LINK_VERIFY_TIP = \"app_link_verify_tip\"\n    private const val DEFAULT_APP_LINK_VERIFY_TIP = false\n\n    /********************\n     ****** Favorites\n     ********************/\n    private const val KEY_FAV_CAT_0 = \"fav_cat_0\"\n    private const val KEY_FAV_CAT_1 = \"fav_cat_1\"\n    private const val KEY_FAV_CAT_2 = \"fav_cat_2\"\n    private const val KEY_FAV_CAT_3 = \"fav_cat_3\"\n    private const val KEY_FAV_CAT_4 = \"fav_cat_4\"\n    private const val KEY_FAV_CAT_5 = \"fav_cat_5\"\n    private const val KEY_FAV_CAT_6 = \"fav_cat_6\"\n    private const val KEY_FAV_CAT_7 = \"fav_cat_7\"\n    private const val KEY_FAV_CAT_8 = \"fav_cat_8\"\n    private const val KEY_FAV_CAT_9 = \"fav_cat_9\"\n    private const val DEFAULT_FAV_CAT_0 = \"Favorites 0\"\n    private const val DEFAULT_FAV_CAT_1 = \"Favorites 1\"\n    private const val DEFAULT_FAV_CAT_2 = \"Favorites 2\"\n    private const val DEFAULT_FAV_CAT_3 = \"Favorites 3\"\n    private const val DEFAULT_FAV_CAT_4 = \"Favorites 4\"\n    private const val DEFAULT_FAV_CAT_5 = \"Favorites 5\"\n    private const val DEFAULT_FAV_CAT_6 = \"Favorites 6\"\n    private const val DEFAULT_FAV_CAT_7 = \"Favorites 7\"\n    private const val DEFAULT_FAV_CAT_8 = \"Favorites 8\"\n    private const val DEFAULT_FAV_CAT_9 = \"Favorites 9\"\n    private const val KEY_FAV_COUNT_0 = \"fav_count_0\"\n    private const val KEY_FAV_COUNT_1 = \"fav_count_1\"\n    private const val KEY_FAV_COUNT_2 = \"fav_count_2\"\n    private const val KEY_FAV_COUNT_3 = \"fav_count_3\"\n    private const val KEY_FAV_COUNT_4 = \"fav_count_4\"\n    private const val KEY_FAV_COUNT_5 = \"fav_count_5\"\n    private const val KEY_FAV_COUNT_6 = \"fav_count_6\"\n    private const val KEY_FAV_COUNT_7 = \"fav_count_7\"\n    private const val KEY_FAV_COUNT_8 = \"fav_count_8\"\n    private const val KEY_FAV_COUNT_9 = \"fav_count_9\"\n    private const val KEY_FAV_LOCAL = \"fav_local\"\n    private const val KEY_FAV_CLOUD = \"fav_cloud\"\n    private const val DEFAULT_FAV_COUNT = 0\n    private const val KEY_RECENT_FAV_CAT = \"recent_fav_cat\"\n    private const val DEFAULT_RECENT_FAV_CAT = FavListUrlBuilder.FAV_CAT_ALL\n\n    // -1 for local, 0 - 9 for cloud favorite, other for no default fav slot\n    const val INVALID_DEFAULT_FAV_SLOT = -2\n    private const val KEY_DEFAULT_FAV_SLOT = \"default_favorite_2\"\n    private const val DEFAULT_DEFAULT_FAV_SLOT = INVALID_DEFAULT_FAV_SLOT\n    private const val KEY_NEVER_ADD_FAV_NOTES = \"never_add_favorite_notes\"\n    private const val DEFAULT_NEVER_ADD_FAV_NOTES = false\n\n    /********************\n     ****** Guide\n     ********************/\n    private const val KEY_GUIDE_GALLERY = \"guide_gallery\"\n    private const val DEFAULT_GUIDE_GALLERY = true\n\n    /********************\n     ****** Others\n     ********************/\n    private val TAG = Settings::class.java.simpleName\n    private const val KEY_SELECT_SITE = \"select_site\"\n    private const val DEFAULT_SELECT_SITE = true\n    private const val KEY_NEED_SIGN_IN = \"need_sign_in\"\n    private const val DEFAULT_NEED_SIGN_IN = true\n    private const val KEY_DISPLAY_NAME = \"display_name\"\n    private val DEFAULT_DISPLAY_NAME: String? = null\n    private const val KEY_AVATAR = \"avatar\"\n    private val DEFAULT_AVATAR: String? = null\n    private const val KEY_QS_SAVE_PROGRESS = \"qs_save_progress\"\n    private const val DEFAULT_QS_SAVE_PROGRESS = false\n    private const val KEY_HAS_DEFAULT_DOWNLOAD_LABEL = \"has_default_download_label\"\n    private const val DEFAULT_HAS_DOWNLOAD_LABEL = false\n    private const val KEY_DEFAULT_DOWNLOAD_LABEL = \"default_download_label\"\n    private val DEFAULT_DOWNLOAD_LABEL: String? = null\n    private const val KEY_RECENT_DOWNLOAD_LABEL = \"recent_download_label\"\n    private val DEFAULT_RECENT_DOWNLOAD_LABEL: String? = null\n    private const val KEY_DEFAULT_SORTING_METHOD = \"default_sorting_method\"\n    private const val DEFAULT_SORTING_METHOD = 0\n    private const val KEY_DEFAULT_TOP_LIST = \"default_top_list\"\n    private const val DEFAULT_TOP_LIST = \"15\"\n    private const val KEY_REMOVE_IMAGE_FILES = \"include_pic\"\n    private const val DEFAULT_REMOVE_IMAGE_FILES = true\n    private const val KEY_CLIPBOARD_TEXT_HASH_CODE = \"clipboard_text_hash_code\"\n    private const val DEFAULT_CLIPBOARD_TEXT_HASH_CODE = 0\n    private const val KEY_ARCHIVE_PASSWDS = \"archive_passwds\"\n    private const val KEY_NOTIFICATION_REQUIRED = \"notification_required\"\n    private lateinit var sSettingsPre: SharedPreferences\n\n    fun initialize() {\n        sSettingsPre = PreferenceManager.getDefaultSharedPreferences(application)\n        fixDefaultValue()\n    }\n\n    private fun fixDefaultValue() {\n        if (\"zh\" == Locale.getDefault().language) {\n            // Enable show tag translations if the language is zh\n            if (!sSettingsPre.contains(KEY_SHOW_TAG_TRANSLATIONS)) {\n                putShowTagTranslations(true)\n            }\n        }\n    }\n\n    private fun getBoolean(key: String, defValue: Boolean): Boolean = try {\n        sSettingsPre.getBoolean(key, defValue)\n    } catch (e: ClassCastException) {\n        Log.d(TAG, \"Get ClassCastException when get $key value\", e)\n        defValue\n    }\n\n    private fun putBoolean(key: String, value: Boolean) {\n        sSettingsPre.edit { putBoolean(key, value) }\n    }\n\n    @JvmStatic\n    fun getInt(key: String, defValue: Int): Int = try {\n        sSettingsPre.getInt(key, defValue)\n    } catch (e: ClassCastException) {\n        Log.d(TAG, \"Get ClassCastException when get $key value\", e)\n        defValue\n    }\n\n    @JvmStatic\n    fun putInt(key: String, value: Int) {\n        sSettingsPre.edit { putInt(key, value) }\n    }\n\n    private fun getLong(key: String, defValue: Long): Long = try {\n        sSettingsPre.getLong(key, defValue)\n    } catch (e: ClassCastException) {\n        Log.d(TAG, \"Get ClassCastException when get $key value\", e)\n        defValue\n    }\n\n    private fun putLong(key: String, value: Long) {\n        sSettingsPre.edit { putLong(key, value) }\n    }\n\n    private fun getString(key: String, defValue: String?): String? = try {\n        sSettingsPre.getString(key, defValue)\n    } catch (e: ClassCastException) {\n        Log.d(TAG, \"Get ClassCastException when get $key value\", e)\n        defValue\n    }\n\n    private fun putString(key: String, value: String?) {\n        sSettingsPre.edit { putString(key, value) }\n    }\n\n    private fun getStringSet(key: String): MutableSet<String>? = sSettingsPre.getStringSet(key, null)\n\n    private fun putStringToStringSet(key: String, value: String) {\n        var set = getStringSet(key)\n        if (set == null) {\n            set =\n                mutableSetOf(value)\n        } else if (set.contains(value)) {\n            return\n        } else {\n            set.add(value)\n        }\n        sSettingsPre.edit { putStringSet(key, set) }\n    }\n\n    private fun getIntFromStr(key: String, defValue: Int): Int = try {\n        NumberUtils.parseIntSafely(\n            sSettingsPre.getString(key, defValue.toString()),\n            defValue,\n        )\n    } catch (e: ClassCastException) {\n        Log.d(TAG, \"Get ClassCastException when get $key value\", e)\n        defValue\n    }\n\n    private fun putIntToStr(key: String, value: Int) {\n        sSettingsPre.edit { putString(key, value.toString()) }\n    }\n\n    private fun dip2px(dpValue: Int): Int {\n        val scale = application.resources.displayMetrics.density\n        return (dpValue * scale + 0.5f).toInt()\n    }\n\n    val locale: Locale\n        get() {\n            return if (appLanguage != null && appLanguage != \"system\") {\n                Locale.forLanguageTag(appLanguage!!)\n            } else {\n                Locale.getDefault()\n            }\n        }\n\n    val gallerySite: Int\n        get() = getIntFromStr(KEY_GALLERY_SITE, DEFAULT_GALLERY_SITE)\n    fun putGallerySite(value: Int) {\n        putIntToStr(KEY_GALLERY_SITE, value)\n    }\n\n    val theme: Int\n        get() = getIntFromStr(KEY_THEME, DEFAULT_THEME)\n    fun putTheme(theme: Int) {\n        putIntToStr(KEY_THEME, theme)\n    }\n\n    val blackDarkTheme\n        get() = getBoolean(KEY_BLACK_DARK_THEME, DEFAULT_BLACK_DARK_THEME)\n\n    val launchPageGalleryListSceneAction: String\n        get() {\n            return when (getIntFromStr(KEY_LAUNCH_PAGE, DEFAULT_LAUNCH_PAGE)) {\n                3 -> GalleryListScene.ACTION_TOP_LIST\n                2 -> GalleryListScene.ACTION_WHATS_HOT\n                1 -> GalleryListScene.ACTION_SUBSCRIPTION\n                else -> GalleryListScene.ACTION_HOMEPAGE\n            }\n        }\n\n    val listMode: Int\n        get() = getIntFromStr(KEY_LIST_MODE, DEFAULT_LIST_MODE)\n\n    val detailSize: Int\n        get() = dip2px(40 * getInt(KEY_DETAIL_SIZE, DEFAULT_DETAIL_SIZE))\n\n    val listThumbSize: Int\n        get() = dip2px(2 * getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE))\n    val listTitleSingleLine: Boolean\n        get() = getInt(KEY_LIST_THUMB_SIZE, DEFAULT_LIST_THUMB_SIZE) < DEFAULT_LIST_THUMB_SIZE - 2\n\n    val thumbSize: Int\n        get() = dip2px(40 * getInt(KEY_THUMB_SIZE, DEFAULT_THUMB_SIZE))\n\n    val thumbShowTitle: Boolean\n        get() = getBoolean(KEY_THUMB_SHOW_TITLE, DEFAULT_THUMB_SHOW_TITLE)\n\n    val showJpnTitle: Boolean\n        get() = getBoolean(KEY_SHOW_JPN_TITLE, DEFAULT_SHOW_JPN_TITLE)\n\n    val showGalleryPages: Boolean\n        get() = getBoolean(KEY_SHOW_GALLERY_PAGES, DEFAULT_SHOW_GALLERY_PAGES)\n\n    val showComments: Boolean\n        get() = getBoolean(KEY_SHOW_COMMENTS, DEFAULT_SHOW_COMMENTS)\n\n    val commentThreshold: Int\n        get() = getInt(KEY_COMMENT_THRESHOLD, DEFAULT_COMMENT_THRESHOLD)\n\n    val previewNum: Int\n        get() = getInt(KEY_PREVIEW_NUM, DEFAULT_PREVIEW_NUM)\n\n    val previewSize: Int\n        get() = dip2px(40 * getInt(KEY_PREVIEW_SIZE, DEFAULT_PREVIEW_SIZE))\n\n    val showTagTranslations: Boolean\n        get() = getBoolean(KEY_SHOW_TAG_TRANSLATIONS, DEFAULT_SHOW_TAG_TRANSLATIONS)\n    private fun putShowTagTranslations(value: Boolean) {\n        putBoolean(KEY_SHOW_TAG_TRANSLATIONS, value)\n    }\n\n    val translationsLastUpdate: Long\n        get() = getLong(KEY_TRANSLATIONS_LAST_UPDATE, -1)\n    fun putTranslationsLastUpdate(value: Long) {\n        putLong(KEY_TRANSLATIONS_LAST_UPDATE, value)\n    }\n\n    val meteredNetworkWarning: Boolean\n        get() = getBoolean(KEY_METERED_NETWORK_WARNING, DEFAULT_METERED_NETWORK_WARNING)\n\n    val requestNews: Boolean\n        get() = getBoolean(KEY_REQUEST_NEWS, DEFAULT_REQUEST_NEWS)\n\n    val hideHvEvents: Boolean\n        get() = getBoolean(KEY_HIDE_HV_EVENTS, DEFAULT_HIDE_HV_EVENTS)\n\n    val screenRotation: Int\n        get() = getIntFromStr(KEY_SCREEN_ROTATION, DEFAULT_SCREEN_ROTATION)\n    fun putScreenRotation(value: Int) {\n        putIntToStr(KEY_SCREEN_ROTATION, value)\n    }\n\n    @GalleryView.LayoutMode\n    val readingDirection: Int\n        get() = GalleryView.sanitizeLayoutMode(getIntFromStr(KEY_READING_DIRECTION, DEFAULT_READING_DIRECTION))\n    fun putReadingDirection(value: Int) {\n        putIntToStr(KEY_READING_DIRECTION, value)\n    }\n\n    @GalleryView.ScaleMode\n    val pageScaling: Int\n        get() = GalleryView.sanitizeScaleMode(getIntFromStr(KEY_PAGE_SCALING, DEFAULT_PAGE_SCALING))\n    fun putPageScaling(value: Int) {\n        putIntToStr(KEY_PAGE_SCALING, value)\n    }\n\n    @GalleryView.StartPosition\n    val startPosition: Int\n        get() = GalleryView.sanitizeStartPosition(getIntFromStr(KEY_START_POSITION, DEFAULT_START_POSITION))\n    fun putStartPosition(value: Int) {\n        putIntToStr(KEY_START_POSITION, value)\n    }\n\n    val readTheme: Int\n        get() = getIntFromStr(KEY_READ_THEME, DEFAULT_READ_THEME)\n    fun putReadTheme(value: Int) {\n        putIntToStr(KEY_READ_THEME, value)\n    }\n\n    val keepScreenOn: Boolean\n        get() = getBoolean(KEY_KEEP_SCREEN_ON, DEFAULT_KEEP_SCREEN_ON)\n    fun putKeepScreenOn(value: Boolean) {\n        putBoolean(KEY_KEEP_SCREEN_ON, value)\n    }\n\n    val showClock: Boolean\n        get() = getBoolean(KEY_SHOW_CLOCK, DEFAULT_SHOW_CLOCK)\n    fun putShowClock(value: Boolean) {\n        putBoolean(KEY_SHOW_CLOCK, value)\n    }\n\n    val showProgress: Boolean\n        get() = getBoolean(KEY_SHOW_PROGRESS, DEFAULT_SHOW_PROGRESS)\n    fun putShowProgress(value: Boolean) {\n        putBoolean(KEY_SHOW_PROGRESS, value)\n    }\n\n    val showBattery: Boolean\n        get() = getBoolean(KEY_SHOW_BATTERY, DEFAULT_SHOW_BATTERY)\n    fun putShowBattery(value: Boolean) {\n        putBoolean(KEY_SHOW_BATTERY, value)\n    }\n\n    val showPageInterval: Boolean\n        get() = getBoolean(KEY_SHOW_PAGE_INTERVAL, DEFAULT_SHOW_PAGE_INTERVAL)\n    fun putShowPageInterval(value: Boolean) {\n        putBoolean(KEY_SHOW_PAGE_INTERVAL, value)\n    }\n\n    val turnPageInterval: Int\n        get() = getInt(KEY_TURN_PAGE_INTERVAL, DEFAULT_TURN_PAGE_INTERVAL)\n    fun putTurnPageInterval(value: Int) {\n        putInt(KEY_TURN_PAGE_INTERVAL, value)\n    }\n\n    val volumePage: Boolean\n        get() = getBoolean(KEY_VOLUME_PAGE, DEFAULT_VOLUME_PAGE)\n    fun putVolumePage(value: Boolean) {\n        putBoolean(KEY_VOLUME_PAGE, value)\n    }\n\n    val volumePageInterval: Int\n        get() = getInt(KEY_VOLUME_PAGE_INTERVAL, DEFAULT_VOLUME_PAGE_INTERVAL)\n    fun putVolumePageInterval(value: Int) {\n        putInt(KEY_VOLUME_PAGE_INTERVAL, value)\n    }\n\n    val reverseVolumePage: Boolean\n        get() = getBoolean(KEY_REVERSE_VOLUME_PAGE, DEFAULT_REVERSE_VOLUME_PAGE)\n    fun putReverseVolumePage(value: Boolean) {\n        putBoolean(KEY_REVERSE_VOLUME_PAGE, value)\n    }\n\n    val readingFullscreen: Boolean\n        get() = getBoolean(KEY_READING_FULLSCREEN, VALUE_READING_FULLSCREEN)\n    fun putReadingFullscreen(value: Boolean) {\n        putBoolean(KEY_READING_FULLSCREEN, value)\n    }\n\n    val customScreenLightness: Boolean\n        get() = getBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, DEFAULT_CUSTOM_SCREEN_LIGHTNESS)\n    fun putCustomScreenLightness(value: Boolean) {\n        putBoolean(KEY_CUSTOM_SCREEN_LIGHTNESS, value)\n    }\n\n    val screenLightness: Int\n        get() = getInt(KEY_SCREEN_LIGHTNESS, DEFAULT_SCREEN_LIGHTNESS)\n    fun putScreenLightness(value: Int) {\n        putInt(KEY_SCREEN_LIGHTNESS, value)\n    }\n\n    val downloadLocation: UniFile?\n        get() {\n            val dir: UniFile?\n            val builder = Uri.Builder()\n            builder.scheme(getString(KEY_DOWNLOAD_SAVE_SCHEME, null))\n            builder.encodedAuthority(getString(KEY_DOWNLOAD_SAVE_AUTHORITY, null))\n            builder.encodedPath(getString(KEY_DOWNLOAD_SAVE_PATH, null))\n            builder.encodedQuery(getString(KEY_DOWNLOAD_SAVE_QUERY, null))\n            builder.encodedFragment(getString(KEY_DOWNLOAD_SAVE_FRAGMENT, null))\n            dir = UniFile.fromUri(application, builder.build())\n            return dir ?: UniFile.fromFile(AppConfig.getDefaultDownloadDir())\n        }\n    fun putDownloadLocation(location: UniFile) {\n        val uri = location.uri\n        putString(KEY_DOWNLOAD_SAVE_SCHEME, uri.scheme)\n        putString(KEY_DOWNLOAD_SAVE_AUTHORITY, uri.encodedAuthority)\n        putString(KEY_DOWNLOAD_SAVE_PATH, uri.encodedPath)\n        putString(KEY_DOWNLOAD_SAVE_QUERY, uri.encodedQuery)\n        putString(KEY_DOWNLOAD_SAVE_FRAGMENT, uri.encodedFragment)\n    }\n\n    val mediaScan: Boolean\n        get() = getBoolean(KEY_MEDIA_SCAN, DEFAULT_MEDIA_SCAN)\n\n    val downloadThreadCount: Int\n        get() = getIntFromStr(KEY_MULTI_THREAD_DOWNLOAD, DEFAULT_MULTI_THREAD_DOWNLOAD)\n\n    val downloadDelay: Int\n        get() = getIntFromStr(KEY_DOWNLOAD_DELAY, DEFAULT_DOWNLOAD_DELAY)\n\n    val downloadTimeout: Int\n        get() = getInt(KEY_DOWNLOAD_TIMEOUT, DEFAULT_DOWNLOAD_TIMEOUT)\n\n    val preloadImage: Int\n        get() = getIntFromStr(KEY_PRELOAD_IMAGE, DEFAULT_PRELOAD_IMAGE)\n\n    fun getDownloadOriginImage(mode: Boolean): Boolean = when (getIntFromStr(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE)) {\n        2 -> mode\n        1 -> true\n        else -> false\n    }\n\n    val skipCopyImage: Boolean\n        get() = getIntFromStr(KEY_DOWNLOAD_ORIGIN_IMAGE, DEFAULT_DOWNLOAD_ORIGIN_IMAGE) == 2\n\n    val security: String?\n        get() = getString(KEY_SECURITY, DEFAULT_SECURITY)\n    fun putSecurity(value: String?) {\n        putString(KEY_SECURITY, value)\n    }\n\n    val enableFingerprint: Boolean\n        get() = getBoolean(KEY_ENABLE_FINGERPRINT, DEFAULT_ENABLE_FINGERPRINT)\n    fun putEnableFingerprint(value: Boolean) {\n        putBoolean(KEY_ENABLE_FINGERPRINT, value)\n    }\n\n    val enabledSecurity: Boolean\n        get() = getBoolean(KEY_SEC_SECURITY, DEFAULT_SEC_SECURITY)\n\n    val saveParseErrorBody: Boolean\n        get() = getBoolean(KEY_SAVE_PARSE_ERROR_BODY, DEFAULT_SAVE_PARSE_ERROR_BODY)\n\n    val saveCrashLog: Boolean\n        get() = getBoolean(KEY_SAVE_CRASH_LOG, DEFAULT_SAVE_CRASH_LOG)\n\n    val readCacheSize: Int\n        get() = getIntFromStr(KEY_READ_CACHE_SIZE, DEFAULT_READ_CACHE_SIZE)\n\n    val appLanguage: String?\n        get() = getString(KEY_APP_LANGUAGE, DEFAULT_APP_LANGUAGE)\n\n    val proxyType: Int\n        get() = getInt(KEY_PROXY_TYPE, DEFAULT_PROXY_TYPE)\n    fun putProxyType(value: Int) {\n        putInt(KEY_PROXY_TYPE, value)\n    }\n\n    val proxyIp: String?\n        get() = getString(KEY_PROXY_IP, DEFAULT_PROXY_IP)\n    fun putProxyIp(value: String?) {\n        putString(KEY_PROXY_IP, value)\n    }\n\n    val proxyPort: Int\n        get() = getInt(KEY_PROXY_PORT, DEFAULT_PROXY_PORT)\n    fun putProxyPort(value: Int) {\n        putInt(KEY_PROXY_PORT, value)\n    }\n\n    val userAgent: String?\n        get() = getString(KEY_USER_AGENT, CHROME_USER_AGENT)\n    fun putUserAgent(value: String?) {\n        putString(KEY_USER_AGENT, value)\n    }\n\n    val appLinkVerifyTip: Boolean\n        get() = getBoolean(KEY_APP_LINK_VERIFY_TIP, DEFAULT_APP_LINK_VERIFY_TIP)\n    fun putAppLinkVerifyTip(value: Boolean) {\n        putBoolean(KEY_APP_LINK_VERIFY_TIP, value)\n    }\n\n    var favCat: Array<String>\n        get() = arrayOf(\n            sSettingsPre.getString(KEY_FAV_CAT_0, DEFAULT_FAV_CAT_0)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_1, DEFAULT_FAV_CAT_1)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_2, DEFAULT_FAV_CAT_2)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_3, DEFAULT_FAV_CAT_3)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_4, DEFAULT_FAV_CAT_4)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_5, DEFAULT_FAV_CAT_5)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_6, DEFAULT_FAV_CAT_6)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_7, DEFAULT_FAV_CAT_7)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_8, DEFAULT_FAV_CAT_8)!!,\n            sSettingsPre.getString(KEY_FAV_CAT_9, DEFAULT_FAV_CAT_9)!!,\n        )\n        set(value) {\n            check(value.size == 10)\n            sSettingsPre.edit {\n                putString(KEY_FAV_CAT_0, value[0])\n                    .putString(KEY_FAV_CAT_1, value[1])\n                    .putString(KEY_FAV_CAT_2, value[2])\n                    .putString(KEY_FAV_CAT_3, value[3])\n                    .putString(KEY_FAV_CAT_4, value[4])\n                    .putString(KEY_FAV_CAT_5, value[5])\n                    .putString(KEY_FAV_CAT_6, value[6])\n                    .putString(KEY_FAV_CAT_7, value[7])\n                    .putString(KEY_FAV_CAT_8, value[8])\n                    .putString(KEY_FAV_CAT_9, value[9])\n            }\n        }\n\n    var favCount: IntArray\n        get() = intArrayOf(\n            sSettingsPre.getInt(KEY_FAV_COUNT_0, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_1, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_2, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_3, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_4, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_5, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_6, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_7, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_8, DEFAULT_FAV_COUNT),\n            sSettingsPre.getInt(KEY_FAV_COUNT_9, DEFAULT_FAV_COUNT),\n        )\n        set(count) {\n            check(count.size == 10)\n            sSettingsPre.edit {\n                putInt(KEY_FAV_COUNT_0, count[0])\n                    .putInt(KEY_FAV_COUNT_1, count[1])\n                    .putInt(KEY_FAV_COUNT_2, count[2])\n                    .putInt(KEY_FAV_COUNT_3, count[3])\n                    .putInt(KEY_FAV_COUNT_4, count[4])\n                    .putInt(KEY_FAV_COUNT_5, count[5])\n                    .putInt(KEY_FAV_COUNT_6, count[6])\n                    .putInt(KEY_FAV_COUNT_7, count[7])\n                    .putInt(KEY_FAV_COUNT_8, count[8])\n                    .putInt(KEY_FAV_COUNT_9, count[9])\n            }\n        }\n\n    val favLocalCount: Int\n        get() = sSettingsPre.getInt(KEY_FAV_LOCAL, DEFAULT_FAV_COUNT)\n    fun putFavLocalCount(count: Int) {\n        sSettingsPre.edit { putInt(KEY_FAV_LOCAL, count) }\n    }\n\n    val favCloudCount: Int\n        get() = sSettingsPre.getInt(KEY_FAV_CLOUD, DEFAULT_FAV_COUNT)\n    fun putFavCloudCount(count: Int) {\n        sSettingsPre.edit { putInt(KEY_FAV_CLOUD, count) }\n    }\n\n    val recentFavCat: Int\n        get() = getInt(KEY_RECENT_FAV_CAT, DEFAULT_RECENT_FAV_CAT)\n    fun putRecentFavCat(value: Int) {\n        putInt(KEY_RECENT_FAV_CAT, value)\n    }\n\n    val defaultFavSlot: Int\n        get() = getInt(KEY_DEFAULT_FAV_SLOT, DEFAULT_DEFAULT_FAV_SLOT)\n    fun putDefaultFavSlot(value: Int) {\n        putInt(KEY_DEFAULT_FAV_SLOT, value)\n    }\n\n    val neverAddFavNotes: Boolean\n        get() = getBoolean(KEY_NEVER_ADD_FAV_NOTES, DEFAULT_NEVER_ADD_FAV_NOTES)\n    fun putNeverAddFavNotes(value: Boolean) {\n        putBoolean(KEY_NEVER_ADD_FAV_NOTES, value)\n    }\n\n    val guideGallery: Boolean\n        get() = getBoolean(KEY_GUIDE_GALLERY, DEFAULT_GUIDE_GALLERY)\n    fun putGuideGallery(value: Boolean) {\n        putBoolean(KEY_GUIDE_GALLERY, value)\n    }\n\n    val selectSite: Boolean\n        get() = getBoolean(KEY_SELECT_SITE, DEFAULT_SELECT_SITE)\n    fun putSelectSite(value: Boolean) {\n        putBoolean(KEY_SELECT_SITE, value)\n    }\n\n    val needSignIn: Boolean\n        get() = getBoolean(KEY_NEED_SIGN_IN, DEFAULT_NEED_SIGN_IN)\n    fun putNeedSignIn(value: Boolean) {\n        putBoolean(KEY_NEED_SIGN_IN, value)\n    }\n\n    val displayName: String?\n        get() = getString(KEY_DISPLAY_NAME, DEFAULT_DISPLAY_NAME)\n    fun putDisplayName(value: String?) {\n        putString(KEY_DISPLAY_NAME, value)\n    }\n\n    val avatar: String?\n        get() = getString(KEY_AVATAR, DEFAULT_AVATAR)\n    fun putAvatar(value: String?) {\n        putString(KEY_AVATAR, value)\n    }\n\n    val qSSaveProgress: Boolean\n        get() = getBoolean(KEY_QS_SAVE_PROGRESS, DEFAULT_QS_SAVE_PROGRESS)\n    fun putQSSaveProgress(value: Boolean) {\n        putBoolean(KEY_QS_SAVE_PROGRESS, value)\n    }\n\n    val hasDefaultDownloadLabel: Boolean\n        get() = getBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, DEFAULT_HAS_DOWNLOAD_LABEL)\n    fun putHasDefaultDownloadLabel(has: Boolean) {\n        putBoolean(KEY_HAS_DEFAULT_DOWNLOAD_LABEL, has)\n    }\n\n    val defaultDownloadLabel: String?\n        get() = getString(KEY_DEFAULT_DOWNLOAD_LABEL, DEFAULT_DOWNLOAD_LABEL)\n    fun putDefaultDownloadLabel(value: String?) {\n        putString(KEY_DEFAULT_DOWNLOAD_LABEL, value)\n    }\n\n    val recentDownloadLabel: String?\n        get() = getString(KEY_RECENT_DOWNLOAD_LABEL, DEFAULT_RECENT_DOWNLOAD_LABEL)\n    fun putRecentDownloadLabel(value: String?) {\n        putString(KEY_RECENT_DOWNLOAD_LABEL, value)\n    }\n\n    val defaultSortingMethod: Int\n        get() = getInt(KEY_DEFAULT_SORTING_METHOD, DEFAULT_SORTING_METHOD)\n    fun putDefaultSortingMethod(value: Int) {\n        putInt(KEY_DEFAULT_SORTING_METHOD, value)\n    }\n\n    val defaultTopList: String?\n        get() = getString(KEY_DEFAULT_TOP_LIST, DEFAULT_TOP_LIST)\n    fun putDefaultTopList(value: String?) {\n        putString(KEY_DEFAULT_TOP_LIST, value)\n    }\n\n    val removeImageFiles: Boolean\n        get() = getBoolean(KEY_REMOVE_IMAGE_FILES, DEFAULT_REMOVE_IMAGE_FILES)\n    fun putRemoveImageFiles(value: Boolean) {\n        putBoolean(KEY_REMOVE_IMAGE_FILES, value)\n    }\n\n    val clipboardTextHashCode: Int\n        get() = getInt(KEY_CLIPBOARD_TEXT_HASH_CODE, DEFAULT_CLIPBOARD_TEXT_HASH_CODE)\n    fun putClipboardTextHashCode(value: Int) {\n        putInt(KEY_CLIPBOARD_TEXT_HASH_CODE, value)\n    }\n\n    val archivePasswds: Set<String>?\n        get() = getStringSet(KEY_ARCHIVE_PASSWDS)\n    fun putPasswdToArchivePasswds(value: String) {\n        putStringToStringSet(KEY_ARCHIVE_PASSWDS, value)\n    }\n\n    val notificationRequired: Boolean\n        get() = getBoolean(KEY_NOTIFICATION_REQUIRED, false)\n    fun putNotificationRequired() {\n        putBoolean(KEY_NOTIFICATION_REQUIRED, true)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/UrlOpener.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer\n\nimport android.content.ActivityNotFoundException\nimport android.content.Context\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.widget.Toast\nimport androidx.browser.customtabs.CustomTabColorSchemeParams\nimport androidx.browser.customtabs.CustomTabsIntent\nimport androidx.core.net.toUri\nimport com.hippo.ehviewer.client.EhUrlOpener.parseUrl\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.client.parser.GalleryPageUrlParser\nimport com.hippo.ehviewer.ui.GalleryActivity\nimport com.hippo.ehviewer.ui.MainActivity\nimport com.hippo.scene.StageActivity\nimport rikka.core.res.resolveColor\n\nobject UrlOpener {\n    fun openUrl(\n        context: Context,\n        url: String?,\n        ehUrl: Boolean,\n        galleryDetail: GalleryDetail? = null,\n    ) {\n        if (url.isNullOrEmpty()) {\n            return\n        }\n        var intent: Intent\n        val uri = url.toUri()\n        if (ehUrl) {\n            galleryDetail?.let {\n                val result = GalleryPageUrlParser.parse(url)\n                if (result != null) {\n                    if (result.gid == it.gid) {\n                        intent = Intent(context, GalleryActivity::class.java)\n                        intent.action = GalleryActivity.ACTION_EH\n                        intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, it)\n                        intent.putExtra(GalleryActivity.KEY_PAGE, result.page)\n                        context.startActivity(intent)\n                        return\n                    }\n                } else if (url.startsWith(\"#c\")) {\n                    try {\n                        intent = Intent(context, GalleryActivity::class.java)\n                        intent.action = GalleryActivity.ACTION_EH\n                        intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, it)\n                        intent.putExtra(GalleryActivity.KEY_PAGE, url.replace(\"#c\", \"\").toInt() - 1)\n                        context.startActivity(intent)\n                        return\n                    } catch (_: NumberFormatException) {\n                    }\n                }\n            }\n            parseUrl(url)?.let {\n                intent = Intent(context, MainActivity::class.java)\n                intent.action = StageActivity.ACTION_START_SCENE\n                intent.putExtra(StageActivity.KEY_SCENE_NAME, it.clazz.name)\n                intent.putExtra(StageActivity.KEY_SCENE_ARGS, it.args)\n                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK\n                context.startActivity(intent)\n                return\n            }\n        }\n        val isNight = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0\n        val customTabsIntent = CustomTabsIntent.Builder()\n        customTabsIntent.setShowTitle(true)\n        val params = CustomTabColorSchemeParams.Builder()\n            .setToolbarColor(context.theme.resolveColor(R.attr.toolbarColor))\n            .build()\n        customTabsIntent.setDefaultColorSchemeParams(params)\n        customTabsIntent.setColorScheme(if (isNight) CustomTabsIntent.COLOR_SCHEME_DARK else CustomTabsIntent.COLOR_SCHEME_LIGHT)\n        try {\n            customTabsIntent.build().launchUrl(context, uri)\n        } catch (_: ActivityNotFoundException) {\n            Toast.makeText(context, R.string.no_browser_installed, Toast.LENGTH_LONG).show()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/WindowInsetsAnimationHelper.java",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n\npackage com.hippo.ehviewer;\n\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.view.WindowInsetsAnimationCompat;\nimport androidx.core.view.WindowInsetsCompat;\n\nimport com.hippo.yorozuya.MathUtils;\n\nimport java.util.HashMap;\nimport java.util.List;\n\npublic class WindowInsetsAnimationHelper extends WindowInsetsAnimationCompat.Callback {\n    private final View[] views;\n    private final HashMap<View, Integer> startPaddings = new HashMap<>();\n    private final HashMap<View, Integer> endPaddings = new HashMap<>();\n    WindowInsetsAnimationCompat animation;\n\n    public WindowInsetsAnimationHelper(int dispatchMode, View... views) {\n        super(dispatchMode);\n        this.views = views;\n    }\n\n    @Override\n    public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) {\n        super.onPrepare(animation);\n        this.animation = animation;\n        for (View view : views) {\n            if (view == null) {\n                continue;\n            }\n            startPaddings.put(view, view.getPaddingBottom());\n        }\n    }\n\n    @NonNull\n    @Override\n    public WindowInsetsAnimationCompat.BoundsCompat onStart(@NonNull WindowInsetsAnimationCompat animation, @NonNull WindowInsetsAnimationCompat.BoundsCompat bounds) {\n        this.animation = animation;\n        for (View view : views) {\n            if (view == null) {\n                continue;\n            }\n            endPaddings.put(view, view.getPaddingBottom());\n            Integer padding = startPaddings.get(view);\n            int startPadding = (padding != null) ? padding : 0;\n            view.setTranslationY(-(startPadding - view.getPaddingBottom()));\n        }\n        return bounds;\n    }\n\n    @NonNull\n    @Override\n    public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets, @NonNull List<WindowInsetsAnimationCompat> runningAnimations) {\n        if (animation == null) {\n            return insets;\n        }\n        WindowInsetsAnimationCompat imeAnimation = null;\n        for (WindowInsetsAnimationCompat animation : runningAnimations) {\n            if ((animation.getTypeMask() & WindowInsetsCompat.Type.ime()) != 0) {\n                imeAnimation = animation;\n                break;\n            }\n        }\n        if (imeAnimation != null) {\n            for (View view : views) {\n                if (view == null) {\n                    continue;\n                }\n                Integer padding1 = startPaddings.get(view);\n                int startPadding = (padding1 != null) ? padding1 : 0;\n                Integer padding2 = endPaddings.get(view);\n                int endPadding = (padding2 != null) ? padding2 : 0;\n                view.setTranslationY(MathUtils.lerp(endPadding - startPadding, 0, animation.getInterpolatedFraction()));\n            }\n        }\n        return insets;\n    }\n\n    @Override\n    public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {\n        super.onEnd(animation);\n        startPaddings.clear();\n        endPaddings.clear();\n        this.animation = null;\n        for (View view : views) {\n            if (view == null) {\n                continue;\n            }\n            view.setTranslationY(0);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhCacheKeyFactory.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport com.hippo.ehviewer.client.data.AbstractGalleryInfo\n\n// E-Hentai Large Preview (v1 Cover): https://ehgt.org/**.jpg\n// ExHentai Large Preview (v1 Cover): https://s.exhentai.org/t/**.jpg\n// E-Hentai v2 Cover: https://ehgt.org/w/**.webp\n// ExHentai v2 Cover: https://s.exhentai.org/w/**.webp\n// Normal Preview (v1 v2): https://*.hath.network/c(m|1|2)/[timed token]/[gid]-[index].(jpg|webp)\n// Large Preview (v2): https://*.hath.network/[timed token]/**.webp\nconst val URL_PREFIX_THUMB_E = \"https://ehgt.org/\"\nconst val URL_PREFIX_THUMB_EX = \"https://s.exhentai.org/\"\nprivate const val URL_PREFIX_V1_THUMB_EX = URL_PREFIX_THUMB_EX + \"t/\"\nprivate const val URL_PREFIX_V1_THUMB_EX_OLD = \"https://exhentai.org/t/\"\nprivate val NormalPreviewKeyRegex = Regex(\"/(c[12m])/[^/]+/(\\\\d+-\\\\d+)\")\n\nfun getImageKey(gid: Long, index: Int) = \"image:$gid:$index\"\n\nfun getThumbKey(gid: Long): String = \"preview:large:$gid:0\"\n\nfun getLargePreviewKey(gid: Long, index: Int) = \"preview:large:$gid:$index\"\n\nfun getNormalPreviewKey(url: String) = NormalPreviewKeyRegex.find(url)?.let { \"preview:normal:${it.groupValues[1]}:${it.groupValues[2]}\" } ?: url\n\nval String.isNormalPreviewKey\n    get() = startsWith(\"preview:normal:\")\n\nval String.thumbUrl\n    get() = removePrefix(URL_PREFIX_THUMB_E)\n        .removePrefix(URL_PREFIX_V1_THUMB_EX_OLD)\n        .removePrefix(URL_PREFIX_V1_THUMB_EX)\n        .removePrefix(URL_PREFIX_THUMB_EX).let {\n            if (it.startsWith(\"https:\")) {\n                it\n            } else {\n                if (EhUtils.isExHentai) {\n                    if (it.endsWith(\"webp\")) URL_PREFIX_THUMB_EX else URL_PREFIX_V1_THUMB_EX\n                } else {\n                    URL_PREFIX_THUMB_E\n                } + it\n            }\n        }\n\nval AbstractGalleryInfo.thumbUrl\n    get() = thumb?.thumbUrl\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhClient.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport java.io.File\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineScope\n\nobject EhClient {\n    internal fun enqueue(request: EhRequest, scope: CoroutineScope) {\n        check(!request.isActive) // Abort if attempt to execute an active request\n        request.job = scope.launchIO {\n            val callback: Callback<Any?>? = request.callback\n            try {\n                val result: Any? = request.run {\n                    if (args == null) {\n                        execute(method)\n                    } else {\n                        execute(method, *args!!)\n                    }\n                }\n                withUIContext { callback?.onSuccess(result) }\n            } catch (e: Exception) {\n                if (e is CancellationException) {\n                    throw e // Don't catch coroutine CancellationException\n                }\n                e.printStackTrace()\n                withUIContext { callback?.onFailure(e) }\n            }\n        }\n    }\n\n    suspend fun execute(method: Int, vararg params: Any?): Any? = when (method) {\n        METHOD_GET_GALLERY_LIST -> EhEngine.getGalleryList(\n            params[0] as String,\n        )\n        METHOD_GET_GALLERY_DETAIL -> EhEngine.getGalleryDetail(\n            params[0] as String,\n        )\n        METHOD_GET_PREVIEW_SET -> EhEngine.getPreviewSet(\n            params[0] as String,\n        )\n        METHOD_GET_RATE_GALLERY -> EhEngine.rateGallery(\n            params[0] as Long,\n            params[1] as String?,\n            params[2] as Long,\n            params[3] as String?,\n            params[4] as Float,\n        )\n        METHOD_GET_COMMENT_GALLERY -> EhEngine.commentGallery(\n            params[0] as String?,\n            params[1] as String,\n            params[2] as String?,\n        )\n        METHOD_GET_GALLERY_TOKEN -> EhEngine.getGalleryToken(\n            params[0] as Long,\n            params[1] as String?,\n            params[2] as Int,\n        )\n        METHOD_GET_FAVORITES -> EhEngine.getFavorites(\n            params[0] as String,\n        )\n        METHOD_ADD_FAVORITES -> EhEngine.addFavorites(\n            params[0] as Long,\n            params[1] as String?,\n            params[2] as Int,\n            params[3] as String?,\n        )\n        METHOD_ADD_FAVORITES_RANGE ->\n            @Suppress(\"UNCHECKED_CAST\")\n            EhEngine.addFavoritesRange(\n                params[0] as LongArray,\n                params[1] as Array<String?>,\n                params[2] as Int,\n            )\n        METHOD_MODIFY_FAVORITES -> EhEngine.modifyFavorites(\n            params[0] as String,\n            params[1] as LongArray,\n            params[2] as Int,\n        )\n        METHOD_GET_TORRENT_LIST -> EhEngine.getTorrentList(\n            params[0] as String,\n            params[1] as Long,\n            params[2] as String?,\n        )\n        METHOD_VOTE_COMMENT -> EhEngine.voteComment(\n            params[0] as Long,\n            params[1] as String?,\n            params[2] as Long,\n            params[3] as String?,\n            params[4] as Long,\n            params[5] as Int,\n        )\n        METHOD_IMAGE_SEARCH -> EhEngine.imageSearch(\n            params[0] as File,\n            params[1] as Boolean,\n            params[2] as Boolean,\n            params[3] as Boolean,\n        )\n        METHOD_ARCHIVE_LIST -> EhEngine.getArchiveList(\n            params[0] as String,\n            params[1] as Long,\n            params[2] as String?,\n        )\n        METHOD_DOWNLOAD_ARCHIVE -> EhEngine.downloadArchive(\n            params[0] as Long,\n            params[1] as String?,\n            params[2] as String?,\n            params[3] as Boolean,\n        )\n        METHOD_VOTE_TAG -> EhEngine.voteTag(\n            params[0] as Long,\n            params[1] as String?,\n            params[2] as Long,\n            params[3] as String?,\n            params[4] as String?,\n            params[5] as Int,\n        )\n        else -> throw IllegalStateException(\"Can't detect method $method\")\n    }\n\n    interface Callback<E> {\n        fun onSuccess(result: E)\n        fun onFailure(e: Exception)\n        fun onCancel()\n    }\n\n    const val METHOD_GET_GALLERY_LIST = 1\n    const val METHOD_GET_GALLERY_DETAIL = 3\n    const val METHOD_GET_PREVIEW_SET = 4\n    const val METHOD_GET_RATE_GALLERY = 5\n    const val METHOD_GET_COMMENT_GALLERY = 6\n    const val METHOD_GET_GALLERY_TOKEN = 7\n    const val METHOD_GET_FAVORITES = 8\n    const val METHOD_ADD_FAVORITES = 9\n    const val METHOD_ADD_FAVORITES_RANGE = 10\n    const val METHOD_MODIFY_FAVORITES = 11\n    const val METHOD_GET_TORRENT_LIST = 12\n    const val METHOD_VOTE_COMMENT = 15\n    const val METHOD_IMAGE_SEARCH = 16\n    const val METHOD_ARCHIVE_LIST = 17\n    const val METHOD_DOWNLOAD_ARCHIVE = 18\n    const val METHOD_VOTE_TAG = 19\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport android.webkit.CookieManager\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.network.CookieDatabase\nimport com.hippo.network.CookieSet\nimport com.hippo.util.launchIO\nimport java.util.Collections\nimport java.util.regex.Pattern\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport okhttp3.Cookie\nimport okhttp3.CookieJar\nimport okhttp3.HttpUrl\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\n@OptIn(DelicateCoroutinesApi::class)\nobject EhCookieStore : CookieJar {\n    private val cookieManager = CookieManager.getInstance()\n    private val db: CookieDatabase = CookieDatabase(EhApplication.application, \"okhttp3-cookie.db\")\n    private val map: MutableMap<String, CookieSet> = db.allCookies\n    private val updateLock = Mutex()\n    const val KEY_CLOUDFLARE = \"cf_clearance\"\n    const val KEY_HATH_PERKS = \"hath_perks\"\n    const val KEY_IPB_MEMBER_ID = \"ipb_member_id\"\n    const val KEY_IPB_PASS_HASH = \"ipb_pass_hash\"\n    const val KEY_IGNEOUS = \"igneous\"\n    const val KEY_QUOTA = \"iq\"\n    const val KEY_SETTINGS_PROFILE = \"sp\"\n    const val KEY_STAR = \"star\"\n    private const val KEY_UTMP = \"__utmp\"\n    private const val KEY_CONTENT_WARNING = \"nw\"\n    private const val CONTENT_WARNING_NOT_SHOW = \"1\"\n    private val sTipsCookie: Cookie = Cookie.Builder()\n        .name(KEY_CONTENT_WARNING)\n        .value(CONTENT_WARNING_NOT_SHOW)\n        .domain(EhUrl.DOMAIN_E)\n        .path(\"/\")\n        .expiresAt(Long.MAX_VALUE)\n        .build()\n\n    fun hasSignedIn(): Boolean {\n        val url = EhUrl.HOST_E.toHttpUrl()\n        return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)\n    }\n\n    suspend fun copyCookie(domain: String, newDomain: String, name: String, path: String = \"/\") {\n        val cookie = map[domain]?.get(name, domain, path)\n        cookie?.let { addCookie(newCookie(it, newDomain)) }\n    }\n\n    suspend fun deleteCookie(url: HttpUrl, name: String) {\n        val deletedCookie = Cookie.Builder()\n            .name(name)\n            .value(\"deleted\")\n            .domain(url.host)\n            .expiresAt(0)\n            .build()\n        addCookie(deletedCookie)\n    }\n\n    suspend fun addCookie(cookie: Cookie) {\n        updateLock.withLock {\n            // For cookie database\n            var toAdd: Cookie? = null\n            var toUpdate: Cookie? = null\n            var toRemove: Cookie? = null\n            var set = map[cookie.domain]\n            if (set == null) {\n                set = CookieSet()\n                map[cookie.domain] = set\n            }\n            if (cookie.expiresAt <= System.currentTimeMillis()) {\n                toRemove = set.remove(cookie)\n                // If the cookie is not persistent, it's not in database\n                if (toRemove != null && !toRemove.persistent) {\n                    toRemove = null\n                }\n            } else {\n                toAdd = cookie\n                toUpdate = set.add(cookie)\n                // If the cookie is not persistent, it's not in database\n                if (!toAdd.persistent) toAdd = null\n                if (toUpdate != null && !toUpdate.persistent) toUpdate = null\n                // Remove the cookie if it updates to null\n                if (toAdd == null && toUpdate != null) {\n                    toRemove = toUpdate\n                    toUpdate = null\n                }\n            }\n            if (toRemove != null) {\n                db.remove(toRemove)\n            }\n            if (toAdd != null) {\n                if (toUpdate != null) {\n                    db.update(toUpdate, toAdd)\n                } else {\n                    db.add(toAdd)\n                }\n            }\n        }\n    }\n\n    fun getCookieHeader(url: HttpUrl): String {\n        val cookies = getCookies(url)\n        val cookieHeader = StringBuilder()\n        for (i in cookies.indices) {\n            if (i > 0) {\n                cookieHeader.append(\"; \")\n            }\n            val cookie = cookies[i]\n            cookieHeader.append(cookie.name).append('=').append(cookie.value)\n        }\n        return cookieHeader.toString()\n    }\n\n    fun getCookieValue(url: HttpUrl, name: String): String? {\n        getCookies(url).forEach {\n            if (it.name == name) return it.value\n        }\n        return null\n    }\n\n    @Synchronized\n    fun getCookies(url: HttpUrl): List<Cookie> {\n        val accepted: MutableList<Cookie> = ArrayList()\n        val expired: MutableList<Cookie> = ArrayList()\n        for ((domain, cookieSet) in map) {\n            if (domainMatch(url, domain)) {\n                cookieSet[url, accepted, expired]\n            }\n        }\n        for (cookie in expired) {\n            if (cookie.persistent) {\n                launchIO {\n                    db.remove(cookie)\n                }\n            }\n        }\n\n        // RFC 6265 Section-5.4 step 2, sort the cookie-list\n        // Cookies with longer paths are listed before cookies with shorter paths.\n        // Ignore creation-time, we don't store them.\n        accepted.sortWith { o1: Cookie, o2: Cookie -> o2.path.length - o1.path.length }\n        return accepted\n    }\n\n    /**\n     * Remove all cookies in this `CookieRepository`.\n     */\n    suspend fun clear() {\n        updateLock.withLock {\n            map.clear()\n            db.clear()\n        }\n    }\n\n    fun newCookie(\n        cookie: Cookie,\n        newDomain: String,\n        forcePersistent: Boolean = false,\n        forceLongLive: Boolean = false,\n        forceNotHostOnly: Boolean = false,\n    ): Cookie {\n        val builder = Cookie.Builder()\n        builder.name(cookie.name)\n        builder.value(cookie.value)\n        if (forceLongLive) {\n            builder.expiresAt(Long.MAX_VALUE)\n        } else if (cookie.persistent) {\n            builder.expiresAt(cookie.expiresAt)\n        } else if (forcePersistent) {\n            builder.expiresAt(Long.MAX_VALUE)\n        }\n        if (cookie.hostOnly && !forceNotHostOnly) {\n            builder.hostOnlyDomain(newDomain)\n        } else {\n            builder.domain(newDomain)\n        }\n        builder.path(cookie.path)\n        if (cookie.secure) {\n            builder.secure()\n        }\n        if (cookie.httpOnly) {\n            builder.httpOnly()\n        }\n        return builder.build()\n    }\n\n    /**\n     * Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation\n     * of Android's private InetAddress#isNumeric API.\n     *\n     *\n     * This matches IPv6 addresses as a hex string containing at least one colon, and possibly\n     * including dots after the first colon. It matches IPv4 addresses as strings containing only\n     * decimal digits and dots. This pattern matches strings like \"a:.23\" and \"54\" that are neither IP\n     * addresses nor hostnames; they will be verified as IP addresses (which is a more strict\n     * verification).\n     */\n    private val VERIFY_AS_IP_ADDRESS =\n        Pattern.compile(\"([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\\\d.]+)\")\n\n    /**\n     * Returns true if `host` is not a host name and might be an IP address.\n     */\n    private fun verifyAsIpAddress(host: String): Boolean = VERIFY_AS_IP_ADDRESS.matcher(host).matches()\n\n    // okhttp3.Cookie.domainMatch(HttpUrl, String)\n    private fun domainMatch(url: HttpUrl, domain: String?): Boolean {\n        val urlHost = url.host\n        return if (urlHost == domain) {\n            true // As in 'example.com' matching 'example.com'.\n        } else {\n            urlHost.endsWith(domain!!) &&\n                urlHost[urlHost.length - domain.length - 1] == '.' &&\n                !verifyAsIpAddress(\n                    urlHost,\n                )\n        }\n        // As in 'example.com' matching 'www.example.com'.\n    }\n\n    private fun contains(url: HttpUrl, name: String?): Boolean {\n        for (cookie in getCookies(url)) {\n            if (cookie.name == name) {\n                return true\n            }\n        }\n        return false\n    }\n\n    fun loadForWebView(url: String, filter: (Cookie) -> Boolean) {\n        cookieManager.removeAllCookies(null)\n        getCookies(url.toHttpUrl()).forEach {\n            if (filter(it)) {\n                cookieManager.setCookie(url, it.toString())\n            }\n        }\n    }\n\n    fun saveFromWebView(url: String, filter: (Cookie) -> Boolean): Boolean {\n        val cookies = cookieManager.getCookie(url) ?: return false\n        var saved = false\n        cookies.split(';').forEach { header ->\n            Cookie.parse(url.toHttpUrl(), header.trim())?.let {\n                if (filter(it)) {\n                    val persistentCookie = Cookie.Builder()\n                        .name(it.name)\n                        .value(it.value)\n                        .domain(it.domain)\n                        .path(it.path)\n                        .expiresAt(System.currentTimeMillis() + 365L * 24 * 60 * 60 * 1000)\n                        .apply {\n                            if (it.secure) secure()\n                            if (it.httpOnly) httpOnly()\n                            if (it.hostOnly) hostOnlyDomain(it.domain)\n                        }\n                        .build()\n                    launchIO { addCookie(persistentCookie) }\n                    saved = true\n                }\n            }\n        }\n        return saved\n    }\n\n    override fun loadForRequest(url: HttpUrl): List<Cookie> {\n        val cookies = getCookies(url)\n        val checkTips = domainMatch(url, EhUrl.DOMAIN_E)\n        return if (checkTips) {\n            val result: MutableList<Cookie> = ArrayList(cookies.size + 1)\n            // Add all but skip some\n            for (cookie in cookies) {\n                val name = cookie.name\n                if (KEY_CONTENT_WARNING == name) {\n                    continue\n                }\n                result.add(cookie)\n            }\n            // Add some\n            result.add(sTipsCookie)\n            Collections.unmodifiableList(result)\n        } else {\n            cookies\n        }\n    }\n\n    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {\n        for (cookie in cookies) {\n            // See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873\n            if (cookie.name != KEY_UTMP) {\n                launchIO { addCookie(cookie) }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhEngine.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport android.util.Log\nimport com.hippo.ehviewer.AppConfig\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.EhApplication.Companion.application\nimport com.hippo.ehviewer.GetText\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.data.GalleryCommentList\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryTagGroup\nimport com.hippo.ehviewer.client.data.PreviewSet\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.client.exception.InsufficientFundsException\nimport com.hippo.ehviewer.client.exception.NotLoggedInException\nimport com.hippo.ehviewer.client.exception.ParseException\nimport com.hippo.ehviewer.client.parser.ArchiveParser\nimport com.hippo.ehviewer.client.parser.EventPaneParser\nimport com.hippo.ehviewer.client.parser.FavoritesParser\nimport com.hippo.ehviewer.client.parser.ForumsParser\nimport com.hippo.ehviewer.client.parser.GalleryApiParser\nimport com.hippo.ehviewer.client.parser.GalleryDetailParser\nimport com.hippo.ehviewer.client.parser.GalleryListParser\nimport com.hippo.ehviewer.client.parser.GalleryMultiPageViewerParser\nimport com.hippo.ehviewer.client.parser.GalleryNotAvailableParser\nimport com.hippo.ehviewer.client.parser.GalleryPageApiParser\nimport com.hippo.ehviewer.client.parser.GalleryPageParser\nimport com.hippo.ehviewer.client.parser.GalleryTokenApiParser\nimport com.hippo.ehviewer.client.parser.HomeParser\nimport com.hippo.ehviewer.client.parser.ProfileParser\nimport com.hippo.ehviewer.client.parser.RateGalleryParser\nimport com.hippo.ehviewer.client.parser.SignInParser\nimport com.hippo.ehviewer.client.parser.TorrentParser\nimport com.hippo.ehviewer.client.parser.UserConfigParser\nimport com.hippo.ehviewer.client.parser.VoteCommentParser\nimport com.hippo.ehviewer.client.parser.VoteTagParser\nimport com.hippo.network.StatusCodeException\nimport java.io.File\nimport kotlin.math.ceil\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.delay\nimport okhttp3.FormBody\nimport okhttp3.Headers\nimport okhttp3.MediaType\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.MultipartBody\nimport okhttp3.Request\nimport okhttp3.RequestBody\nimport okhttp3.RequestBody.Companion.asRequestBody\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport okhttp3.coroutines.executeAsync\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport org.jsoup.Jsoup\n\nprivate val okHttpClient = EhApplication.okHttpClient\nprivate val MEDIA_TYPE_JSON: MediaType = \"application/json; charset=utf-8\".toMediaType()\nprivate const val TAG = \"EhEngine\"\nprivate const val MAX_REQUEST_SIZE = 25\nprivate val MEDIA_TYPE_JPEG: MediaType = \"image/jpeg\".toMediaType()\nprivate var sEhFilter = EhFilter\n\nprivate fun rethrowExactly(code: Int, body: String, e: Throwable) {\n    // Check sad panda (without panda)\n    if (body.isEmpty()) {\n        if (EhUtils.isExHentai) {\n            throw EhException(\"Sad Panda\\n(without panda)\")\n        } else {\n            throw EhException(\"IP banned\")\n        }\n    }\n\n    // Check 503\n    if (body.contains(\"Backend fetch failed\")) {\n        throw EhException(\"Error 503\\nBackend fetch failed\")\n    }\n\n    // Check Gallery Not Available\n    if (body.contains(\"Gallery Not Available - \")) {\n        val error = GalleryNotAvailableParser.parse(body)\n        if (!error.isNullOrBlank()) {\n            throw EhException(error)\n        }\n    }\n\n    // Check bad response code\n    if (code >= 400) {\n        if (Settings.saveParseErrorBody && e is ParseException && body.isNotEmpty()) {\n            AppConfig.saveParseErrorBody(e, body)\n        }\n        throw StatusCodeException(code)\n    }\n\n    if (e is ParseException) {\n        if (!body.contains(\"<\")) {\n            throw EhException(body)\n        } else {\n            if (Settings.saveParseErrorBody) {\n                AppConfig.saveParseErrorBody(e, body)\n            }\n            throw EhException(GetText.getString(R.string.error_parse_error), e)\n        }\n    }\n\n    // We can't translate it, rethrow it anyway\n    throw e\n}\n\nprivate suspend inline fun <T> Request.Builder.executeAndParsingWith(block: String.() -> T): T = okHttpClient.newCall(this.build()).executeAsync().use { response ->\n    val body = response.body.string()\n    runCatching {\n        block(body)\n    }.onFailure {\n        rethrowExactly(response.code, body, it)\n    }.getOrThrow()\n}\n\nobject EhEngine {\n    suspend fun getOriginalImageUrl(url: String, referer: String?): String {\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).build().executeNoRedirect {\n            header(\"Location\")?.apply {\n                if (contains(\"bounce_login\")) {\n                    throw NotLoggedInException()\n                }\n            } ?: throw InsufficientFundsException()\n        }\n    }\n\n    suspend fun signIn(username: String, password: String): String {\n        val referer = \"https://forums.e-hentai.org/index.php?act=Login&CODE=00\"\n        val builder = FormBody.Builder()\n            .add(\"referer\", referer)\n            .add(\"b\", \"\")\n            .add(\"bt\", \"\")\n            .add(\"UserName\", username)\n            .add(\"PassWord\", password)\n            .add(\"CookieDate\", \"1\")\n        val url = EhUrl.API_SIGN_IN\n        val origin = \"https://forums.e-hentai.org\"\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(builder.build())\n            .executeAndParsingWith(SignInParser::parse)\n    }\n\n    private suspend fun fillGalleryList(\n        list: MutableList<GalleryInfo>,\n        url: String,\n        filter: Boolean,\n    ) {\n        // Filter title and uploader\n        if (filter) {\n            list.removeAll { !sEhFilter.filterTitle(it) || !sEhFilter.filterUploader(it) }\n        }\n        var hasTags = false\n        var hasPages = false\n        var hasRated = false\n        for (gi in list) {\n            if (gi.simpleTags != null) {\n                hasTags = true\n            }\n            if (gi.pages != 0) {\n                hasPages = true\n            }\n            if (gi.rated) {\n                hasRated = true\n            }\n        }\n        val needApi = filter && sEhFilter.needTags() && !hasTags || Settings.showGalleryPages && !hasPages || hasRated\n        if (needApi) {\n            fillGalleryListByApi(list, url)\n        }\n\n        // Filter tag\n        if (filter) {\n            // Thumbnail mode need filter uploader again\n            list.removeAll {\n                !sEhFilter.filterUploader(it) || !sEhFilter.filterTag(it) || !sEhFilter.filterTagNamespace(it)\n            }\n        }\n    }\n\n    suspend fun getGalleryList(url: String): GalleryListParser.Result {\n        val referer = EhUrl.referer\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith(GalleryListParser::parse)\n            .apply { fillGalleryList(galleryInfoList, url, true) }\n    }\n\n    suspend fun fillGalleryListByApi(\n        galleryInfoList: List<GalleryInfo>,\n        referer: String,\n    ): List<GalleryInfo> {\n        val requestItems: MutableList<GalleryInfo> = ArrayList(MAX_REQUEST_SIZE)\n        var i = 0\n        val size = galleryInfoList.size\n        while (i < size) {\n            requestItems.add(galleryInfoList[i])\n            if (requestItems.size == MAX_REQUEST_SIZE || i == size - 1) {\n                doFillGalleryListByApi(requestItems, referer)\n                requestItems.clear()\n            }\n            i++\n        }\n        return galleryInfoList\n    }\n\n    private suspend fun doFillGalleryListByApi(\n        galleryInfoList: List<GalleryInfo>,\n        referer: String,\n    ) {\n        val json = JSONObject()\n        json.put(\"method\", \"gdata\")\n        val ja = JSONArray()\n        var i = 0\n        val size = galleryInfoList.size\n        while (i < size) {\n            val gi = galleryInfoList[i]\n            val g = JSONArray()\n            g.put(gi.gid)\n            g.put(gi.token)\n            ja.put(g)\n            i++\n        }\n        json.put(\"gidlist\", ja)\n        json.put(\"namespace\", 1)\n        val url = EhUrl.apiUrl\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(json.toString().toRequestBody(MEDIA_TYPE_JSON))\n            .executeAndParsingWith {\n                GalleryApiParser.parse(this, galleryInfoList)\n            }\n    }\n\n    suspend fun getGalleryDetail(url: String): GalleryDetail {\n        val referer = EhUrl.referer\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith {\n            EventPaneParser.parse(this)?.let {\n                application.showEventPane(it)\n            }\n            GalleryDetailParser.parse(this)\n        }\n    }\n\n    suspend fun getPreviewSet(url: String): Pair<PreviewSet, Int> {\n        val referer = EhUrl.referer\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith {\n            GalleryDetailParser.parsePreviewSet(this) to GalleryDetailParser.parsePreviewPages(this)\n        }\n    }\n\n    @Throws(Throwable::class)\n    suspend fun rateGallery(\n        apiUid: Long,\n        apiKey: String?,\n        gid: Long,\n        token: String?,\n        rating: Float,\n    ): RateGalleryParser.Result {\n        val json = JSONObject()\n        json.put(\"method\", \"rategallery\")\n        json.put(\"apiuid\", apiUid)\n        json.put(\"apikey\", apiKey)\n        json.put(\"gid\", gid)\n        json.put(\"token\", token)\n        json.put(\"rating\", ceil((rating * 2).toDouble()).toInt())\n        val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)\n        val url = EhUrl.apiUrl\n        val referer = EhUrl.getGalleryDetailUrl(gid, token)\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(requestBody)\n            .executeAndParsingWith(RateGalleryParser::parse)\n    }\n\n    suspend fun commentGallery(\n        url: String?,\n        comment: String,\n        id: String?,\n    ): GalleryCommentList {\n        val builder = FormBody.Builder()\n        if (id == null) {\n            builder.add(\"commenttext_new\", comment)\n        } else {\n            builder.add(\"commenttext_edit\", comment)\n            builder.add(\"edit_comment\", id)\n        }\n        val origin = EhUrl.origin\n        Log.d(TAG, url!!)\n        return EhRequestBuilder(url, url, origin)\n            .post(builder.build())\n            .executeAndParsingWith {\n                val document = Jsoup.parse(this)\n                val elements = document.select(\"#chd + p\")\n                if (elements.isNotEmpty()) {\n                    throw EhException(elements[0].text())\n                }\n                GalleryDetailParser.parseComments(document)\n            }\n    }\n\n    suspend fun getGalleryToken(\n        gid: Long,\n        gtoken: String?,\n        page: Int,\n    ): String {\n        val json = JSONObject()\n            .put(\"method\", \"gtoken\")\n            .put(\n                \"pagelist\",\n                JSONArray().put(\n                    JSONArray().put(gid).put(gtoken).put(page + 1),\n                ),\n            )\n        val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)\n        val url = EhUrl.apiUrl\n        val referer = EhUrl.referer\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(requestBody)\n            .executeAndParsingWith(GalleryTokenApiParser::parse)\n    }\n\n    suspend fun getFavorites(\n        url: String,\n    ): FavoritesParser.Result {\n        val referer = EhUrl.referer\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith(FavoritesParser::parse)\n            .apply { fillGalleryList(galleryInfoList, url, false) }\n    }\n\n    /**\n     * @param dstCat -1 for delete, 0 - 9 for cloud favorite, others throw Exception\n     * @param note   max 250 characters\n     */\n    suspend fun addFavorites(\n        gid: Long,\n        token: String?,\n        dstCat: Int,\n        note: String?,\n    ) {\n        val catStr: String = when (dstCat) {\n            -1 -> {\n                \"favdel\"\n            }\n            in 0..9 -> {\n                dstCat.toString()\n            }\n            else -> {\n                throw EhException(\"Invalid dstCat: $dstCat\")\n            }\n        }\n        val builder = FormBody.Builder()\n        builder.add(\"favcat\", catStr)\n        builder.add(\"favnote\", note ?: \"\")\n        // submit=Add+to+Favorites is not necessary, just use submit=Apply+Changes all the time\n        builder.add(\"submit\", \"Apply Changes\")\n        builder.add(\"update\", \"1\")\n        val url = EhUrl.getAddFavorites(gid, token)\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, url, origin)\n            .post(builder.build())\n            .executeAndParsingWith { }\n    }\n\n    @Throws(Throwable::class)\n    suspend fun addFavoritesRange(\n        gidArray: LongArray,\n        tokenArray: Array<String?>,\n        dstCat: Int,\n    ): Void? {\n        check(gidArray.size == tokenArray.size)\n        var i = 0\n        val n = gidArray.size\n        while (i < n) {\n            addFavorites(gidArray[i], tokenArray[i], dstCat, null)\n            i++\n        }\n        return null\n    }\n\n    suspend fun modifyFavorites(\n        url: String,\n        gidArray: LongArray,\n        dstCat: Int,\n    ): FavoritesParser.Result {\n        val catStr: String = when (dstCat) {\n            -1 -> {\n                \"delete\"\n            }\n            in 0..9 -> {\n                \"fav$dstCat\"\n            }\n            else -> {\n                throw EhException(\"Invalid dstCat: $dstCat\")\n            }\n        }\n        val builder = FormBody.Builder()\n        builder.add(\"ddact\", catStr)\n        for (gid in gidArray) {\n            builder.add(\"modifygids[]\", gid.toString())\n        }\n        builder.add(\"apply\", \"Apply\")\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, url, origin)\n            .post(builder.build())\n            .executeAndParsingWith(FavoritesParser::parse)\n            .apply { fillGalleryList(galleryInfoList, url, false) }\n    }\n\n    suspend fun getTorrentList(\n        url: String,\n        gid: Long,\n        token: String?,\n    ): List<TorrentParser.Result> {\n        val referer = EhUrl.getGalleryDetailUrl(gid, token)\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith(TorrentParser::parse)\n    }\n\n    suspend fun getArchiveList(\n        url: String,\n        gid: Long,\n        token: String?,\n    ): ArchiveParser.Result {\n        val referer = EhUrl.getGalleryDetailUrl(gid, token)\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith(ArchiveParser::parse)\n            .apply { funds = funds ?: getFunds() }\n    }\n\n    suspend fun downloadArchive(\n        gid: Long,\n        token: String?,\n        res: String?,\n        isHAtH: Boolean,\n    ): String? {\n        if (res.isNullOrEmpty()) {\n            throw EhException(\"Invalid res: $res\")\n        }\n        val builder = FormBody.Builder()\n        if (isHAtH) {\n            builder.add(\"hathdl_xres\", res)\n        } else {\n            builder.add(\"dltype\", res)\n            if (res == \"org\") {\n                builder.add(\"dlcheck\", \"Download Original Archive\")\n            } else {\n                builder.add(\"dlcheck\", \"Download Resample Archive\")\n            }\n        }\n        val url = EhUrl.getDownloadArchive(gid, token)\n        val referer = EhUrl.getGalleryDetailUrl(gid, token)\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        val request = EhRequestBuilder(url, referer, origin)\n            .post(builder.build())\n        var result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)\n        if (!isHAtH) {\n            if (result == null) {\n                // Wait for the server to prepare archives\n                delay(1000)\n                result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)\n                if (result == null) {\n                    throw EhException(\"Archive unavailable\")\n                }\n            }\n            return result\n        }\n        return null\n    }\n\n    private suspend fun getFunds(): HomeParser.Funds {\n        val url = EhUrl.URL_FUNDS\n        Log.d(TAG, url)\n        return EhRequestBuilder(url).executeAndParsingWith(HomeParser::parseFunds)\n    }\n\n    private suspend fun getImageLimitsInternal(): HomeParser.Limits {\n        val url = EhUrl.URL_HOME\n        Log.d(TAG, url)\n        return EhRequestBuilder(url).executeAndParsingWith(HomeParser::parse)\n    }\n\n    suspend fun getImageLimits(): HomeParser.Result = coroutineScope {\n        val limitsDeferred = async {\n            getImageLimitsInternal()\n        }\n        val fundsDeferred = async {\n            getFunds()\n        }\n        HomeParser.Result(limitsDeferred.await(), fundsDeferred.await())\n    }\n\n    suspend fun resetImageLimits(unlock: Boolean = false): HomeParser.Limits {\n        val builder = FormBody.Builder()\n            .add(\"reset_imagelimit\", if (unlock) \"Unlock Quota\" else \"Reset Quota\")\n        val url = EhUrl.URL_HOME\n        Log.d(TAG, url)\n        return EhRequestBuilder(url)\n            .post(builder.build())\n            .executeAndParsingWith(HomeParser::parseResetLimits)\n    }\n\n    suspend fun getNews(parse: Boolean): String? {\n        val url = EhUrl.URL_NEWS\n        val referer = EhUrl.REFERER_E\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith {\n            if (parse) EventPaneParser.parse(this) else null\n        }\n    }\n\n    private suspend fun getProfileInternal(\n        url: String,\n        referer: String,\n    ): ProfileParser.Result {\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith(ProfileParser::parse)\n    }\n\n    suspend fun getProfile(): ProfileParser.Result {\n        val url = EhUrl.URL_FORUMS\n        Log.d(TAG, url)\n        return getProfileInternal(\n            EhRequestBuilder(url).executeAndParsingWith(ForumsParser::parse),\n            url,\n        )\n    }\n\n    private suspend fun getUConfigInternal(url: String) {\n        Log.d(TAG, url)\n        EhRequestBuilder(url).executeAndParsingWith(UserConfigParser::parse)\n    }\n\n    suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {\n        runCatching {\n            getUConfigInternal(url)\n        }.onFailure {\n            // It may get redirected when accessing ex for the first time\n            if (url == EhUrl.URL_UCONFIG_EX) {\n                it.printStackTrace()\n                getUConfigInternal(url)\n            } else {\n                throw it\n            }\n        }\n    }\n\n    suspend fun voteComment(\n        apiUid: Long,\n        apiKey: String?,\n        gid: Long,\n        token: String?,\n        commentId: Long,\n        commentVote: Int,\n    ): VoteCommentParser.Result {\n        val json = JSONObject()\n        json.put(\"method\", \"votecomment\")\n        json.put(\"apiuid\", apiUid)\n        json.put(\"apikey\", apiKey)\n        json.put(\"gid\", gid)\n        json.put(\"token\", token)\n        json.put(\"comment_id\", commentId)\n        json.put(\"comment_vote\", commentVote)\n        val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)\n        val url = EhUrl.apiUrl\n        val referer = EhUrl.referer\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(requestBody)\n            .executeAndParsingWith {\n                VoteCommentParser.parse(this, commentVote)\n            }\n    }\n\n    suspend fun voteTag(\n        apiUid: Long,\n        apiKey: String?,\n        gid: Long,\n        token: String?,\n        tags: String?,\n        vote: Int,\n    ): Pair<String, Array<GalleryTagGroup>?> {\n        val json = JSONObject()\n        json.put(\"method\", \"taggallery\")\n        json.put(\"apiuid\", apiUid)\n        json.put(\"apikey\", apiKey)\n        json.put(\"gid\", gid)\n        json.put(\"token\", token)\n        json.put(\"tags\", tags)\n        json.put(\"vote\", vote)\n        val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)\n        val url = EhUrl.apiUrl\n        val referer = EhUrl.referer\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(requestBody)\n            .executeAndParsingWith(VoteTagParser::parse)\n    }\n\n    /**\n     * @param image Must be jpeg\n     */\n    suspend fun imageSearch(\n        image: File,\n        uss: Boolean,\n        osc: Boolean,\n        se: Boolean,\n    ): GalleryListParser.Result {\n        val builder = MultipartBody.Builder()\n        builder.setType(MultipartBody.FORM)\n        builder.addPart(\n            Headers.headersOf(\n                \"Content-Disposition\",\n                \"form-data; name=\\\"sfile\\\"; filename=\\\"a.jpg\\\"\",\n            ),\n            image.asRequestBody(MEDIA_TYPE_JPEG),\n        )\n        if (uss) {\n            builder.addPart(\n                Headers.headersOf(\"Content-Disposition\", \"form-data; name=\\\"fs_similar\\\"\"),\n                \"on\".toRequestBody(),\n            )\n        }\n        if (osc) {\n            builder.addPart(\n                Headers.headersOf(\"Content-Disposition\", \"form-data; name=\\\"fs_covers\\\"\"),\n                \"on\".toRequestBody(),\n            )\n        }\n        if (se) {\n            builder.addPart(\n                Headers.headersOf(\"Content-Disposition\", \"form-data; name=\\\"fs_exp\\\"\"),\n                \"on\".toRequestBody(),\n            )\n        }\n        builder.addPart(\n            Headers.headersOf(\"Content-Disposition\", \"form-data; name=\\\"f_sfile\\\"\"),\n            \"File Search\".toRequestBody(),\n        )\n        val url = EhUrl.imageSearchUrl\n        val referer = EhUrl.referer\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(builder.build())\n            .executeAndParsingWith(GalleryListParser::parse)\n            .apply { fillGalleryList(galleryInfoList, url, true) }\n    }\n\n    suspend fun getGalleryPage(\n        url: String?,\n        gid: Long,\n        token: String?,\n    ): GalleryPageParser.Result {\n        val referer = EhUrl.getGalleryDetailUrl(gid, token)\n        Log.d(TAG, url!!)\n        return EhRequestBuilder(url, referer).executeAndParsingWith(GalleryPageParser::parse)\n    }\n\n    suspend fun getGalleryPageApi(\n        gid: Long,\n        index: Int,\n        pToken: String?,\n        showKey: String?,\n        previousPToken: String?,\n    ): GalleryPageApiParser.Result {\n        val json = JSONObject()\n        json.put(\"method\", \"showpage\")\n        json.put(\"gid\", gid)\n        json.put(\"page\", index + 1)\n        json.put(\"imgkey\", pToken)\n        json.put(\"showkey\", showKey)\n        val requestBody: RequestBody = json.toString().toRequestBody(MEDIA_TYPE_JSON)\n        val url = EhUrl.apiUrl\n        var referer: String? = null\n        if (index > 0 && previousPToken != null) {\n            referer = EhUrl.getPageUrl(gid, index - 1, previousPToken)\n        }\n        val origin = EhUrl.origin\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer, origin)\n            .post(requestBody)\n            .executeAndParsingWith(GalleryPageApiParser::parse)\n    }\n\n    suspend fun getPTokenFromMultiPageViewer(\n        gid: Long,\n        token: String?,\n        sha1: Boolean = false,\n    ): List<String> {\n        val url = EhUrl.getGalleryMultiPageViewerUrl(gid, token!!, sha1)\n        val referer = EhUrl.getGalleryDetailUrl(gid, token)\n        val parser = if (sha1) GalleryMultiPageViewerParser::parseSha1 else GalleryMultiPageViewerParser::parsePToken\n        Log.d(TAG, url)\n        return EhRequestBuilder(url, referer).executeAndParsingWith(parser)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhFilter.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport android.util.Log\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.dao.Filter\nimport java.util.Locale\nimport java.util.regex.Pattern\n\nobject EhFilter {\n    private val mTitleFilterList: MutableList<Filter> = ArrayList()\n    private val mUploaderFilterList: MutableList<Filter> = ArrayList()\n    private val mTagFilterList: MutableList<Filter> = ArrayList()\n    private val mTagNamespaceFilterList: MutableList<Filter> = ArrayList()\n    private val mCommenterFilterList: MutableList<Filter> = ArrayList()\n    private val mCommentFilterList: MutableList<Filter> = ArrayList()\n\n    private const val MODE_TITLE = 0\n    const val MODE_UPLOADER = 1\n    const val MODE_TAG = 2\n    private const val MODE_TAG_NAMESPACE = 3\n    const val MODE_COMMENTER = 4\n    private const val MODE_COMMENT = 5\n    private val TAG = EhFilter::class.java.simpleName\n\n    init {\n        val list = EhDB.allFilter\n        var i = 0\n        val n = list.size\n        while (i < n) {\n            val filter = list[i]\n            when (filter.mode) {\n                MODE_TITLE -> {\n                    filter.text = filter.text!!.lowercase(Locale.getDefault())\n                    mTitleFilterList.add(filter)\n                }\n                MODE_TAG -> {\n                    filter.text = filter.text!!.lowercase(Locale.getDefault())\n                    mTagFilterList.add(filter)\n                }\n                MODE_TAG_NAMESPACE -> {\n                    filter.text = filter.text!!.lowercase(Locale.getDefault())\n                    mTagNamespaceFilterList.add(filter)\n                }\n                MODE_UPLOADER -> mUploaderFilterList.add(filter)\n                MODE_COMMENTER -> mCommenterFilterList.add(filter)\n                MODE_COMMENT -> mCommentFilterList.add(filter)\n                else -> Log.d(TAG, \"Unknown mode: \" + filter.mode)\n            }\n            i++\n        }\n    }\n\n    val titleFilterList: List<Filter>\n        get() = mTitleFilterList\n    val uploaderFilterList: List<Filter>\n        get() = mUploaderFilterList\n    val tagFilterList: List<Filter>\n        get() = mTagFilterList\n    val tagNamespaceFilterList: List<Filter>\n        get() = mTagNamespaceFilterList\n    val commenterFilterList: List<Filter>\n        get() = mCommenterFilterList\n    val commentFilterList: List<Filter>\n        get() = mCommentFilterList\n\n    fun applyTranslation(filter: Filter): String? {\n        var text = filter.text ?: return null\n        if (EhTagDatabase.isInitialized()) {\n            when (filter.mode) {\n                MODE_TAG_NAMESPACE -> {\n                    EhTagDatabase.getTranslation(tag = text)?.let {\n                        text = \"$text ($it)\"\n                    }\n                }\n                MODE_TAG -> {\n                    val index = text.indexOf(':')\n                    if (index > 0) {\n                        EhTagDatabase.getTranslation(\n                            EhTagDatabase.namespaceToPrefix(text.substring(0, index)),\n                            text.substring(index + 1),\n                        )?.let {\n                            text = \"$text ($it)\"\n                        }\n                    }\n                }\n            }\n        }\n        return text\n    }\n\n    @Synchronized\n    fun addFilter(filter: Filter): Boolean {\n        // enable filter by default before it is added to database\n        filter.enable = true\n        if (!EhDB.addFilter(filter)) return false\n        when (filter.mode) {\n            MODE_TITLE -> {\n                filter.text = filter.text!!.lowercase(Locale.getDefault())\n                mTitleFilterList.add(filter)\n            }\n            MODE_TAG -> {\n                filter.text = filter.text!!.lowercase(Locale.getDefault())\n                mTagFilterList.add(filter)\n            }\n            MODE_TAG_NAMESPACE -> {\n                filter.text = filter.text!!.lowercase(Locale.getDefault())\n                mTagNamespaceFilterList.add(filter)\n            }\n            MODE_UPLOADER -> mUploaderFilterList.add(filter)\n            MODE_COMMENTER -> mCommenterFilterList.add(filter)\n            MODE_COMMENT -> mCommentFilterList.add(filter)\n            else -> Log.d(TAG, \"Unknown mode: \" + filter.mode)\n        }\n        return true\n    }\n\n    @Synchronized\n    fun triggerFilter(filter: Filter) {\n        EhDB.triggerFilter(filter)\n    }\n\n    @Synchronized\n    fun deleteFilter(filter: Filter) {\n        EhDB.deleteFilter(filter)\n        when (filter.mode) {\n            MODE_TITLE -> mTitleFilterList.remove(filter)\n            MODE_TAG -> mTagFilterList.remove(filter)\n            MODE_TAG_NAMESPACE -> mTagNamespaceFilterList.remove(filter)\n            MODE_UPLOADER -> mUploaderFilterList.remove(filter)\n            MODE_COMMENTER -> mCommenterFilterList.remove(filter)\n            MODE_COMMENT -> mCommentFilterList.remove(filter)\n            else -> Log.d(TAG, \"Unknown mode: \" + filter.mode)\n        }\n    }\n\n    @Synchronized\n    fun needTags(): Boolean = mTagFilterList.isNotEmpty() || mTagNamespaceFilterList.isNotEmpty()\n\n    @Synchronized\n    fun filterTitle(info: GalleryInfo?): Boolean {\n        if (null == info) {\n            return false\n        }\n\n        // Title\n        val title = info.title\n        val filters: List<Filter> = mTitleFilterList\n        if (null != title && filters.isNotEmpty()) {\n            var i = 0\n            val n = filters.size\n            while (i < n) {\n                if (filters[i].enable!! &&\n                    title.lowercase(Locale.getDefault()).contains(\n                        filters[i].text!!,\n                    )\n                ) {\n                    return false\n                }\n                i++\n            }\n        }\n        return true\n    }\n\n    @Synchronized\n    fun filterUploader(info: GalleryInfo?): Boolean {\n        if (null == info) {\n            return false\n        }\n\n        // Uploader\n        val uploader = info.uploader\n        val filters: List<Filter> = mUploaderFilterList\n        if (null != uploader && filters.isNotEmpty()) {\n            var i = 0\n            val n = filters.size\n            while (i < n) {\n                if (filters[i].enable!! && uploader == filters[i].text) {\n                    return false\n                }\n                i++\n            }\n        }\n        return true\n    }\n\n    private fun matchTag(tag: String?, filter: String?): Boolean {\n        if (null == tag || null == filter) {\n            return false\n        }\n        val tagNamespace: String?\n        val tagName: String\n        val filterNamespace: String?\n        val filterName: String\n        var index = tag.indexOf(':')\n        if (index < 0) {\n            tagNamespace = null\n            tagName = tag\n        } else {\n            tagNamespace = tag.substring(0, index)\n            tagName = tag.substring(index + 1)\n        }\n        index = filter.indexOf(':')\n        if (index < 0) {\n            filterNamespace = null\n            filterName = filter\n        } else {\n            filterNamespace = filter.substring(0, index)\n            filterName = filter.substring(index + 1)\n        }\n        return if (null != tagNamespace &&\n            null != filterNamespace &&\n            tagNamespace != filterNamespace\n        ) {\n            false\n        } else {\n            tagName == filterName\n        }\n    }\n\n    @Synchronized\n    fun filterTag(info: GalleryInfo?): Boolean {\n        if (null == info) {\n            return false\n        }\n\n        // Tag\n        val tags = info.simpleTags\n        val filters: List<Filter> = mTagFilterList\n        if (null != tags && filters.isNotEmpty()) {\n            for (tag in tags) {\n                var i = 0\n                val n = filters.size\n                while (i < n) {\n                    if (filters[i].enable!! && matchTag(tag, filters[i].text)) {\n                        return false\n                    }\n                    i++\n                }\n            }\n        }\n        return true\n    }\n\n    private fun matchTagNamespace(tag: String?, filter: String?): Boolean {\n        if (null == tag || null == filter) {\n            return false\n        }\n        val tagNamespace: String\n        val index = tag.indexOf(':')\n        return if (index >= 0) {\n            tagNamespace = tag.substring(0, index)\n            tagNamespace == filter\n        } else {\n            false\n        }\n    }\n\n    @Synchronized\n    fun filterTagNamespace(info: GalleryInfo?): Boolean {\n        if (null == info) {\n            return false\n        }\n        val tags = info.simpleTags\n        val filters: List<Filter> = mTagNamespaceFilterList\n        if (null != tags && filters.isNotEmpty()) {\n            for (tag in tags) {\n                var i = 0\n                val n = filters.size\n                while (i < n) {\n                    if (filters[i].enable!! && matchTagNamespace(tag, filters[i].text)) {\n                        return false\n                    }\n                    i++\n                }\n            }\n        }\n        return true\n    }\n\n    @Synchronized\n    fun filterCommenter(commenter: String?): Boolean {\n        if (null == commenter) {\n            return false\n        }\n        val filters: List<Filter> = mCommenterFilterList\n        if (filters.isNotEmpty()) {\n            var i = 0\n            val n = filters.size\n            while (i < n) {\n                if (filters[i].enable!! && commenter == filters[i].text) {\n                    return false\n                }\n                i++\n            }\n        }\n        return true\n    }\n\n    @Synchronized\n    fun filterComment(comment: String?): Boolean {\n        if (null == comment) {\n            return false\n        }\n        val filters: List<Filter> = mCommentFilterList\n        if (filters.isNotEmpty()) {\n            var i = 0\n            val n = filters.size\n            while (i < n) {\n                if (filters[i].enable!!) {\n                    val p = Pattern.compile(filters[i].text!!)\n                    val m = p.matcher(comment)\n                    if (m.find()) return false\n                }\n                i++\n            }\n        }\n        return true\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhRequest.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport android.app.Activity\nimport androidx.annotation.MainThread\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentActivity\nimport androidx.lifecycle.lifecycleScope\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.Job\n\nclass EhRequest {\n    internal var job: Job? = null\n    val isActive\n        get() = job?.isActive == true\n    var method = 0\n        private set\n    var args: Array<out Any?>? = null\n        private set\n    var callback: EhClient.Callback<Any?>? = null\n        private set\n\n    fun setMethod(method: Int): EhRequest {\n        this.method = method\n        return this\n    }\n\n    fun setArgs(vararg args: Any?): EhRequest {\n        this.args = args\n        return this\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun setCallback(callback: EhClient.Callback<*>?): EhRequest {\n        this.callback = callback as EhClient.Callback<Any?>?\n        return this\n    }\n\n    fun enqueue(scope: CoroutineScope) {\n        EhClient.enqueue(this, scope)\n    }\n\n    @DelicateCoroutinesApi\n    fun enqueue() {\n        EhClient.enqueue(this, GlobalScope)\n    }\n\n    fun enqueue(fragment: Fragment) {\n        enqueue(fragment.viewLifecycleOwner.lifecycleScope)\n    }\n\n    fun enqueue(activity: Activity) {\n        check(activity is FragmentActivity)\n        enqueue(activity.lifecycleScope)\n    }\n\n    @MainThread\n    fun cancel() {\n        if (isActive) {\n            job?.cancel()\n            callback?.onCancel()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient\nimport com.hippo.okhttp.ChromeRequestBuilder\nimport okhttp3.Request\nimport okhttp3.Response\nimport okhttp3.coroutines.executeAsync\n\nclass EhRequestBuilder(\n    url: String,\n    referer: String? = null,\n    origin: String? = null,\n) : ChromeRequestBuilder(url) {\n    init {\n        referer?.let { addHeader(\"Referer\", it) }\n        origin?.let { addHeader(\"Origin\", it) }\n    }\n}\n\nsuspend inline fun <R> Request.executeNoRedirect(block: Response.() -> R) = noRedirectOkHttpClient.newCall(this).executeAsync().use(block)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhTagDatabase.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer.client\n\nimport android.content.Context\nimport com.hippo.ehviewer.AppConfig\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.EhApplication.Companion.nonCacheOkHttpClient\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.unifile.UniFile\nimport com.hippo.unifile.sha1\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.copyToFile\nimport java.io.File\nimport java.io.IOException\nimport java.nio.charset.StandardCharsets\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport okhttp3.Request\nimport okhttp3.coroutines.executeAsync\nimport okio.BufferedSource\nimport okio.buffer\nimport okio.source\nimport org.json.JSONException\nimport org.json.JSONObject\n\nprivate typealias TagGroup = Map<String, String>\nprivate typealias TagGroups = Map<String, TagGroup>\n\nobject EhTagDatabase {\n    private const val NAMESPACE_PREFIX = \"n\"\n    private const val UPDATE_INTERVAL = 3 * 24 * 3600 * 1000\n    const val TYPE_EQUAL = 0\n    const val TYPE_START = 1\n    const val TYPE_CONTAIN = 2\n    private lateinit var tagGroups: TagGroups\n    private val dir = AppConfig.getFilesDir(\"tag-translations\")\n    private val urls = getMetadata(EhApplication.application)\n    private val sha1Name = urls?.get(0)!!\n    private val sha1Url = urls?.get(1)!!\n    private val dataName = urls?.get(2)!!\n    private val dataUrl = urls?.get(3)!!\n    private val updateLock = Mutex()\n\n    fun isInitialized(): Boolean = this::tagGroups.isInitialized\n\n    private fun JSONObject.toTagGroups(): TagGroups = keys().asSequence().associateWith { getJSONObject(it).toTagGroup() }\n\n    private fun JSONObject.toTagGroup(): TagGroup = keys().asSequence().associateWith { getString(it) }\n\n    private fun updateData(source: BufferedSource) {\n        try {\n            tagGroups = JSONObject(source.readString(StandardCharsets.UTF_8)).toTagGroups()\n        } catch (e: JSONException) {\n            e.printStackTrace()\n        }\n    }\n\n    fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }\n\n    private fun internalSuggestFlow(\n        tags: Map<String, String>,\n        keyword: String,\n        translate: Boolean,\n        type: Int,\n    ): Flow<Pair<String?, String>> = flow {\n        when (type) {\n            TYPE_EQUAL -> {\n                if (translate) {\n                    tags.forEach { (tag, hint) ->\n                        if (tag.equalsIgnoreSpace(keyword) || hint.equalsIgnoreSpace(keyword)) {\n                            emit(Pair(hint, tag))\n                        }\n                    }\n                } else {\n                    tags[keyword]?.let {\n                        emit(Pair(null, keyword))\n                    }\n                }\n            }\n            TYPE_START -> {\n                if (translate) {\n                    tags.forEach { (tag, hint) ->\n                        if (!tag.equalsIgnoreSpace(keyword) &&\n                            !hint.equalsIgnoreSpace(keyword) &&\n                            (tag.startsWithIgnoreSpace(keyword) || hint.startsWithIgnoreSpace(keyword))\n                        ) {\n                            emit(Pair(hint, tag))\n                        }\n                    }\n                } else {\n                    tags.keys.forEach { tag ->\n                        if (!tag.equalsIgnoreSpace(keyword) && tag.startsWithIgnoreSpace(keyword)) {\n                            emit(Pair(null, tag))\n                        }\n                    }\n                }\n            }\n            TYPE_CONTAIN -> {\n                if (translate) {\n                    tags.forEach { (tag, hint) ->\n                        if (!tag.equalsIgnoreSpace(keyword) &&\n                            !hint.equalsIgnoreSpace(keyword) &&\n                            !tag.startsWithIgnoreSpace(keyword) &&\n                            !hint.startsWithIgnoreSpace(keyword) &&\n                            (tag.containsIgnoreSpace(keyword) || hint.containsIgnoreSpace(keyword))\n                        ) {\n                            emit(Pair(hint, tag))\n                        }\n                    }\n                } else {\n                    tags.keys.forEach { tag ->\n                        if (!tag.equalsIgnoreSpace(keyword) &&\n                            !tag.startsWithIgnoreSpace(keyword) &&\n                            tag.containsIgnoreSpace(keyword)\n                        ) {\n                            emit(Pair(null, tag))\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /* Construct a cold flow for tag database suggestions */\n    fun suggestFlow(\n        keyword: String,\n        translate: Boolean,\n        type: Int,\n    ): Flow<Pair<String?, String>> = flow {\n        val keywordPrefix = keyword.substringBefore(':')\n        val keywordTag = keyword.drop(keywordPrefix.length + 1)\n        val prefix = namespaceToPrefix(keywordPrefix) ?: keywordPrefix\n        val tags = tagGroups[prefix.takeIf { keywordTag.isNotEmpty() && it != NAMESPACE_PREFIX }]\n        tags?.let {\n            internalSuggestFlow(it, keywordTag, translate, type).collect { (hint, tag) ->\n                emit(Pair(hint, \"$prefix:$tag\"))\n            }\n        } ?: tagGroups.forEach { (prefix, tags) ->\n            internalSuggestFlow(tags, keyword, translate, type).collect { (hint, tag) ->\n                emit(Pair(hint, if (prefix == NAMESPACE_PREFIX) \"$tag:\" else \"$prefix:$tag\"))\n            }\n        }\n    }\n\n    private fun String.removeSpace(): String = replace(\" \", \"\")\n\n    private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().contains(other.removeSpace(), ignoreCase)\n\n    private fun String.equalsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().equals(other.removeSpace(), ignoreCase)\n\n    private fun String.startsWithIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean = removeSpace().startsWith(other.removeSpace(), ignoreCase)\n\n    private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {\n        it[\"artist\"] = \"a\"\n        it[\"character\"] = \"c\"\n        it[\"cosplayer\"] = \"cos\"\n        it[\"female\"] = \"f\"\n        it[\"group\"] = \"g\"\n        it[\"language\"] = \"l\"\n        it[\"location\"] = \"loc\"\n        it[\"male\"] = \"m\"\n        it[\"mixed\"] = \"x\"\n        it[\"other\"] = \"o\"\n        it[\"parody\"] = \"p\"\n        it[\"reclass\"] = \"r\"\n    }\n\n    fun namespaceToPrefix(namespace: String): String? = NAMESPACE_TO_PREFIX[namespace]\n\n    private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)\n        .takeIf { it.size == 4 }\n\n    fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)\n\n    private fun getFileContent(file: File): String? = runCatching {\n        file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }\n    }.getOrNull()\n\n    private fun checkData(sha1: String?, data: File): Boolean = sha1 != null && sha1 == UniFile.fromFile(data)?.sha1()\n\n    private suspend fun save(url: String, file: File): Boolean {\n        val request: Request = Request.Builder().url(url).build()\n        val call = nonCacheOkHttpClient.newCall(request)\n        runCatching {\n            call.executeAsync().use { response ->\n                if (!response.isSuccessful) {\n                    return false\n                }\n                response.body.use {\n                    it.copyToFile(file)\n                }\n                return true\n            }\n        }.onFailure {\n            file.delete()\n            it.printStackTrace()\n        }\n        return false\n    }\n\n    suspend fun read() {\n        if (urls != null && !isInitialized()) {\n            runCatching {\n                checkNotNull(dir)\n                val sha1File = File(dir, sha1Name)\n                val dataFile = File(dir, dataName)\n\n                // Check current sha1 and current data\n                val sha1 = getFileContent(sha1File)\n                if (!checkData(sha1, dataFile)) {\n                    FileUtils.delete(sha1File)\n                    FileUtils.delete(dataFile)\n                    Settings.putTranslationsLastUpdate(-1)\n                }\n\n                // Read current EhTagDatabase\n                if (dataFile.exists()) {\n                    try {\n                        dataFile.source().buffer().use { updateData(it) }\n                    } catch (_: IOException) {\n                        FileUtils.delete(sha1File)\n                        FileUtils.delete(dataFile)\n                        Settings.putTranslationsLastUpdate(-1)\n                    }\n                }\n            }.onFailure {\n                it.printStackTrace()\n            }\n            update()\n        }\n    }\n\n    suspend fun update(force: Boolean = false) {\n        val time = System.currentTimeMillis()\n        if (urls != null && (force || time - Settings.translationsLastUpdate > UPDATE_INTERVAL)) {\n            updateLock.withLock {\n                runCatching {\n                    checkNotNull(dir)\n                    val sha1File = File(dir, sha1Name)\n                    val dataFile = File(dir, dataName)\n\n                    // Save new sha1\n                    val tempSha1File = File(dir, \"$sha1Name.tmp\")\n                    check(save(sha1Url, tempSha1File))\n                    val tempSha1 = getFileContent(tempSha1File)\n\n                    // Check new sha1 and current sha1\n                    if (tempSha1 == getFileContent(sha1File)) {\n                        // The data is the same\n                        FileUtils.delete(tempSha1File)\n                        return@runCatching\n                    }\n\n                    // Save new data\n                    val tempDataFile = File(dir, \"$dataName.tmp\")\n                    check(save(dataUrl, tempDataFile))\n\n                    // Check new sha1 and new data\n                    if (!checkData(tempSha1, tempDataFile)) {\n                        FileUtils.delete(tempSha1File)\n                        FileUtils.delete(tempDataFile)\n                        return@runCatching\n                    }\n\n                    // Replace current sha1 and current data with new sha1 and new data\n                    FileUtils.delete(sha1File)\n                    FileUtils.delete(dataFile)\n                    tempSha1File.renameTo(sha1File)\n                    tempDataFile.renameTo(dataFile)\n\n                    // Read new EhTagDatabase\n                    try {\n                        dataFile.source().buffer().use { updateData(it) }\n                        Settings.putTranslationsLastUpdate(time)\n                    } catch (_: IOException) {\n                    }\n                }.onFailure {\n                    it.printStackTrace()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhUrl.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport com.hippo.ehviewer.Settings\nimport com.hippo.network.UrlBuilder\n\nobject EhUrl {\n    const val SITE_E = 0\n    const val SITE_EX = 1\n    const val DOMAIN_E = \"e-hentai.org\"\n    const val DOMAIN_EX = \"exhentai.org\"\n    const val DOMAIN_LOFI = \"lofi.e-hentai.org\"\n    const val HOST_E = \"https://$DOMAIN_E/\"\n    const val HOST_EX = \"https://$DOMAIN_EX/\"\n    private const val API_E = \"https://api.e-hentai.org/api.php\"\n    private const val API_EX = \"https://s.exhentai.org/api.php\"\n    private const val URL_FAVORITES_E = HOST_E + \"favorites.php\"\n    private const val URL_FAVORITES_EX = HOST_EX + \"favorites.php\"\n    private const val URL_IMAGE_SEARCH_E = \"https://upload.e-hentai.org/image_lookup.php\"\n    private const val URL_IMAGE_SEARCH_EX = \"https://exhentai.org/upload/image_lookup.php\"\n    private const val URL_MY_TAGS_E = HOST_E + \"mytags\"\n    private const val URL_MY_TAGS_EX = HOST_EX + \"mytags\"\n    private const val URL_POPULAR_E = HOST_E + \"popular\"\n    private const val URL_POPULAR_EX = HOST_EX + \"popular\"\n    private const val URL_WATCHED_E = HOST_E + \"watched\"\n    private const val URL_WATCHED_EX = HOST_EX + \"watched\"\n    const val URL_UCONFIG_E = HOST_E + \"uconfig.php\"\n    const val URL_UCONFIG_EX = HOST_EX + \"uconfig.php\"\n    const val URL_FORUMS = \"https://forums.e-hentai.org/\"\n    const val URL_SIGN_IN = URL_FORUMS + \"index.php?act=Login\"\n    const val URL_REGISTER = URL_FORUMS + \"index.php?act=Reg&CODE=00\"\n    const val API_SIGN_IN = \"$URL_SIGN_IN&CODE=01\"\n    const val URL_FUNDS = HOST_E + \"exchange.php?t=gp\"\n    const val URL_HOME = HOST_E + \"home.php\"\n    const val URL_NEWS = HOST_E + \"news.php\"\n    const val REFERER_E = \"https://$DOMAIN_E\"\n    private const val REFERER_EX = \"https://$DOMAIN_EX\"\n    private const val ORIGIN_E = REFERER_E\n    private const val ORIGIN_EX = REFERER_EX\n\n    val host: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> HOST_E\n            SITE_EX -> HOST_EX\n            else -> HOST_E\n        }\n\n    val favoritesUrl: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> URL_FAVORITES_E\n            SITE_EX -> URL_FAVORITES_EX\n            else -> URL_FAVORITES_E\n        }\n\n    val apiUrl: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> API_E\n            SITE_EX -> API_EX\n            else -> API_E\n        }\n\n    val referer: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> REFERER_E\n            SITE_EX -> REFERER_EX\n            else -> REFERER_E\n        }\n\n    val origin: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> ORIGIN_E\n            SITE_EX -> ORIGIN_EX\n            else -> ORIGIN_E\n        }\n\n    val uConfigUrl: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> URL_UCONFIG_E\n            SITE_EX -> URL_UCONFIG_EX\n            else -> URL_UCONFIG_E\n        }\n\n    val myTagsUrl: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> URL_MY_TAGS_E\n            SITE_EX -> URL_MY_TAGS_EX\n            else -> URL_MY_TAGS_E\n        }\n\n    val popularUrl: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> URL_POPULAR_E\n            SITE_EX -> URL_POPULAR_EX\n            else -> URL_POPULAR_E\n        }\n\n    val imageSearchUrl: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> URL_IMAGE_SEARCH_E\n            SITE_EX -> URL_IMAGE_SEARCH_EX\n            else -> URL_IMAGE_SEARCH_E\n        }\n\n    val watchedUrl: String\n        get() = when (Settings.gallerySite) {\n            SITE_E -> URL_WATCHED_E\n            SITE_EX -> URL_WATCHED_EX\n            else -> URL_WATCHED_E\n        }\n\n    fun getGalleryDetailUrl(\n        gid: Long,\n        token: String?,\n        index: Int = 0,\n        allComment: Boolean = false,\n        sha1: Boolean = false,\n    ): String {\n        val builder = UrlBuilder(host + \"g/\" + gid + '/' + token + '/')\n        if (index > 0) builder.addQuery(\"p\", index)\n        if (allComment) builder.addQuery(\"hc\", 1)\n        if (sha1) builder.addQuery(\"datatags\", 1)\n        return builder.build()\n    }\n\n    fun getGalleryMultiPageViewerUrl(gid: Long, token: String, sha1: Boolean = false): String {\n        val builder = UrlBuilder(host + \"mpv/\" + gid + '/' + token + '/')\n        if (sha1) builder.addQuery(\"datatags\", 1)\n        return builder.build()\n    }\n\n    fun getPageUrl(gid: Long, index: Int, pToken: String): String = host + \"s/\" + pToken + '/' + gid + '-' + (index + 1)\n\n    fun getAddFavorites(gid: Long, token: String?): String = host + \"gallerypopups.php?gid=\" + gid + \"&t=\" + token + \"&act=addfav\"\n\n    fun getDownloadArchive(gid: Long, token: String?): String = host + \"archiver.php?gid=\" + gid + \"&token=\" + token\n\n    fun getTagDefinitionUrl(tag: String): String = \"https://ehwiki.org/wiki/\" + tag.replace(' ', '_')\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhUrlOpener.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.util.Log\nimport com.hippo.ehviewer.client.parser.GalleryDetailUrlParser\nimport com.hippo.ehviewer.client.parser.GalleryListUrlParser\nimport com.hippo.ehviewer.client.parser.GalleryPageUrlParser\nimport com.hippo.ehviewer.ui.scene.GalleryDetailScene\nimport com.hippo.ehviewer.ui.scene.GalleryListScene\nimport com.hippo.ehviewer.ui.scene.ProgressScene\nimport com.hippo.scene.Announcer\n\nobject EhUrlOpener {\n    private val TAG = EhUrlOpener::class.java.simpleName\n    fun parseUrl(url: String): Announcer? {\n        if (TextUtils.isEmpty(url)) {\n            return null\n        }\n        val listUrlBuilder = GalleryListUrlParser.parse(url)\n        if (listUrlBuilder != null) {\n            val args = Bundle()\n            args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_LIST_URL_BUILDER)\n            args.putParcelable(GalleryListScene.KEY_LIST_URL_BUILDER, listUrlBuilder)\n            return Announcer(GalleryListScene::class.java).setArgs(args)\n        }\n        val result1 = GalleryDetailUrlParser.parse(url)\n        if (result1 != null) {\n            val args = Bundle()\n            args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)\n            args.putLong(GalleryDetailScene.KEY_GID, result1.gid)\n            args.putString(GalleryDetailScene.KEY_TOKEN, result1.token)\n            return Announcer(GalleryDetailScene::class.java).setArgs(args)\n        }\n        val result2 = GalleryPageUrlParser.parse(url)\n        if (result2 != null) {\n            val args = Bundle()\n            args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)\n            args.putLong(ProgressScene.KEY_GID, result2.gid)\n            args.putString(ProgressScene.KEY_PTOKEN, result2.pToken)\n            args.putInt(ProgressScene.KEY_PAGE, result2.page)\n            return Announcer(ProgressScene::class.java).setArgs(args)\n        }\n        Log.i(TAG, \"Can't parse url: $url\")\n        return null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/EhUtils.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client\n\nimport android.text.TextUtils\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUrl.host\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport java.util.regex.Pattern\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nobject EhUtils {\n    const val NONE = -1 // Use it for homepage\n    const val MISC = 0x1\n    const val DOUJINSHI = 0x2\n    const val MANGA = 0x4\n    const val ARTIST_CG = 0x8\n    const val GAME_CG = 0x10\n    const val IMAGE_SET = 0x20\n    const val COSPLAY = 0x40\n    const val ASIAN_PORN = 0x80\n    const val NON_H = 0x100\n    const val WESTERN = 0x200\n    const val ALL_CATEGORY = 0x3ff\n    const val PRIVATE = 0x400\n    const val UNKNOWN = 0x800\n\n    // https://youtrack.jetbrains.com/issue/KT-4749\n    private const val BG_COLOR_DOUJINSHI = 0xfff44336u\n    private const val BG_COLOR_MANGA = 0xffff9800u\n    private const val BG_COLOR_ARTIST_CG = 0xfffbc02du\n    private const val BG_COLOR_GAME_CG = 0xff4caf50u\n    private const val BG_COLOR_WESTERN = 0xff8bc34au\n    private const val BG_COLOR_NON_H = 0xff2196f3u\n    private const val BG_COLOR_IMAGE_SET = 0xff3f51b5u\n    private const val BG_COLOR_COSPLAY = 0xff9c27b0u\n    private const val BG_COLOR_ASIAN_PORN = 0xff9575cdu\n    private const val BG_COLOR_MISC = 0xfff06292u\n    private const val BG_COLOR_UNKNOWN = 0xff000000u\n\n    // Remove [XXX], (XXX), {XXX}, ~XXX~ stuff\n    private val PATTERN_TITLE_PREFIX = Pattern.compile(\n        \"^(?:\\\\([^)]*\\\\)|\\\\[[^]]*]|\\\\{[^}]*\\\\}|~[^~]*~|\\\\s+)*\",\n    )\n\n    // Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23\n    private val PATTERN_TITLE_SUFFIX = Pattern.compile(\n        \"(?:\\\\s+ch.[\\\\s\\\\d-]+)?(?:\\\\([^)]*\\\\)|\\\\[[^]]*]|\\\\{[^}]*\\\\}|~[^~]*~|\\\\s+)*$\",\n        Pattern.CASE_INSENSITIVE,\n    )\n\n    private val CATEGORY_VALUES = hashMapOf(\n        MISC to arrayOf(\"misc\"),\n        DOUJINSHI to arrayOf(\"doujinshi\"),\n        MANGA to arrayOf(\"manga\"),\n        ARTIST_CG to arrayOf(\"artistcg\", \"Artist CG Sets\", \"Artist CG\"),\n        GAME_CG to arrayOf(\"gamecg\", \"Game CG Sets\", \"Game CG\"),\n        IMAGE_SET to arrayOf(\"imageset\", \"Image Sets\", \"Image Set\"),\n        COSPLAY to arrayOf(\"cosplay\"),\n        ASIAN_PORN to arrayOf(\"asianporn\", \"Asian Porn\"),\n        NON_H to arrayOf(\"non-h\"),\n        WESTERN to arrayOf(\"western\"),\n        PRIVATE to arrayOf(\"private\"),\n        UNKNOWN to arrayOf(\"unknown\"),\n    )\n    private val CATEGORY_STRINGS = CATEGORY_VALUES.entries.map { (k, v) -> v to k }\n\n    val isExHentai: Boolean\n        get() = Settings.gallerySite == EhUrl.SITE_EX\n\n    val isMPVAvailable: Boolean\n        get() = EhCookieStore.getCookieValue(\n            host.toHttpUrl(),\n            EhCookieStore.KEY_HATH_PERKS,\n        )?.substringBefore(\"-\")?.contains(\"q\") == true\n\n    fun getCategory(type: String?): Int {\n        for (entry in CATEGORY_STRINGS) {\n            for (str in entry.first) {\n                if (str.equals(type, ignoreCase = true)) {\n                    return entry.second\n                }\n            }\n        }\n        return UNKNOWN\n    }\n\n    fun getCategory(type: Int): String = CATEGORY_VALUES[type]?.let { it[0] } ?: CATEGORY_VALUES[UNKNOWN]!![0]\n\n    fun getCategoryColor(category: Int): Int = when (category) {\n        DOUJINSHI -> BG_COLOR_DOUJINSHI\n        MANGA -> BG_COLOR_MANGA\n        ARTIST_CG -> BG_COLOR_ARTIST_CG\n        GAME_CG -> BG_COLOR_GAME_CG\n        WESTERN -> BG_COLOR_WESTERN\n        NON_H -> BG_COLOR_NON_H\n        IMAGE_SET -> BG_COLOR_IMAGE_SET\n        COSPLAY -> BG_COLOR_COSPLAY\n        ASIAN_PORN -> BG_COLOR_ASIAN_PORN\n        MISC -> BG_COLOR_MISC\n        else -> BG_COLOR_UNKNOWN\n    }.toInt()\n\n    suspend fun signOut() {\n        EhCookieStore.clear()\n        Settings.putAvatar(null)\n        Settings.putDisplayName(null)\n        Settings.putGallerySite(EhUrl.SITE_E)\n        Settings.putNeedSignIn(true)\n        Settings.putSelectSite(true)\n    }\n\n    fun needSignedIn(): Boolean = Settings.needSignIn\n\n    fun getSuitableTitle(gi: GalleryInfo): String = if (Settings.showJpnTitle) {\n        if (TextUtils.isEmpty(gi.titleJpn)) gi.title else gi.titleJpn\n    } else {\n        if (TextUtils.isEmpty(gi.title)) gi.titleJpn else gi.title\n    }.orEmpty()\n\n    fun extractTitle(fullTitle: String?): String? {\n        var title: String = fullTitle ?: return null\n        title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst(\"\")\n        title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst(\"\")\n        // Sometimes title is combined by romaji and english translation.\n        // Only need romaji.\n        // TODO But not sure every '|' means that\n        val index = title.indexOf('|')\n        if (index >= 0) {\n            title = title.substring(0, index)\n        }\n        return title.ifEmpty { null }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/AbstractGalleryInfo.kt",
    "content": "/*\n * Copyright 2023 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.client.data\n\ninterface AbstractGalleryInfo {\n    var gid: Long\n    var token: String?\n    var title: String?\n    var titleJpn: String?\n    var thumb: String?\n    var category: Int\n    var posted: String?\n    var uploader: String?\n    var disowned: Boolean\n    var rating: Float\n    var rated: Boolean\n    var simpleTags: Array<String>?\n    var pages: Int\n    var thumbWidth: Int\n    var thumbHeight: Int\n    var spanSize: Int\n    var spanIndex: Int\n    var spanGroupIndex: Int\n    var simpleLanguage: String?\n    var favoriteSlot: Int\n    var favoriteName: String?\n    var favoriteNote: String?\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Ignore\nimport androidx.room.PrimaryKey\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nopen class BaseGalleryInfo(\n    @PrimaryKey\n    @ColumnInfo(name = \"GID\")\n    override var gid: Long = 0,\n\n    @ColumnInfo(name = \"TOKEN\")\n    override var token: String? = null,\n\n    @ColumnInfo(name = \"TITLE\")\n    override var title: String? = null,\n\n    @ColumnInfo(name = \"TITLE_JPN\")\n    override var titleJpn: String? = null,\n\n    @ColumnInfo(name = \"THUMB\")\n    override var thumb: String? = null,\n\n    @ColumnInfo(name = \"CATEGORY\")\n    override var category: Int = 0,\n\n    @ColumnInfo(name = \"POSTED\")\n    override var posted: String? = null,\n\n    @ColumnInfo(name = \"UPLOADER\")\n    override var uploader: String? = null,\n\n    @Ignore\n    override var disowned: Boolean = false,\n\n    @ColumnInfo(name = \"RATING\")\n    override var rating: Float = 0f,\n\n    @Ignore\n    override var rated: Boolean = false,\n\n    @Ignore\n    override var simpleTags: Array<String>? = null,\n\n    @Ignore\n    override var pages: Int = 0,\n\n    @Ignore\n    override var thumbWidth: Int = 0,\n\n    @Ignore\n    override var thumbHeight: Int = 0,\n\n    @Ignore\n    override var spanSize: Int = 0,\n\n    @Ignore\n    override var spanIndex: Int = 0,\n\n    @Ignore\n    override var spanGroupIndex: Int = 0,\n\n    @ColumnInfo(name = \"SIMPLE_LANGUAGE\")\n    override var simpleLanguage: String? = null,\n\n    @Ignore\n    override var favoriteSlot: Int = -2,\n\n    @Ignore\n    override var favoriteName: String? = null,\n\n    @Ignore\n    override var favoriteNote: String? = null,\n) : GalleryInfo\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/FavListUrlBuilder.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport android.os.Parcelable\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.network.UrlBuilder\nimport com.hippo.util.encodeUTF8\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nclass FavListUrlBuilder(\n    private var mPrev: String? = null,\n    private var mNext: String? = null,\n    var jumpTo: String? = null,\n    var keyword: String? = null,\n    var favCat: Int = FAV_CAT_ALL,\n) : Parcelable {\n    fun setIndex(index: String?, isNext: Boolean) {\n        mNext = index.takeIf { isNext }\n        mPrev = index.takeUnless { isNext }\n    }\n\n    fun build(): String {\n        val ub = UrlBuilder(EhUrl.favoritesUrl)\n        if (isValidFavCat(favCat)) {\n            ub.addQuery(\"favcat\", favCat.toString())\n        } else if (favCat == FAV_CAT_ALL) {\n            ub.addQuery(\"favcat\", \"all\")\n        }\n        keyword?.takeIf { it.isNotBlank() }?.let { ub.addQuery(\"f_search\", encodeUTF8(it)) }\n        mPrev?.takeIf { it.isNotEmpty() }?.let { ub.addQuery(\"prev\", it) }\n        mNext?.takeIf { it.isNotEmpty() }?.let { ub.addQuery(\"next\", it) }\n        jumpTo?.takeIf { it.isNotEmpty() }?.let { ub.addQuery(\"seek\", it) }\n        return ub.build()\n    }\n\n    companion object {\n        const val FAV_CAT_ALL = -1\n        const val FAV_CAT_LOCAL = -2\n        fun isValidFavCat(favCat: Int): Boolean = favCat in 0..9\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/GalleryComment.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nclass GalleryComment(\n    // 0 for uploader comment. can't vote\n    var id: Long = 0,\n    var score: Int = 0,\n    var editable: Boolean = false,\n    var voteUpAble: Boolean = false,\n    var voteUpEd: Boolean = false,\n    var voteDownAble: Boolean = false,\n    var voteDownEd: Boolean = false,\n    var uploader: Boolean = false,\n    var voteState: String? = null,\n    var time: Long = 0,\n    var user: String? = null,\n    var comment: String? = null,\n    var lastEdited: Long = 0,\n) : Parcelable\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/GalleryCommentList.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nclass GalleryCommentList(\n    var comments: Array<GalleryComment>?,\n    var hasMore: Boolean,\n) : Parcelable\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/GalleryDetail.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport com.hippo.ehviewer.client.data.GalleryInfo.Companion.S_LANGS\nimport kotlinx.parcelize.IgnoredOnParcel\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nclass GalleryDetail(\n    val galleryInfo: GalleryInfo = BaseGalleryInfo(),\n    var apiUid: Long = -1L,\n    var apiKey: String? = null,\n    var torrentCount: Int = 0,\n    var torrentUrl: String? = null,\n    var archiveUrl: String? = null,\n    var parent: String? = null,\n    var newerVersions: ArrayList<GalleryInfo> = arrayListOf(),\n    var visible: String? = null,\n    var language: String? = null,\n    var size: String? = null,\n    var favoriteCount: Int = 0,\n    var isFavorited: Boolean = false,\n    var ratingCount: Int = 0,\n    var tags: Array<GalleryTagGroup>? = null,\n    var comments: GalleryCommentList? = null,\n    var previewPages: Int = 0,\n\n    @IgnoredOnParcel\n    var previewSet: PreviewSet? = null,\n) : AbstractGalleryInfo by galleryInfo,\n    GalleryInfo {\n    override fun generateSLang() {\n        val index = LANGUAGES.indexOf(language)\n        if (index != -1) {\n            simpleLanguage = S_LANGS[index]\n        }\n    }\n\n    companion object {\n        private val LANGUAGES = arrayOf(\n            \"English\",\n            \"Chinese\",\n            \"Spanish\",\n            \"Korean\",\n            \"Russian\",\n            \"French\",\n            \"Portuguese\",\n            \"Thai\",\n            \"German\",\n            \"Italian\",\n            \"Vietnamese\",\n            \"Polish\",\n            \"Hungarian\",\n            \"Dutch\",\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/GalleryInfo.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport android.os.Parcelable\nimport java.util.regex.Pattern\n\ninterface GalleryInfo :\n    AbstractGalleryInfo,\n    Parcelable {\n    fun generateSLang() {\n        simpleLanguage = simpleTags?.let { generateSLangFromTags(it) }\n            ?: title?.let { generateSLangFromTitle(it) }\n    }\n\n    private fun generateSLangFromTags(simpleTags: Array<String>): String? {\n        for (tag in simpleTags) {\n            for (i in S_LANGS.indices) {\n                if (S_LANG_TAGS[i] == tag) {\n                    return S_LANGS[i]\n                }\n            }\n        }\n        return null\n    }\n\n    private fun generateSLangFromTitle(title: String): String? {\n        for (i in S_LANGS.indices) {\n            if (S_LANG_PATTERNS[i].matcher(title).find()) {\n                return S_LANGS[i]\n            }\n        }\n        return null\n    }\n\n    companion object {\n        /**\n         * ISO 639-1\n         */\n        private const val S_LANG_JA = \"JA\"\n        private const val S_LANG_EN = \"EN\"\n        private const val S_LANG_ZH = \"ZH\"\n        private const val S_LANG_NL = \"NL\"\n        private const val S_LANG_FR = \"FR\"\n        private const val S_LANG_DE = \"DE\"\n        private const val S_LANG_HU = \"HU\"\n        private const val S_LANG_IT = \"IT\"\n        private const val S_LANG_KO = \"KO\"\n        private const val S_LANG_PL = \"PL\"\n        private const val S_LANG_PT = \"PT\"\n        private const val S_LANG_RU = \"RU\"\n        private const val S_LANG_ES = \"ES\"\n        private const val S_LANG_TH = \"TH\"\n        private const val S_LANG_VI = \"VI\"\n        val S_LANGS = arrayOf(\n            S_LANG_EN,\n            S_LANG_ZH,\n            S_LANG_ES,\n            S_LANG_KO,\n            S_LANG_RU,\n            S_LANG_FR,\n            S_LANG_PT,\n            S_LANG_TH,\n            S_LANG_DE,\n            S_LANG_IT,\n            S_LANG_VI,\n            S_LANG_PL,\n            S_LANG_HU,\n            S_LANG_NL,\n        )\n        val S_LANG_PATTERNS = arrayOf(\n            Pattern.compile(\n                \"[(\\\\[]eng(?:lish)?[)\\\\]]|英訳\",\n                Pattern.CASE_INSENSITIVE,\n            ),\n            // [(（\\[]ch(?:inese)?[)）\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳\n            Pattern.compile(\n                \"[(\\uFF08\\\\[]ch(?:inese)?[)\\uFF09\\\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳\",\n                Pattern.CASE_INSENSITIVE,\n            ),\n            Pattern.compile(\n                \"[(\\\\[]spanish[)\\\\]]|[(\\\\[]Español[)\\\\]]|スペイン翻訳\",\n                Pattern.CASE_INSENSITIVE,\n            ),\n            Pattern.compile(\"[(\\\\[]korean?[)\\\\]]|韓国翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\"[(\\\\[]rus(?:sian)?[)\\\\]]|ロシア翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\"[(\\\\[]fr(?:ench)?[)\\\\]]|フランス翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\"[(\\\\[]portuguese|ポルトガル翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\n                \"[(\\\\[]thai(?: ภาษาไทย)?[)\\\\]]|แปลไทย|タイ翻訳\",\n                Pattern.CASE_INSENSITIVE,\n            ),\n            Pattern.compile(\"[(\\\\[]german[)\\\\]]|ドイツ翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\"[(\\\\[]italiano?[)\\\\]]|イタリア翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\n                \"[(\\\\[]vietnamese(?: Tiếng Việt)?[)\\\\]]|ベトナム翻訳\",\n                Pattern.CASE_INSENSITIVE,\n            ),\n            Pattern.compile(\"[(\\\\[]polish[)\\\\]]|ポーランド翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\"[(\\\\[]hun(?:garian)?[)\\\\]]|ハンガリー翻訳\", Pattern.CASE_INSENSITIVE),\n            Pattern.compile(\"[(\\\\[]dutch[)\\\\]]|オランダ翻訳\", Pattern.CASE_INSENSITIVE),\n        )\n        val S_LANG_TAGS = arrayOf(\n            \"language:english\",\n            \"language:chinese\",\n            \"language:spanish\",\n            \"language:korean\",\n            \"language:russian\",\n            \"language:french\",\n            \"language:portuguese\",\n            \"language:thai\",\n            \"language:german\",\n            \"language:italian\",\n            \"language:vietnamese\",\n            \"language:polish\",\n            \"language:hungarian\",\n            \"language:dutch\",\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/GalleryPreview.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport android.os.Parcelable\nimport com.hippo.util.isAtLeastQ\nimport com.hippo.widget.LoadImageView\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\nclass GalleryPreview(\n    var imageKey: String? = null,\n    var imageUrl: String? = null,\n    var pageUrl: String? = null,\n    var position: Int = 0,\n    var offsetX: Int = Int.MIN_VALUE,\n    var offsetY: Int = Int.MIN_VALUE,\n    var clipWidth: Int = Int.MIN_VALUE,\n    var clipHeight: Int = Int.MIN_VALUE,\n) : Parcelable {\n    fun load(view: LoadImageView) {\n        view.setClip(offsetX, offsetY, clipWidth, clipHeight)\n        view.load(imageKey!!, imageUrl!!, offsetY == Int.MIN_VALUE || isAtLeastQ)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/GalleryTagGroup.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Suppress(\"JavaDefaultMethodsNotOverriddenByDelegation\")\n@Parcelize\nclass GalleryTagGroup(\n    private val mTagList: ArrayList<String> = arrayListOf(),\n    var groupName: String? = null,\n) : Parcelable, MutableList<String> by mTagList\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/LargePreviewSet.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport com.hippo.ehviewer.client.getLargePreviewKey\nimport com.hippo.ehviewer.client.thumbUrl\nimport com.hippo.widget.LoadImageView\nimport com.hippo.yorozuya.collect.IntList\n\nclass LargePreviewSet(\n    private val mPositionList: IntList = IntList(),\n    private val mImageUrlList: ArrayList<String> = arrayListOf(),\n    private val mPageUrlList: ArrayList<String> = arrayListOf(),\n    private val mSha1List: ArrayList<String> = arrayListOf(),\n) : PreviewSet() {\n    fun addItem(index: Int, imageUrl: String, pageUrl: String, sha1: String) {\n        mPositionList.add(index)\n        mImageUrlList.add(imageUrl)\n        mPageUrlList.add(pageUrl)\n        mSha1List.add(sha1)\n    }\n\n    override fun size(): Int = mImageUrlList.size\n\n    override fun getPosition(index: Int): Int = mPositionList[index]\n\n    override fun getPageUrlAt(index: Int): String = mPageUrlList[index]\n\n    override fun getSha1At(index: Int): String = mSha1List[index]\n\n    override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {\n        val galleryPreview = GalleryPreview()\n        galleryPreview.position = mPositionList[index]\n        galleryPreview.imageKey = getLargePreviewKey(gid, galleryPreview.position)\n        galleryPreview.imageUrl = mImageUrlList[index]\n        galleryPreview.pageUrl = mPageUrlList[index]\n        return galleryPreview\n    }\n\n    override fun load(view: LoadImageView, gid: Long, index: Int) {\n        view.resetClip()\n        view.load(\n            getLargePreviewKey(gid, mPositionList[index]),\n            mImageUrlList[index].thumbUrl,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/ListUrlBuilder.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport android.os.Parcelable\nimport android.text.TextUtils\nimport androidx.annotation.IntDef\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.dao.QuickSearch\nimport com.hippo.ehviewer.widget.AdvanceSearchTable\nimport com.hippo.network.UrlBuilder\nimport com.hippo.util.encodeUTF8\nimport com.hippo.yorozuya.NumberUtils\nimport com.hippo.yorozuya.StringUtils\nimport java.io.UnsupportedEncodingException\nimport java.net.URLDecoder\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class ListUrlBuilder(\n    @get:Mode @param:Mode\n    var mode: Int = MODE_NORMAL,\n    private var mPrev: String? = null,\n    private var mNext: String? = null,\n    private var mJumpTo: String? = null,\n    var category: Int = EhUtils.NONE,\n    private var mKeyword: String? = null,\n    var hash: String? = null,\n    var advanceSearch: Int = -1,\n    var minRating: Int = -1,\n    var pageFrom: Int = -1,\n    var pageTo: Int = -1,\n    var isShowExpunged: Boolean = false,\n) : Parcelable {\n    fun reset() {\n        mode = MODE_NORMAL\n        mPrev = null\n        mNext = null\n        mJumpTo = null\n        this.category = EhUtils.NONE\n        mKeyword = null\n        advanceSearch = -1\n        minRating = -1\n        pageFrom = -1\n        pageTo = -1\n        isShowExpunged = false\n        hash = null\n    }\n\n    fun setIndex(index: String?, isNext: Boolean = true) {\n        mNext = index?.takeIf { isNext }\n        mPrev = index?.takeUnless { isNext }\n    }\n\n    fun setJumpTo(jumpTo: String?) {\n        mJumpTo = jumpTo\n    }\n\n    var keyword: String?\n        get() = if (MODE_UPLOADER == mode) \"uploader:$mKeyword\" else mKeyword\n        set(keyword) {\n            mKeyword = keyword\n        }\n\n    fun set(q: QuickSearch) {\n        mode = q.mode\n        this.category = q.category\n        mKeyword = q.keyword\n        advanceSearch = q.advanceSearch\n        minRating = q.minRating\n        pageFrom = q.pageFrom\n        pageTo = q.pageTo\n        isShowExpunged = false\n    }\n\n    fun toQuickSearch(): QuickSearch {\n        val q = QuickSearch()\n        q.mode = mode\n        q.category = this.category\n        q.keyword = mKeyword\n        q.advanceSearch = advanceSearch\n        q.minRating = minRating\n        q.pageFrom = pageFrom\n        q.pageTo = pageTo\n        return q\n    }\n\n    fun equalsQuickSearch(q: QuickSearch?): Boolean {\n        if (null == q) {\n            return false\n        }\n        if (q.mode != mode) {\n            return false\n        }\n        if (q.category != this.category) {\n            return false\n        }\n        if (!StringUtils.equals(q.keyword, mKeyword)) {\n            return false\n        }\n        if (q.advanceSearch != advanceSearch) {\n            return false\n        }\n        if (q.minRating != minRating) {\n            return false\n        }\n        return if (q.pageFrom != pageFrom) {\n            false\n        } else {\n            q.pageTo == pageTo\n        }\n    }\n\n    /**\n     * @param query xxx=yyy&mmm=nnn\n     */\n    // TODO page\n    fun setQuery(query: String?) {\n        reset()\n        if (TextUtils.isEmpty(query)) {\n            return\n        }\n        val queries = StringUtils.split(query, '&')\n        var category = 0\n        var keyword: String? = null\n        var enableAdvanceSearch = false\n        var advanceSearch = 0\n        var enableMinRating = false\n        var minRating = -1\n        var enablePage = false\n        var pageFrom = -1\n        var pageTo = -1\n        for (str in queries) {\n            val index = str.indexOf('=')\n            if (index < 0) {\n                continue\n            }\n            val key = str.substring(0, index)\n            val value = str.substring(index + 1)\n            when (key) {\n                \"f_cats\" -> {\n                    val cats = NumberUtils.parseIntSafely(value, EhUtils.ALL_CATEGORY)\n                    category = category or (cats.inv() and EhUtils.ALL_CATEGORY)\n                }\n                \"f_doujinshi\" -> if (\"1\" == value) {\n                    category = category or EhUtils.DOUJINSHI\n                }\n                \"f_manga\" -> if (\"1\" == value) {\n                    category = category or EhUtils.MANGA\n                }\n                \"f_artistcg\" -> if (\"1\" == value) {\n                    category = category or EhUtils.ARTIST_CG\n                }\n                \"f_gamecg\" -> if (\"1\" == value) {\n                    category = category or EhUtils.GAME_CG\n                }\n                \"f_western\" -> if (\"1\" == value) {\n                    category = category or EhUtils.WESTERN\n                }\n                \"f_non-h\" -> if (\"1\" == value) {\n                    category = category or EhUtils.NON_H\n                }\n                \"f_imageset\" -> if (\"1\" == value) {\n                    category = category or EhUtils.IMAGE_SET\n                }\n                \"f_cosplay\" -> if (\"1\" == value) {\n                    category = category or EhUtils.COSPLAY\n                }\n                \"f_asianporn\" -> if (\"1\" == value) {\n                    category = category or EhUtils.ASIAN_PORN\n                }\n                \"f_misc\" -> if (\"1\" == value) {\n                    category = category or EhUtils.MISC\n                }\n                \"f_search\" -> try {\n                    keyword = URLDecoder.decode(value, \"utf-8\")\n                } catch (_: UnsupportedEncodingException) {\n                    // Ignore\n                } catch (_: IllegalArgumentException) {\n                }\n                \"advsearch\" -> if (\"1\" == value) {\n                    enableAdvanceSearch = true\n                }\n                \"f_sh\" -> if (\"on\" == value) {\n                    advanceSearch = advanceSearch or AdvanceSearchTable.SH\n                }\n                \"f_sto\" -> if (\"on\" == value) {\n                    advanceSearch = advanceSearch or AdvanceSearchTable.STO\n                }\n                \"f_sfl\" -> if (\"on\" == value) {\n                    advanceSearch = advanceSearch or AdvanceSearchTable.SFL\n                }\n                \"f_sfu\" -> if (\"on\" == value) {\n                    advanceSearch = advanceSearch or AdvanceSearchTable.SFU\n                }\n                \"f_sft\" -> if (\"on\" == value) {\n                    advanceSearch = advanceSearch or AdvanceSearchTable.SFT\n                }\n                \"f_sr\" -> if (\"on\" == value) {\n                    enableMinRating = true\n                }\n                \"f_srdd\" -> minRating = NumberUtils.parseIntSafely(value, -1)\n                \"f_sp\" -> if (\"on\" == value) {\n                    enablePage = true\n                }\n                \"f_spf\" -> pageFrom = NumberUtils.parseIntSafely(value, -1)\n                \"f_spt\" -> pageTo = NumberUtils.parseIntSafely(value, -1)\n                \"f_shash\" -> hash = value\n            }\n        }\n        this.category = category\n        mKeyword = keyword\n        if (enableAdvanceSearch) {\n            this.advanceSearch = advanceSearch\n            if (enableMinRating) {\n                this.minRating = minRating\n            } else {\n                this.minRating = -1\n            }\n            if (enablePage) {\n                this.pageFrom = pageFrom\n                this.pageTo = pageTo\n            } else {\n                this.pageFrom = -1\n                this.pageTo = -1\n            }\n        } else {\n            this.advanceSearch = -1\n        }\n    }\n\n    fun build(): String = when (mode) {\n        MODE_NORMAL, MODE_SUBSCRIPTION -> {\n            val url: String = if (mode == MODE_NORMAL) {\n                EhUrl.host\n            } else {\n                EhUrl.watchedUrl\n            }\n            val ub = UrlBuilder(url)\n            if (this.category != EhUtils.NONE) {\n                ub.addQuery(\"f_cats\", category.inv() and EhUtils.ALL_CATEGORY)\n            }\n            // Search key\n            mKeyword?.run {\n                val keyword = trim { it <= ' ' }\n                if (keyword.isNotEmpty()) {\n                    ub.addQuery(\"f_search\", encodeUTF8(this))\n                }\n            }\n            hash?.let {\n                ub.addQuery(\"f_shash\", it)\n            }\n            mJumpTo?.let {\n                ub.addQuery(\"seek\", it)\n            }\n            mPrev?.let {\n                ub.addQuery(\"prev\", it)\n            }\n            mNext?.let {\n                ub.addQuery(\"next\", it)\n            }\n            // Advance search\n            if (advanceSearch != -1) {\n                ub.addQuery(\"advsearch\", \"1\")\n                if (advanceSearch and AdvanceSearchTable.SH != 0) ub.addQuery(\"f_sh\", \"on\")\n                if (advanceSearch and AdvanceSearchTable.STO != 0) ub.addQuery(\"f_sto\", \"on\")\n                if (advanceSearch and AdvanceSearchTable.SFL != 0) ub.addQuery(\"f_sfl\", \"on\")\n                if (advanceSearch and AdvanceSearchTable.SFU != 0) ub.addQuery(\"f_sfu\", \"on\")\n                if (advanceSearch and AdvanceSearchTable.SFT != 0) ub.addQuery(\"f_sft\", \"on\")\n                // Set min star\n                if (minRating != -1) {\n                    ub.addQuery(\"f_sr\", \"on\")\n                    ub.addQuery(\"f_srdd\", minRating)\n                }\n                // Pages\n                if (pageFrom != -1 || pageTo != -1) {\n                    ub.addQuery(\"f_sp\", \"on\")\n                    ub.addQuery(\"f_spf\", if (pageFrom != -1) pageFrom.toString() else \"\")\n                    ub.addQuery(\"f_spt\", if (pageTo != -1) pageTo.toString() else \"\")\n                }\n            }\n            ub.build()\n        }\n        MODE_UPLOADER -> {\n            val sb = StringBuilder(EhUrl.host)\n            mKeyword?.let {\n                sb.append(\"uploader/\")\n                sb.append(encodeUTF8(it))\n            }\n            mPrev?.let {\n                sb.append(\"?prev=\").append(it)\n            }\n            mNext?.let {\n                sb.append(\"?next=\").append(it)\n            }\n            mJumpTo?.let {\n                sb.append(\"&seek=\").append(it)\n            }\n            sb.toString()\n        }\n        MODE_TAG -> {\n            val sb = StringBuilder(EhUrl.host)\n            mKeyword?.let {\n                sb.append(\"tag/\")\n                sb.append(encodeUTF8(it))\n            }\n            mPrev?.let {\n                sb.append(\"?prev=\").append(it)\n            }\n            mNext?.let {\n                sb.append(\"?next=\").append(it)\n            }\n            mJumpTo?.let {\n                sb.append(\"&seek=\").append(it)\n            }\n            sb.toString()\n        }\n        MODE_WHATS_HOT -> EhUrl.popularUrl\n        MODE_IMAGE_SEARCH -> {\n            val ub = UrlBuilder(EhUrl.host)\n            hash?.let {\n                ub.addQuery(\"f_shash\", it)\n            }\n            ub.build()\n        }\n        MODE_TOPLIST -> {\n            val sb = StringBuilder(EhUrl.HOST_E)\n            sb.append(\"toplist.php?tl=\")\n            mKeyword.orEmpty().let {\n                sb.append(encodeUTF8(it))\n            }\n            mJumpTo?.let {\n                sb.append(\"&p=\").append(it)\n            }\n            sb.toString()\n        }\n        else -> throw IllegalStateException(\"Unexpected value: $mode\")\n    }\n\n    @IntDef(\n        MODE_NORMAL,\n        MODE_UPLOADER,\n        MODE_TAG,\n        MODE_WHATS_HOT,\n        MODE_IMAGE_SEARCH,\n        MODE_SUBSCRIPTION,\n        MODE_TOPLIST,\n    )\n    @Retention(AnnotationRetention.SOURCE)\n    private annotation class Mode\n    companion object {\n        const val MODE_NORMAL = 0x0\n        const val MODE_UPLOADER = 0x1\n        const val MODE_TAG = 0x2\n        const val MODE_WHATS_HOT = 0x3\n        const val MODE_IMAGE_SEARCH = 0x4\n        const val MODE_SUBSCRIPTION = 0x5\n        const val MODE_TOPLIST = 0x6\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/NormalPreviewSet.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport com.hippo.ehviewer.client.getNormalPreviewKey\nimport com.hippo.util.isAtLeastQ\nimport com.hippo.widget.LoadImageView\nimport com.hippo.yorozuya.collect.IntList\n\nclass NormalPreviewSet(\n    private var mPositionList: IntList = IntList(),\n    private var mImageKeyList: ArrayList<String> = arrayListOf(),\n    private var mImageUrlList: ArrayList<String> = arrayListOf(),\n    private var mOffsetXList: IntList = IntList(),\n    private var mOffsetYList: IntList = IntList(),\n    private var mClipWidthList: IntList = IntList(),\n    private var mClipHeightList: IntList = IntList(),\n    private var mPageUrlList: ArrayList<String> = arrayListOf(),\n    private val mSha1List: ArrayList<String> = arrayListOf(),\n) : PreviewSet() {\n    fun addItem(\n        position: Int,\n        imageUrl: String,\n        xOffset: Int,\n        yOffset: Int,\n        width: Int,\n        height: Int,\n        pageUrl: String,\n        sha1: String,\n    ) {\n        mPositionList.add(position)\n        mImageKeyList.add(getNormalPreviewKey(imageUrl))\n        mImageUrlList.add(imageUrl)\n        mOffsetXList.add(xOffset)\n        mOffsetYList.add(yOffset)\n        mClipWidthList.add(width)\n        mClipHeightList.add(height)\n        mPageUrlList.add(pageUrl)\n        mSha1List.add(sha1)\n    }\n\n    override fun size(): Int = mPositionList.size\n\n    override fun getPosition(index: Int): Int = mPositionList[index]\n\n    override fun getPageUrlAt(index: Int): String = mPageUrlList[index]\n\n    override fun getSha1At(index: Int): String = mSha1List[index]\n\n    override fun getGalleryPreview(gid: Long, index: Int): GalleryPreview {\n        val galleryPreview = GalleryPreview()\n        galleryPreview.position = mPositionList[index]\n        galleryPreview.imageKey = mImageKeyList[index]\n        galleryPreview.imageUrl = mImageUrlList[index]\n        galleryPreview.pageUrl = mPageUrlList[index]\n        galleryPreview.offsetX = mOffsetXList[index]\n        galleryPreview.offsetY = mOffsetYList[index]\n        galleryPreview.clipWidth = mClipWidthList[index]\n        galleryPreview.clipHeight = mClipHeightList[index]\n        return galleryPreview\n    }\n\n    override fun load(view: LoadImageView, gid: Long, index: Int) {\n        view.setClip(\n            mOffsetXList[index],\n            mOffsetYList[index],\n            mClipWidthList[index],\n            mClipHeightList[index],\n        )\n        view.load(mImageKeyList[index], mImageUrlList[index], isAtLeastQ)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/data/PreviewSet.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.data\n\nimport com.hippo.widget.LoadImageView\n\nabstract class PreviewSet {\n    abstract fun size(): Int\n    abstract fun getPosition(index: Int): Int\n    abstract fun getPageUrlAt(index: Int): String\n    abstract fun getSha1At(index: Int): String\n    abstract fun getGalleryPreview(gid: Long, index: Int): GalleryPreview\n    abstract fun load(view: LoadImageView, gid: Long, index: Int)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/CloudflareBypassException.kt",
    "content": "package com.hippo.ehviewer.client.exception\n\nimport com.hippo.ehviewer.R\n\nclass CloudflareBypassException : EhException(R.string.cloudflare_bypass_failed)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/EhException.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.exception\n\nimport androidx.annotation.StringRes\nimport com.hippo.ehviewer.GetText\n\nopen class EhException : Exception {\n    constructor(detailMessage: String?) : super(detailMessage)\n    constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)\n    constructor(@StringRes message: Int) : super(GetText.getString(message))\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/InsufficientFundsException.kt",
    "content": "package com.hippo.ehviewer.client.exception\n\nimport com.hippo.ehviewer.R\n\nclass InsufficientFundsException : EhException(R.string.insufficient_funds)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/NoHAtHClientException.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.exception\n\nimport com.hippo.ehviewer.R\n\nclass NoHAtHClientException : EhException(R.string.download_archive_failure_no_hath)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/NotLoggedInException.kt",
    "content": "/*\n * Copyright 2023 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.client.exception\n\nimport com.hippo.ehviewer.R\n\nclass NotLoggedInException : EhException(R.string.need_sign_in)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/OffensiveException.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.ehviewer.client.exception\n\n/**\n * It is an exception for get offensive tip for g.e-hentai.org\n */\nclass OffensiveException : EhException(\"OFFENSIVE\")\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/ParseException.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.exception\n\nclass ParseException : EhException {\n    constructor(detailMessage: String?) : super(detailMessage)\n\n    constructor(detailMessage: String?, cause: Throwable?) : super(detailMessage, cause)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/PiningException.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.ehviewer.client.exception\n\nclass PiningException : EhException(\"pining for the fjords\")\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/exception/QuotaExceededException.kt",
    "content": "/*\n * Copyright 2023 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.client.exception\n\nimport com.hippo.ehviewer.R\n\nclass QuotaExceededException : EhException(R.string.error_509)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/ArchiveParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.InsufficientFundsException\nimport com.hippo.ehviewer.client.exception.NoHAtHClientException\nimport org.jsoup.Jsoup\n\nobject ArchiveParser {\n    private val PATTERN_CURRENT_FUNDS =\n        Regex(\"<p>([\\\\d,]+) GP \\\\[[^]]*] &nbsp; ([\\\\d,]+) Credits \\\\[[^]]*]</p>\")\n    private val PATTERN_HATH_ARCHIVE =\n        Regex(\"<p><a href=\\\"[^\\\"]*\\\" onclick=\\\"return do_hathdl\\\\('([0-9]+|org)'\\\\)\\\">([^<]+)</a></p>\\\\s*<p>([\\\\w. ]+)</p>\\\\s*<p>([\\\\w. ]+)</p>\")\n    private const val ERROR_NEED_HATH_CLIENT =\n        \"You must have a H@H client assigned to your account to use this feature.\"\n    private const val ERROR_INSUFFICIENT_FUNDS =\n        \"You do not have enough funds to download this archive.\"\n\n    fun parse(body: String): Result {\n        val archiveList = ArrayList<Archive>()\n        Jsoup.parse(body).select(\"#db>div>div\").forEach { element ->\n            if (element.childrenSize() > 0 && !element.attr(\"style\").contains(\"color:#CCCCCC\")) {\n                runCatching {\n                    val res = element.selectFirst(\"form>input\")!!.attr(\"value\")\n                    val name = element.selectFirst(\"form>div>input\")!!.attr(\"value\")\n                    val size = element.selectFirst(\"p>strong\")!!.text()\n                    val cost = element.selectFirst(\"div>strong\")!!.text().replace(\",\", \"\")\n                    Archive(res, name, size, cost, false)\n                }.onSuccess {\n                    archiveList.add(it)\n                }.onFailure {\n                    it.printStackTrace()\n                }\n            }\n        }\n        PATTERN_HATH_ARCHIVE.findAll(body).forEach { matchResult ->\n            val (res, name, size, cost) = matchResult.groupValues.slice(1..4)\n                .map { ParserUtils.trim(it) }\n            val item = Archive(res, name, size, cost, true)\n            archiveList.add(item)\n        }\n        val result = Result(archiveList, null)\n        PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {\n            val fundsGP = ParserUtils.parseInt(get(1), 0)\n            val fundsC = ParserUtils.parseInt(get(2), 0)\n            val funds = HomeParser.Funds(fundsGP, fundsC)\n            result.funds = funds\n        }\n        return result\n    }\n\n    fun parseArchiveUrl(body: String): String? {\n        if (body.contains(ERROR_NEED_HATH_CLIENT)) {\n            throw NoHAtHClientException()\n        } else if (body.contains(ERROR_INSUFFICIENT_FUNDS)) {\n            throw InsufficientFundsException()\n        }\n        return Jsoup.parse(body).selectFirst(\"#continue>a[href]\")\n            ?.let { it.attr(\"href\") + \"?start=1\" }\n        // TODO: Check more errors\n    }\n\n    class Archive(\n        val res: String,\n        val name: String,\n        val size: String,\n        val cost: String,\n        val isHAtH: Boolean,\n    )\n\n    class Result(val archiveList: List<Archive>, var funds: HomeParser.Funds?)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/EventPaneParser.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.client.parser\n\nimport org.jsoup.Jsoup\n\nobject EventPaneParser {\n    fun parse(body: String): String? = Jsoup.parse(body).getElementById(\"eventpane\")?.html()\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/FavoritesParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.NotLoggedInException\nimport com.hippo.ehviewer.client.exception.ParseException\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.JsoupUtils\nimport org.jsoup.Jsoup\n\nobject FavoritesParser {\n    fun parse(body: String): Result {\n        if (body.contains(\"This page requires you to log on.</p>\")) {\n            throw NotLoggedInException()\n        }\n        val catArray = arrayOfNulls<String>(10)\n        val countArray = IntArray(10)\n        val d = Jsoup.parse(body)\n        runCatching {\n            val ido = JsoupUtils.getElementByClass(d, \"ido\")\n            val fps = ido!!.getElementsByClass(\"fp\")\n            // Last one is \"fp fps\"\n            check(fps.size == 11)\n            for (i in 0..9) {\n                val fp = fps[i]\n                countArray[i] = ParserUtils.parseInt(fp.child(0).text(), 0)\n                catArray[i] = ParserUtils.trim(fp.child(2).text())\n            }\n        }.onFailure {\n            ExceptionUtils.throwIfFatal(it)\n            it.printStackTrace()\n            throw ParseException(\"Parse favorites error\")\n        }\n        val result = GalleryListParser.parse(d, body)\n        return Result(catArray.requireNoNulls(), countArray, result)\n    }\n\n    class Result(\n        val catArray: Array<String>,\n        val countArray: IntArray,\n        galleryListResult: GalleryListParser.Result,\n    ) {\n        val prev = galleryListResult.prev\n        val next = galleryListResult.next\n        val galleryInfoList = galleryListResult.galleryInfoList\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/ForumsParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.exception.NotLoggedInException\nimport com.hippo.util.ExceptionUtils\nimport org.jsoup.Jsoup\n\nobject ForumsParser {\n    fun parse(body: String): String = runCatching {\n        val d = Jsoup.parse(body, EhUrl.URL_FORUMS)\n        val userlinks = d.getElementById(\"userlinks\")\n        val child = userlinks!!.child(0).child(0).child(0)\n        child.attr(\"href\")\n    }.getOrElse {\n        ExceptionUtils.throwIfFatal(it)\n        throw NotLoggedInException()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryApiParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.EhUtils.getCategory\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.yorozuya.NumberUtils\nimport org.json.JSONObject\n\nobject GalleryApiParser {\n    fun parse(body: String, galleryInfoList: List<GalleryInfo>) {\n        val jo = JSONObject(body)\n        val ja = jo.getJSONArray(\"gmetadata\")\n        for (i in 0 until ja.length()) {\n            val g = ja.getJSONObject(i)\n            val gid = g.getLong(\"gid\")\n            val gi = galleryInfoList.find { it.gid == gid } ?: continue\n            gi.title = ParserUtils.trim(g.getString(\"title\"))\n            gi.titleJpn = ParserUtils.trim(g.getString(\"title_jpn\"))\n            gi.category = getCategory(g.getString(\"category\"))\n            gi.thumb = g.getString(\"thumb\")\n            gi.uploader = g.getString(\"uploader\")\n            gi.posted =\n                ParserUtils.formatDate(ParserUtils.parseLong(g.getString(\"posted\"), 0) * 1000)\n            gi.rating = NumberUtils.parseFloatSafely(g.getString(\"rating\"), 0.0f)\n            // tags\n            val tagJa = g.getJSONArray(\"tags\")\n            gi.simpleTags = (0 until tagJa.length()).map { tagJa.getString(it) }.toTypedArray()\n            gi.pages = NumberUtils.parseIntSafely(g.getString(\"filecount\"), 0)\n            gi.generateSLang()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhFilter\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.EhUtils.getCategory\nimport com.hippo.ehviewer.client.data.BaseGalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryComment\nimport com.hippo.ehviewer.client.data.GalleryCommentList\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.client.data.GalleryTagGroup\nimport com.hippo.ehviewer.client.data.LargePreviewSet\nimport com.hippo.ehviewer.client.data.NormalPreviewSet\nimport com.hippo.ehviewer.client.data.PreviewSet\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.client.exception.OffensiveException\nimport com.hippo.ehviewer.client.exception.ParseException\nimport com.hippo.ehviewer.client.exception.PiningException\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.JsoupUtils\nimport com.hippo.util.toEpochMillis\nimport com.hippo.yorozuya.NumberUtils\nimport com.hippo.yorozuya.StringUtils\nimport com.hippo.yorozuya.trimAnd\nimport com.hippo.yorozuya.unescapeXml\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.format.MonthNames\nimport kotlinx.datetime.format.char\nimport org.jsoup.Jsoup\nimport org.jsoup.nodes.Document\nimport org.jsoup.nodes.Element\nimport org.jsoup.nodes.Node\nimport org.jsoup.select.Elements\nimport org.jsoup.select.NodeTraversor\nimport org.jsoup.select.NodeVisitor\n\nobject GalleryDetailParser {\n    private val PATTERN_ERROR = Regex(\"<div class=\\\"d\\\">\\n<p>([^<]+)</p>\")\n    private val PATTERN_DETAIL = Regex(\n        \"var gid = (\\\\d+);.+?var token = \\\"([a-f0-9]+)\\\";.+?var apiuid = ([\\\\-\\\\d]+);.+?var apikey = \\\"([a-f0-9]+)\\\";\",\n        RegexOption.DOT_MATCHES_ALL,\n    )\n    private val PATTERN_TORRENT =\n        Regex(\"<a[^<>]*onclick=\\\"return popUp\\\\('([^']+)'[^)]+\\\\)\\\">Torrent Download \\\\((\\\\d+)\\\\)</a>\")\n    private val PATTERN_ARCHIVE =\n        Regex(\"<a[^<>]*onclick=\\\"return popUp\\\\('([^']+)'[^)]+\\\\)\\\">Archive Download</a>\")\n    private val PATTERN_COVER = Regex(\"width:(\\\\d+)px; height:(\\\\d+)px.+?url\\\\((.+?)\\\\)\")\n    private val PATTERN_PAGES =\n        Regex(\"<tr><td[^<>]*>Length:</td><td[^<>]*>([\\\\d,]+) pages</td></tr>\")\n    private val PATTERN_PREVIEW_PAGES =\n        Regex(\"<td[^>]+><a[^>]+>([\\\\d,]+)</a></td><td[^>]+>(?:<a[^>]+>)?&gt;(?:</a>)?</td>\")\n    private val PATTERN_PREVIEW =\n        Regex(\"<a href=\\\"([^\\\"]+)\\\">(?:<div>)?<div title=\\\"Page (\\\\d+)(?:[^\\\"]+\\\"){2}\\\\D+(\\\\d+)\\\\D+(\\\\d+)[^(]+\\\\(([^)]+)\\\\)(?: -(\\\\d+))?(?:.*?data-orghash=\\\"([^\\\"]+)\\\")?\")\n    private val PATTERN_NEWER_DATE = Regex(\", added (.+?)<br />\")\n    private val PATTERN_FAVORITE_SLOT =\n        Regex(\"/fav.png\\\\); background-position:0px -(\\\\d+)px\")\n    private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()\n    private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)\n\n    // dd MMMM yyyy, HH:mm\n    private val WEB_COMMENT_DATE_FORMAT = LocalDateTime.Format {\n        day()\n        char(' ')\n        monthName(MonthNames.ENGLISH_FULL)\n        char(' ')\n        year()\n        chars(\", \")\n        hour()\n        char(':')\n        minute()\n    }\n    private const val OFFENSIVE_STRING =\n        \"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>\"\n    private const val PINING_STRING = \"<p>This gallery is pining for the fjords.</p>\"\n\n    @Throws(EhException::class)\n    fun parse(body: String): GalleryDetail {\n        if (body.contains(OFFENSIVE_STRING)) {\n            throw OffensiveException()\n        }\n        if (body.contains(PINING_STRING)) {\n            throw PiningException()\n        }\n\n        // Error info\n        PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }\n        val galleryDetail = GalleryDetail()\n        val document = Jsoup.parse(body)\n        parseDetail(galleryDetail, document, body)\n        galleryDetail.tags = parseTagGroups(document)\n        galleryDetail.comments = parseComments(document)\n        galleryDetail.previewPages = parsePreviewPages(document)\n        galleryDetail.previewSet = parsePreviewSet(body)\n\n        // Generate simpleLanguage for local favorites\n        galleryDetail.generateSLang()\n        return galleryDetail\n    }\n\n    @Throws(ParseException::class)\n    private fun parseDetail(gd: GalleryDetail, d: Document, body: String) {\n        PATTERN_DETAIL.find(body)?.apply {\n            gd.gid = groupValues[1].toLongOrNull() ?: -1L\n            gd.token = groupValues[2]\n            gd.apiUid = groupValues[3].toLongOrNull() ?: -1L\n            gd.apiKey = groupValues[4]\n        } ?: throw ParseException(\"Can't parse gallery detail\")\n        if (gd.gid == -1L) {\n            throw ParseException(\"Can't parse gallery detail\")\n        }\n        PATTERN_TORRENT.find(body)?.run {\n            gd.torrentUrl = groupValues[1].trim().unescapeXml()\n            gd.torrentCount = groupValues[2].toIntOrNull() ?: 0\n        }\n        PATTERN_ARCHIVE.find(body)?.run {\n            gd.archiveUrl = groupValues[1].trim().unescapeXml()\n        }\n        try {\n            val gm = d.getElementsByClass(\"gm\")[0]\n            // Thumb url\n            gm.getElementById(\"gd1\")?.child(0)?.attr(\"style\")?.trim()?.let {\n                gd.thumb = PATTERN_COVER.find(it)?.run {\n                    groupValues[3]\n                }\n            }\n\n            // Title\n            gd.title = gm.getElementById(\"gn\")?.text()?.trim()\n\n            // Jpn title\n            gd.titleJpn = gm.getElementById(\"gj\")?.text()?.trim()\n\n            // Category\n            val gdc = gm.getElementById(\"gdc\")\n            try {\n                var ce = JsoupUtils.getElementByClass(gdc, \"cn\")\n                if (ce == null) {\n                    ce = JsoupUtils.getElementByClass(gdc, \"cs\")\n                }\n                gd.category = getCategory(ce!!.text())\n            } catch (e: Throwable) {\n                ExceptionUtils.throwIfFatal(e)\n                gd.category = EhUtils.UNKNOWN\n            }\n\n            // Uploader\n            val gdn = gm.getElementById(\"gdn\")\n            if (null != gdn) {\n                gd.disowned = gdn.attr(\"style\").contains(\"opacity:0.5\")\n                gd.uploader = StringUtils.trim(gdn.text())\n            } else {\n                gd.uploader = \"\"\n            }\n            val gdd = gm.getElementById(\"gdd\")\n            gd.posted = \"\"\n            gd.parent = \"\"\n            gd.visible = \"\"\n            gd.size = \"\"\n            gd.pages = 0\n            gd.favoriteCount = 0\n            val es = gdd!!.child(0).child(0).children()\n            es.forEach {\n                parseDetailInfo(gd, it)\n            }\n\n            // Rating count\n            val ratingCount = gm.getElementById(\"rating_count\")\n            if (null != ratingCount) {\n                gd.ratingCount = NumberUtils.parseIntSafely(\n                    StringUtils.trim(ratingCount.text()),\n                    0,\n                )\n            } else {\n                gd.ratingCount = 0\n            }\n\n            // Rating\n            val ratingLabel = gm.getElementById(\"rating_label\")\n            if (null != ratingLabel) {\n                val ratingStr = StringUtils.trim(ratingLabel.text())\n                if (\"Not Yet Rated\" == ratingStr) {\n                    gd.rating = -1.0f\n                } else {\n                    val index = ratingStr.indexOf(' ')\n                    if (index == -1 || index >= ratingStr.length) {\n                        gd.rating = 0f\n                    } else {\n                        gd.rating = NumberUtils.parseFloatSafely(ratingStr.substring(index + 1), 0f)\n                    }\n                }\n            } else {\n                gd.rating = -1.0f\n            }\n\n            // Is favorited\n            val gdf = gm.getElementById(\"gdf\")\n            gd.isFavorited = false\n            if (gdf != null) {\n                val favoriteName = StringUtils.trim(gdf.text())\n                if (favoriteName == \"Add to Favorites\") {\n                    gd.favoriteName = null\n                } else {\n                    gd.isFavorited = true\n                    gd.favoriteName = StringUtils.trim(gdf.text())\n                    PATTERN_FAVORITE_SLOT.find(body)?.run {\n                        gd.favoriteSlot = ((groupValues[1].toIntOrNull() ?: 2) - 2) / 19\n                    }\n                }\n            }\n            if (gd.favoriteSlot == -2 && EhDB.containLocalFavorites(gd.gid)) {\n                gd.favoriteSlot = -1\n            }\n        } catch (e: Throwable) {\n            ExceptionUtils.throwIfFatal(e)\n            throw ParseException(\"Can't parse gallery detail\")\n        }\n\n        // Newer version\n        d.getElementById(\"gnd\")?.run {\n            val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()\n            select(\"a\").forEachIndexed { index, element ->\n                val gi = BaseGalleryInfo()\n                val result = GalleryDetailUrlParser.parse(element.attr(\"href\"))\n                if (result != null) {\n                    gi.gid = result.gid\n                    gi.token = result.token\n                    gi.title = StringUtils.trim(element.text())\n                    gi.posted = dates[index]\n                    gd.newerVersions.add(gi)\n                }\n            }\n        }\n    }\n\n    private fun parseDetailInfo(gd: GalleryDetail, e: Element) {\n        val es = e.children()\n        if (es.size < 2) {\n            return\n        }\n        val key = StringUtils.trim(es[0].text())\n        val value = StringUtils.trim(es[1].ownText())\n        if (key.startsWith(\"Posted\")) {\n            gd.posted = value\n        } else if (key.startsWith(\"Parent\")) {\n            val a = es[1].children().first()\n            if (a != null) {\n                gd.parent = a.attr(\"href\")\n            }\n        } else if (key.startsWith(\"Visible\")) {\n            gd.visible = value\n        } else if (key.startsWith(\"Language\")) {\n            gd.language = value\n        } else if (key.startsWith(\"File Size\")) {\n            gd.size = value\n        } else if (key.startsWith(\"Length\")) {\n            val index = value.indexOf(' ')\n            if (index >= 0) {\n                gd.pages = NumberUtils.parseIntSafely(value.substring(0, index), 1)\n            } else {\n                gd.pages = 1\n            }\n        } else if (key.startsWith(\"Favorited\")) {\n            when (value) {\n                \"Never\" -> gd.favoriteCount = 0\n                \"Once\" -> gd.favoriteCount = 1\n                else -> {\n                    val index = value.indexOf(' ')\n                    if (index == -1) {\n                        gd.favoriteCount = 0\n                    } else {\n                        gd.favoriteCount = NumberUtils.parseIntSafely(value.substring(0, index), 0)\n                    }\n                }\n            }\n        }\n    }\n\n    private fun parseTagGroup(element: Element): GalleryTagGroup? = try {\n        val group = GalleryTagGroup()\n        var nameSpace = element.child(0).text()\n        // Remove last ':'\n        nameSpace = nameSpace.substring(0, nameSpace.length - 1)\n        group.groupName = nameSpace\n        val tags = element.child(1).children()\n        tags.forEach {\n            var tag = it.text()\n            // Sometimes parody tag is followed with '|' and english translate, just remove them\n            val index = tag.indexOf('|')\n            if (index >= 0) {\n                tag = tag.substring(0, index).trim()\n            }\n            // Vote status\n            if (it.child(0).hasClass(\"tup\")) {\n                tag = \"_U$tag\"\n            } else if (it.child(0).hasClass(\"tdn\")) {\n                tag = \"_D$tag\"\n            }\n            // Weak tag\n            if (it.hasClass(\"gtw\")) {\n                tag = \"_W$tag\"\n            }\n            // Active tag\n            if (it.hasClass(\"gtl\")) {\n                tag = \"_L$tag\"\n            }\n            group.add(tag)\n        }\n        group.ifEmpty { null }\n    } catch (e: Throwable) {\n        ExceptionUtils.throwIfFatal(e)\n        e.printStackTrace()\n        null\n    }\n\n    /**\n     * Parse tag groups with html parser\n     */\n    fun parseTagGroups(document: Document): Array<GalleryTagGroup>? {\n        return try {\n            val taglist = document.getElementById(\"taglist\")!!\n            if (taglist.children().isEmpty()) return null\n            val tagGroups = taglist.child(0).child(0).children()\n            parseTagGroups(tagGroups)\n        } catch (e: Throwable) {\n            ExceptionUtils.throwIfFatal(e)\n            e.printStackTrace()\n            EMPTY_GALLERY_TAG_GROUP_ARRAY\n        }\n    }\n\n    private fun parseTagGroups(trs: Elements): Array<GalleryTagGroup> = try {\n        trs.mapNotNull { parseTagGroup(it) }.toTypedArray()\n    } catch (e: Throwable) {\n        ExceptionUtils.throwIfFatal(e)\n        e.printStackTrace()\n        EMPTY_GALLERY_TAG_GROUP_ARRAY\n    }\n\n    private fun parseComment(element: Element): GalleryComment? {\n        return try {\n            val comment = GalleryComment()\n            // Id\n            val a = element.previousElementSibling()\n            val name = a!!.attr(\"name\")\n            comment.id = name trimAnd { substring(1).toInt().toLong() }\n            // Editable, vote up and vote down\n            val c4 = JsoupUtils.getElementByClass(element, \"c4\")\n            if (null != c4) {\n                if (\"Uploader Comment\" == c4.text()) {\n                    comment.uploader = true\n                }\n                for (e in c4.children()) {\n                    when (e.text()) {\n                        \"Vote+\" -> {\n                            comment.voteUpAble = true\n                            comment.voteUpEd = StringUtils.trim(e.attr(\"style\")).isNotEmpty()\n                        }\n                        \"Vote-\" -> {\n                            comment.voteDownAble = true\n                            comment.voteDownEd = StringUtils.trim(e.attr(\"style\")).isNotEmpty()\n                        }\n                        \"Edit\" -> comment.editable = true\n                    }\n                }\n            }\n            // Vote state\n            val c7 = JsoupUtils.getElementByClass(element, \"c7\")\n            if (null != c7) {\n                comment.voteState = StringUtils.trim(c7.text())\n            }\n            // Score\n            val c5 = JsoupUtils.getElementByClass(element, \"c5\")\n            if (null != c5) {\n                val es = c5.children()\n                if (!es.isEmpty()) {\n                    comment.score = NumberUtils.parseIntSafely(\n                        StringUtils.trim(es[0].text()),\n                        0,\n                    )\n                }\n            }\n            // Time\n            val c3 = JsoupUtils.getElementByClass(element, \"c3\")\n            val temp = c3!!.ownText()\n            val hasUserName = temp.endsWith(\":\")\n            val time = if (hasUserName) temp.substring(\"Posted on \".length, temp.length - \" by:\".length) else temp.substring(\"Posted on \".length)\n            comment.time = WEB_COMMENT_DATE_FORMAT.parse(time).toEpochMillis()\n            // User\n            comment.user = if (hasUserName) c3.child(0).text() else \"Anonymous\"\n            // Comment\n            val c6 = JsoupUtils.getElementByClass(element, \"c6\")\n            for (e in c6!!.children()) {\n                val tagName = e.tagName()\n                // Fix underline support\n                if (\"span\" == tagName && \"text-decoration:underline;\" == e.attr(\"style\")) {\n                    e.tagName(\"u\")\n                    // Temporary workaround, see https://github.com/jhy/jsoup/issues/1850\n                } else if (\"del\" == tagName) {\n                    e.tagName(\"s\")\n                }\n            }\n            comment.comment = c6.html()\n            // Filter comment\n            if (!comment.uploader) {\n                val sEhFilter = EhFilter\n                if (comment.score <= Settings.commentThreshold || !sEhFilter.filterCommenter(comment.user) || !sEhFilter.filterComment(comment.comment)) {\n                    return null\n                }\n            }\n            // Last edited\n            val c8 = JsoupUtils.getElementByClass(element, \"c8\")\n            if (c8 != null) {\n                val e = c8.children().first()\n                if (e != null) {\n                    comment.lastEdited = WEB_COMMENT_DATE_FORMAT.parse(e.text()).toEpochMillis()\n                }\n            }\n            comment\n        } catch (e: Throwable) {\n            ExceptionUtils.throwIfFatal(e)\n            e.printStackTrace()\n            null\n        }\n    }\n\n    /**\n     * Parse comments with html parser\n     */\n    fun parseComments(document: Document): GalleryCommentList = try {\n        val cdiv = document.getElementById(\"cdiv\")!!\n        val c1s = cdiv.getElementsByClass(\"c1\")\n        val list = c1s.mapNotNull { parseComment(it) }\n        val chd = cdiv.getElementById(\"chd\")\n        var hasMore = false\n        NodeTraversor.traverse(\n            object : NodeVisitor {\n                override fun head(node: Node, depth: Int) {\n                    if (node is Element && node.text() == \"click to show all\") {\n                        hasMore = true\n                    }\n                }\n\n                override fun tail(node: Node, depth: Int) {}\n            },\n            chd!!,\n        )\n        GalleryCommentList(list.toTypedArray(), hasMore)\n    } catch (e: Throwable) {\n        ExceptionUtils.throwIfFatal(e)\n        e.printStackTrace()\n        EMPTY_GALLERY_COMMENT_ARRAY\n    }\n\n    /**\n     * Parse preview pages with html parser\n     */\n    @Throws(ParseException::class)\n    fun parsePreviewPages(document: Document): Int = try {\n        val ptt = document.getElementsByClass(\"ptt\").first()!!\n        val elements = ptt.child(0).child(0).children()\n        elements[elements.size - 2].text().toInt()\n    } catch (e: Throwable) {\n        ExceptionUtils.throwIfFatal(e)\n        e.printStackTrace()\n        throw ParseException(\"Can't parse preview pages\")\n    }\n\n    /**\n     * Parse preview pages with regular expressions\n     */\n    @Throws(ParseException::class)\n    fun parsePreviewPages(body: String): Int = PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()\n        ?: throw ParseException(\"Parse preview page count error\")\n\n    /**\n     * Parse pages with regular expressions\n     */\n    @Throws(ParseException::class)\n    fun parsePages(body: String): Int = PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()\n        ?: throw ParseException(\"Parse pages error\")\n\n    /**\n     * Parse previews with regular expressions\n     */\n    @Throws(ParseException::class)\n    fun parsePreviewSet(body: String): PreviewSet {\n        val largePreviewSet = LargePreviewSet()\n        val normalPreviewSet = NormalPreviewSet()\n        PATTERN_PREVIEW.findAll(body).forEach {\n            val pageUrl = it.groupValues[1]\n            val position = it.groupValues[2].toInt() - 1\n            val url = it.groupValues[5]\n            val offset = it.groupValues[6]\n            val sha1 = it.groupValues[7]\n            if (offset.isEmpty()) {\n                largePreviewSet.addItem(position, url, pageUrl, sha1)\n            } else {\n                val width = it.groupValues[3].toInt()\n                val height = it.groupValues[4].toInt()\n                normalPreviewSet.addItem(position, url, offset.toInt(), 0, width, height, pageUrl, sha1)\n            }\n        }\n        if (largePreviewSet.size() > 0) return largePreviewSet\n        if (normalPreviewSet.size() > 0) return normalPreviewSet\n        throw ParseException(\"Can't parse preview\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryDetailUrlParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.yorozuya.NumberUtils\nimport java.util.regex.Pattern\n\n/**\n * Like http://exhentai.org/g/1234567/a1b2c3d4e5<br></br>\n */\nobject GalleryDetailUrlParser {\n    private val URL_STRICT_PATTERN = Pattern.compile(\n        \"https?://(?:\" + EhUrl.DOMAIN_EX + \"|\" + EhUrl.DOMAIN_E + \"|\" + EhUrl.DOMAIN_LOFI + \")/(?:g|mpv)/(\\\\d+)/([0-9a-f]{10})\",\n    )\n    private val URL_PATTERN = Pattern.compile(\"(\\\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)\")\n\n    fun parse(url: String?, strict: Boolean = true): Result? {\n        url ?: return null\n        val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN\n        val m = pattern.matcher(url)\n        return if (m.find()) {\n            val gid = NumberUtils.parseLongSafely(m.group(1), 0).takeIf { it > 0 }\n            gid?.let { Result(it, m.group(2)!!) }\n        } else {\n            null\n        }\n    }\n\n    class Result(val gid: Long, val token: String)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryListParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport android.util.Log\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.BaseGalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.client.exception.ParseException\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.JsoupUtils\nimport com.hippo.yorozuya.NumberUtils\nimport java.util.regex.Pattern\nimport org.jsoup.Jsoup\nimport org.jsoup.nodes.Document\nimport org.jsoup.nodes.Element\n\nobject GalleryListParser {\n    private val TAG = GalleryListParser::class.java.simpleName\n    private const val NO_UNFILTERED_TEXT =\n        \"No unfiltered results in this page range. You either requested an invalid page or used too aggressive filters.\"\n    private val PATTERN_RATING = Pattern.compile(\"\\\\d+px\")\n    private val PATTERN_THUMB_SIZE = Pattern.compile(\"height:(\\\\d+)px;width:(\\\\d+)px\")\n    private val PATTERN_FAVORITE_SLOT =\n        Pattern.compile(\"background-color:rgba\\\\((\\\\d+),(\\\\d+),(\\\\d+),\")\n    private val PATTERN_PAGES = Pattern.compile(\"(\\\\d+) page\")\n    private val PATTERN_NEXT_PAGE = Pattern.compile(\"p=(\\\\d+)\")\n    private val PATTERN_PREV = Pattern.compile(\"prev=(\\\\d+(-\\\\d+)?)\")\n    private val PATTERN_NEXT = Pattern.compile(\"next=(\\\\d+(-\\\\d+)?)\")\n    private val FAVORITE_SLOT_RGB = arrayOf(\n        Triple(\"0\", \"0\", \"0\"),\n        Triple(\"240\", \"0\", \"0\"),\n        Triple(\"240\", \"160\", \"0\"),\n        Triple(\"208\", \"208\", \"0\"),\n        Triple(\"0\", \"128\", \"0\"),\n        Triple(\"144\", \"240\", \"64\"),\n        Triple(\"64\", \"176\", \"240\"),\n        Triple(\"0\", \"0\", \"240\"),\n        Triple(\"80\", \"0\", \"128\"),\n        Triple(\"224\", \"128\", \"224\"),\n    )\n\n    private fun parseRating(ratingStyle: String): String? {\n        val m = PATTERN_RATING.matcher(ratingStyle)\n        var num1 = Int.MIN_VALUE\n        var num2 = Int.MIN_VALUE\n        var rate = 5\n        if (m.find()) {\n            num1 = ParserUtils.parseInt(m.group().replace(\"px\", \"\"), Int.MIN_VALUE)\n        }\n        if (m.find()) {\n            num2 = ParserUtils.parseInt(m.group().replace(\"px\", \"\"), Int.MIN_VALUE)\n        }\n        if (num1 == Int.MIN_VALUE || num2 == Int.MIN_VALUE) {\n            return null\n        }\n        rate -= num1 / 16\n        return if (num2 == 21) {\n            \"${rate - 1}.5\"\n        } else {\n            rate.toString()\n        }\n    }\n\n    private fun parseFavoriteSlot(style: String): Int {\n        val m = PATTERN_FAVORITE_SLOT.matcher(style)\n        if (m.find()) {\n            val r = m.group(1)!!\n            val g = m.group(2)!!\n            val b = m.group(3)!!\n            return FAVORITE_SLOT_RGB.indexOf(Triple(r, g, b))\n        }\n        return -2\n    }\n\n    private fun parseGalleryInfo(e: Element): GalleryInfo? {\n        val gi: GalleryInfo = BaseGalleryInfo()\n\n        // Title, gid, token (required)\n        val glname = JsoupUtils.getElementByClass(e, \"glname\")\n        if (glname != null) {\n            var a = JsoupUtils.getElementByTag(glname, \"a\")\n            if (a == null) {\n                val parent = glname.parent()\n                if (parent != null && \"a\" == parent.tagName()) {\n                    a = parent\n                }\n            }\n            if (a != null) {\n                val result = GalleryDetailUrlParser.parse(a.attr(\"href\"))\n                if (result != null) {\n                    gi.gid = result.gid\n                    gi.token = result.token\n                }\n            }\n            var child: Element = glname\n            var children = glname.children()\n            while (children.isNotEmpty()) {\n                child = children[0]\n                children = child.children()\n            }\n            gi.title = child.text().trim { it <= ' ' }\n        }\n        if (gi.title == null) {\n            return null\n        }\n\n        // Tags\n        val gts = e.select(\".gt, .gtl\")\n        if (gts.isNotEmpty()) {\n            val tags = ArrayList<String>()\n            for (gt in gts) {\n                tags.add(gt.attr(\"title\"))\n            }\n            gi.simpleTags = tags.toTypedArray()\n        }\n\n        // Category\n        gi.category = EhUtils.UNKNOWN\n        var ce = JsoupUtils.getElementByClass(e, \"cn\")\n        if (ce == null) {\n            ce = JsoupUtils.getElementByClass(e, \"cs\")\n        }\n        if (ce != null) {\n            gi.category = EhUtils.getCategory(ce.text())\n        }\n\n        // Thumb and pages\n        val glthumb = JsoupUtils.getElementByClass(e, \"glthumb\")\n        if (glthumb != null) {\n            val img = glthumb.select(\"div:nth-child(1)>img\").first()\n            if (img != null) {\n                // Thumb size\n                val m = PATTERN_THUMB_SIZE.matcher(img.attr(\"style\"))\n                if (m.find()) {\n                    gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0)\n                    gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0)\n                } else {\n                    Log.w(TAG, \"Can't parse gallery info thumb size\")\n                    gi.thumbWidth = 0\n                    gi.thumbHeight = 0\n                }\n                // Thumb url\n                var url = img.attr(\"data-src\")\n                if (url.isEmpty()) {\n                    url = img.attr(\"src\")\n                }\n                if (url.isNotEmpty()) {\n                    gi.thumb = url\n                }\n            }\n            // Pages\n            val div = glthumb.select(\"div:nth-child(2)>div:nth-child(2)>div:nth-child(2)\").first()\n            if (div != null) {\n                val matcher = PATTERN_PAGES.matcher(div.text())\n                if (matcher.find()) {\n                    gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0)\n                }\n            }\n        }\n\n        // Try extended and thumbnail version\n        if (gi.thumb == null) {\n            var gl = JsoupUtils.getElementByClass(e, \"gl1e\")\n            if (gl == null) {\n                gl = JsoupUtils.getElementByClass(e, \"gl3t\")\n            }\n            if (gl != null) {\n                val img = JsoupUtils.getElementByTag(gl, \"img\")\n                if (img != null) {\n                    // Thumb size\n                    val m = PATTERN_THUMB_SIZE.matcher(img.attr(\"style\"))\n                    if (m.find()) {\n                        gi.thumbWidth = NumberUtils.parseIntSafely(m.group(2), 0)\n                        gi.thumbHeight = NumberUtils.parseIntSafely(m.group(1), 0)\n                    } else {\n                        Log.w(TAG, \"Can't parse gallery info thumb size\")\n                        gi.thumbWidth = 0\n                        gi.thumbHeight = 0\n                    }\n                    gi.thumb = img.attr(\"src\")\n                }\n            }\n        }\n\n        // Posted\n        val posted = e.getElementById(\"posted_\" + gi.gid)\n        if (posted != null) {\n            gi.posted = posted.text().trim { it <= ' ' }\n            gi.favoriteSlot = parseFavoriteSlot(posted.attr(\"style\"))\n        }\n        if (gi.favoriteSlot < 0) {\n            gi.favoriteSlot = if (EhDB.containLocalFavorites(gi.gid)) -1 else -2\n        }\n\n        // Rating\n        val ir = JsoupUtils.getElementByClass(e, \"ir\")\n        if (ir != null) {\n            gi.rating = NumberUtils.parseFloatSafely(parseRating(ir.attr(\"style\")), -1.0f)\n            // TODO The gallery may be rated even if it doesn't has one of these classes\n            gi.rated = ir.hasClass(\"irr\") || ir.hasClass(\"irg\") || ir.hasClass(\"irb\")\n        }\n\n        // Uploader and pages\n        var gl = JsoupUtils.getElementByClass(e, \"glhide\")\n        var uploaderIndex = 0\n        var pagesIndex = 1\n        if (gl == null) {\n            // For extended\n            gl = JsoupUtils.getElementByClass(e, \"gl3e\")\n            uploaderIndex = 3\n            pagesIndex = 4\n        }\n        if (gl != null) {\n            val children = gl.children()\n            if (children.size > uploaderIndex) {\n                val div = children[uploaderIndex]\n                gi.disowned = div.attr(\"style\").contains(\"opacity:0.5\")\n                val a = div.children().first()\n                gi.uploader = a?.text()?.trim { it <= ' ' } ?: div.text().trim { it <= ' ' }\n            }\n            if (children.size > pagesIndex) {\n                val matcher = PATTERN_PAGES.matcher(children[pagesIndex].text())\n                if (matcher.find()) {\n                    gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0)\n                }\n            }\n        }\n        // For thumbnail\n        val gl5t = JsoupUtils.getElementByClass(e, \"gl5t\")\n        if (gl5t != null) {\n            val div = gl5t.select(\"div:nth-child(2)>div:nth-child(2)\").first()\n            if (div != null) {\n                val matcher = PATTERN_PAGES.matcher(div.text())\n                if (matcher.find()) {\n                    gi.pages = NumberUtils.parseIntSafely(matcher.group(1), 0)\n                }\n            }\n        }\n\n        // Favorite note\n        val glfnote = JsoupUtils.getElementByClass(e, \"glfnote\")\n        if (glfnote != null) {\n            val favoriteNote = glfnote.text().trim { it <= ' ' }\n            if (favoriteNote.isNotEmpty()) {\n                gi.favoriteNote = favoriteNote\n            }\n        }\n        gi.generateSLang()\n        return gi\n    }\n\n    fun parse(body: String): Result {\n        val d = Jsoup.parse(body)\n        return parse(d, body)\n    }\n\n    fun parse(d: Document, body: String): Result {\n        val result = Result()\n        try {\n            val prev = d.getElementById(\"uprev\")\n            val next = d.getElementById(\"unext\")\n            assert(prev != null)\n            assert(next != null)\n            val matcherPrev = PATTERN_PREV.matcher(prev!!.attr(\"href\"))\n            val matcherNext = PATTERN_NEXT.matcher(next!!.attr(\"href\"))\n            if (matcherPrev.find()) result.prev = matcherPrev.group(1)\n            if (matcherNext.find()) result.next = matcherNext.group(1)\n        } catch (e: Throwable) {\n            ExceptionUtils.throwIfFatal(e)\n            result.noWatchedTags = body.contains(\"<p>You do not have any watched tags\")\n            if (body.contains(\"No hits found</p>\")) {\n                val warn = d.getElementsByClass(\"searchwarn\").text()\n                if (warn.isEmpty()) {\n                    return result\n                } else {\n                    throw EhException(warn)\n                }\n            }\n        }\n        try { // For toplists\n            val ptt = d.getElementsByClass(\"ptt\").first()\n            if (ptt != null) {\n                val es = ptt.child(0).child(0).children()\n                result.pages = es[es.size - 2].text().trim { it <= ' ' }.toInt()\n                var e = es[es.size - 1]\n                e = e.children().first() as Element\n                val href = e.attr(\"href\")\n                val matcher = PATTERN_NEXT_PAGE.matcher(href)\n                if (matcher.find()) {\n                    result.nextPage = NumberUtils.parseIntSafely(matcher.group(1), 0)\n                }\n            }\n        } catch (e: Throwable) {\n            e.printStackTrace()\n        }\n        try {\n            val itg = d.getElementsByClass(\"itg\").first()\n            val es = if (\"table\".equals(itg!!.tagName(), ignoreCase = true)) {\n                itg.child(0).children()\n            } else {\n                itg.children()\n            }\n            val list = result.galleryInfoList\n            // First one is table header, skip it\n            for (i in es.indices) {\n                val gi = parseGalleryInfo(es[i])\n                if (null != gi) {\n                    list.add(gi)\n                }\n            }\n            if (list.isEmpty()) {\n                if (es.size < 2 || NO_UNFILTERED_TEXT != es[1].text()) {\n                    Log.d(TAG, \"No gallery found\")\n                }\n            }\n        } catch (e: Throwable) {\n            ExceptionUtils.throwIfFatal(e)\n            e.printStackTrace()\n            throw ParseException(\"Can't parse gallery list\")\n        }\n        return result\n    }\n\n    class Result {\n        var pages = 0\n        var nextPage = 0\n        var prev: String? = null\n        var next: String? = null\n        var noWatchedTags = false\n        val galleryInfoList = mutableListOf<GalleryInfo>()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryListUrlParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport android.text.TextUtils\nimport androidx.core.net.toUri\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.data.ListUrlBuilder\nimport com.hippo.util.isAtLeastT\nimport com.hippo.yorozuya.Utilities\nimport java.io.UnsupportedEncodingException\nimport java.net.MalformedURLException\nimport java.net.URL\nimport java.net.URLDecoder\nimport java.nio.charset.StandardCharsets\n\nobject GalleryListUrlParser {\n    private val VALID_HOSTS = arrayOf(EhUrl.DOMAIN_EX, EhUrl.DOMAIN_E, EhUrl.DOMAIN_LOFI)\n    private const val PATH_NORMAL = \"/\"\n    private const val PATH_UPLOADER = \"/uploader/\"\n    private const val PATH_TAG = \"/tag/\"\n    private const val PATH_TOPLIST = \"/toplist.php\"\n    fun parse(urlStr: String): ListUrlBuilder? {\n        val url = try {\n            URL(urlStr)\n        } catch (_: MalformedURLException) {\n            return null\n        }\n        if (!Utilities.contain(VALID_HOSTS, url.host)) {\n            return null\n        }\n        val path = url.path ?: return null\n        return if (PATH_NORMAL == path || path.isEmpty()) {\n            val builder = ListUrlBuilder()\n            builder.setQuery(url.query)\n            builder\n        } else if (path.startsWith(PATH_UPLOADER)) {\n            parseUploader(path)\n        } else if (path.startsWith(PATH_TAG)) {\n            parseTag(path)\n        } else if (path.startsWith(PATH_TOPLIST)) {\n            parseToplist(urlStr)\n        } else if (path.startsWith(\"/\")) {\n            val category = try {\n                path.substring(1).toInt()\n            } catch (_: NumberFormatException) {\n                return null\n            }\n            val builder = ListUrlBuilder()\n            builder.setQuery(url.query)\n            builder.category = category\n            builder\n        } else {\n            null\n        }\n    }\n\n    // TODO get page\n    private fun parseUploader(path: String): ListUrlBuilder? {\n        var uploader: String?\n        val prefixLength = PATH_UPLOADER.length\n        val index = path.indexOf('/', prefixLength)\n        uploader = if (index < 0) {\n            path.substring(prefixLength)\n        } else {\n            path.substring(prefixLength, index)\n        }\n        uploader = if (isAtLeastT) {\n            URLDecoder.decode(uploader, StandardCharsets.UTF_8)\n        } else {\n            try {\n                URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName())\n            } catch (e: UnsupportedEncodingException) {\n                e.printStackTrace()\n                return null\n            }\n        }\n        if (TextUtils.isEmpty(uploader)) {\n            return null\n        }\n        val builder = ListUrlBuilder()\n        builder.mode = ListUrlBuilder.MODE_UPLOADER\n        builder.keyword = uploader\n        return builder\n    }\n\n    // TODO get page\n    private fun parseTag(path: String): ListUrlBuilder? {\n        var tag: String?\n        val prefixLength = PATH_TAG.length\n        val index = path.indexOf('/', prefixLength)\n        tag = if (index < 0) {\n            path.substring(prefixLength)\n        } else {\n            path.substring(prefixLength, index)\n        }\n        tag = if (isAtLeastT) {\n            URLDecoder.decode(tag, StandardCharsets.UTF_8)\n        } else {\n            try {\n                URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName())\n            } catch (e: UnsupportedEncodingException) {\n                e.printStackTrace()\n                return null\n            }\n        }\n        if (TextUtils.isEmpty(tag)) {\n            return null\n        }\n        val builder = ListUrlBuilder()\n        builder.mode = ListUrlBuilder.MODE_TAG\n        builder.keyword = tag\n        return builder\n    }\n\n    // TODO get page\n    private fun parseToplist(path: String): ListUrlBuilder? {\n        val uri = path.toUri()\n        if (TextUtils.isEmpty(uri.getQueryParameter(\"tl\"))) {\n            return null\n        }\n        val tl = uri.getQueryParameter(\"tl\")!!.toInt()\n        if (tl > 15 || tl < 11) {\n            return null\n        }\n        val builder = ListUrlBuilder()\n        builder.mode = ListUrlBuilder.MODE_TOPLIST\n        builder.keyword = tl.toString()\n        return builder\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryMultiPageViewerParser.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.ParseException\nimport org.json.JSONArray\n\nobject GalleryMultiPageViewerParser {\n    private const val IMAGE_LIST_STRING = \"var imagelist = \"\n    private val PATTERN_SHA1 = Regex(\"data-orghash=\\\"([^\\\"]+)\\\"\")\n\n    fun parsePToken(body: String): List<String> = runCatching {\n        val index = body.indexOf(IMAGE_LIST_STRING)\n        val imageList = body.substring(index + IMAGE_LIST_STRING.length, body.indexOf(\";\", index))\n        val ja = JSONArray(imageList)\n        (0 until ja.length()).map { ja.getJSONObject(it).getString(\"k\") }\n    }.getOrElse {\n        throw ParseException(\"Parse pToken from MPV error\", it)\n    }\n\n    fun parseSha1(body: String): List<String> = runCatching {\n        PATTERN_SHA1.findAll(body).map { it.groupValues[1] }.toList()\n    }.getOrElse {\n        throw ParseException(\"Parse sha1 from MPV error\", it)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryNotAvailableParser.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.client.parser\n\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.JsoupUtils\nimport org.jsoup.Jsoup\n\nobject GalleryNotAvailableParser {\n    fun parse(body: String): String? = runCatching {\n        val document = Jsoup.parse(body)\n        val d = JsoupUtils.getElementByClass(document, \"d\")\n        d!!.child(0).html().replace(\"<br>\", \"\\n\")\n    }.getOrElse {\n        ExceptionUtils.throwIfFatal(it)\n        it.printStackTrace()\n        null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageApiParser.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.ParseException\nimport com.hippo.yorozuya.StringUtils\nimport java.util.regex.Matcher\nimport java.util.regex.Pattern\nimport org.json.JSONException\nimport org.json.JSONObject\n\nobject GalleryPageApiParser {\n    private val PATTERN_IMAGE_URL = Pattern.compile(\"<img[^>]*src=\\\"([^\\\"]+)\\\" style\")\n    private val PATTERN_SKIP_HATH_KEY = Pattern.compile(\"onclick=\\\"return nl\\\\('([^)]+)'\\\\)\")\n    private val PATTERN_ORIGIN_IMAGE_URL =\n        Pattern.compile(\"<a href=\\\"([^\\\"]+)fullimg([^\\\"]+)\\\">\")\n\n    fun parse(body: String): Result = try {\n        var m: Matcher\n        val jo = JSONObject(body)\n        if (jo.has(\"error\")) {\n            throw ParseException(jo.getString(\"error\"))\n        }\n        val i3 = jo.getString(\"i3\")\n        m = PATTERN_IMAGE_URL.matcher(i3)\n        val imageUrl = if (m.find()) {\n            StringUtils.unescapeXml(StringUtils.trim(m.group(1)))\n        } else {\n            null\n        }\n        val i6 = jo.getString(\"i6\")\n        m = PATTERN_SKIP_HATH_KEY.matcher(i6)\n        val skipHathKey = if (m.find()) {\n            StringUtils.unescapeXml(StringUtils.trim(m.group(1)))\n        } else {\n            null\n        }\n        m = PATTERN_ORIGIN_IMAGE_URL.matcher(i6)\n        val originImageUrl = if (m.find()) {\n            StringUtils.unescapeXml(m.group(1)) + \"fullimg\" +\n                StringUtils.unescapeXml(m.group(2))\n        } else {\n            null\n        }\n        if (!imageUrl.isNullOrEmpty()) {\n            Result(imageUrl, skipHathKey, originImageUrl)\n        } else {\n            throw ParseException(\"Parse image url and skip hath key error\")\n        }\n    } catch (e: JSONException) {\n        throw ParseException(\"Can't parse json\", e)\n    }\n\n    class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.ParseException\nimport com.hippo.yorozuya.StringUtils\nimport java.util.regex.Pattern\n\nobject GalleryPageParser {\n    private val PATTERN_IMAGE_URL = Pattern.compile(\"<img[^>]*src=\\\"([^\\\"]+)\\\" style\")\n    private val PATTERN_SKIP_HATH_KEY = Pattern.compile(\"onclick=\\\"return nl\\\\('([^)]+)'\\\\)\")\n    private val PATTERN_ORIGIN_IMAGE_URL =\n        Pattern.compile(\"<a href=\\\"([^\\\"]+)fullimg([^\\\"]+)\\\">\")\n\n    // TODO Not sure about the size of show keys\n    private val PATTERN_SHOW_KEY = Pattern.compile(\"var showkey=\\\"([0-9a-z]+)\\\";\")\n\n    fun parse(body: String): Result {\n        var m = PATTERN_IMAGE_URL.matcher(body)\n        val imageUrl = if (m.find()) {\n            StringUtils.unescapeXml(StringUtils.trim(m.group(1)))\n        } else {\n            null\n        }\n        m = PATTERN_SKIP_HATH_KEY.matcher(body)\n        val skipHathKey = if (m.find()) {\n            StringUtils.unescapeXml(StringUtils.trim(m.group(1)))\n        } else {\n            null\n        }\n        m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)\n        val originImageUrl = if (m.find()) {\n            StringUtils.unescapeXml(m.group(1)) + \"fullimg\" +\n                StringUtils.unescapeXml(m.group(2))\n        } else {\n            null\n        }\n        m = PATTERN_SHOW_KEY.matcher(body)\n        val showKey = if (m.find()) {\n            m.group(1)\n        } else {\n            null\n        }\n        return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {\n            Result(imageUrl, skipHathKey, originImageUrl, showKey)\n        } else {\n            throw ParseException(\"Parse image url and show error\")\n        }\n    }\n\n    class Result(\n        val imageUrl: String,\n        val skipHathKey: String?,\n        val originImageUrl: String?,\n        val showKey: String,\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryPageUrlParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.yorozuya.NumberUtils\nimport java.util.regex.Pattern\n\n/**\n * Like http://exhentai.org/s/91ea4b6d89/901103-12\n */\nobject GalleryPageUrlParser {\n    private val URL_STRICT_PATTERN = Pattern.compile(\n        \"https?://(?:\" + EhUrl.DOMAIN_EX + \"|\" + EhUrl.DOMAIN_E + \"|\" + EhUrl.DOMAIN_LOFI + \")/s/([0-9a-f]{10}|[0-9a-f]{40})/(\\\\d+)-(\\\\d+)\",\n    )\n    private val URL_PATTERN = Pattern.compile(\"([0-9a-f]{10})/(\\\\d+)-(\\\\d+)\")\n\n    fun parse(url: String?, strict: Boolean = true): Result? {\n        url ?: return null\n        val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN\n        val m = pattern.matcher(url)\n        return if (m.find()) {\n            val gid = NumberUtils.parseLongSafely(m.group(2), -1L)\n            val pToken = m.group(1)!!\n            val page = NumberUtils.parseIntSafely(m.group(3), 0) - 1\n            if (gid < 0 || page < 0) {\n                null\n            } else {\n                Result(gid, pToken, page)\n            }\n        } else {\n            null\n        }\n    }\n\n    class Result(val gid: Long, val pToken: String, val page: Int)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/GalleryTokenApiParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.util.ExceptionUtils\nimport org.json.JSONObject\n\nobject GalleryTokenApiParser {\n    /**\n     * {\n     * \"tokenlist\": [\n     * {\n     * \"gid\":618395,\n     * \"token\":\"0439fa3666\"\n     * }\n     * ]\n     * }\n     */\n    fun parse(body: String): String {\n        val jo = JSONObject(body).getJSONArray(\"tokenlist\").getJSONObject(0)\n        return runCatching {\n            jo.getString(\"token\")\n        }.getOrElse {\n            ExceptionUtils.throwIfFatal(it)\n            throw EhException(jo.getString(\"error\"))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/HomeParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.InsufficientFundsException\nimport com.hippo.ehviewer.client.exception.ParseException\nimport org.jsoup.Jsoup\n\nobject HomeParser {\n    const val IP_NORMAL = -1\n    const val IP_RESTRICTED = -2\n    private val PATTERN_FUNDS =\n        Regex(\"Available: ([\\\\d,]+) Credits.*Available: ([\\\\d,]+) kGP\", RegexOption.DOT_MATCHES_ALL)\n    private const val INSUFFICIENT_FUNDS = \"Insufficient funds.\"\n    private const val IP_RESTRICTED_STR = \"Due to a high request rate, your IP is currently restricted to lower-resolution images.\"\n\n    fun parse(body: String): Limits {\n        Jsoup.parse(body).selectFirst(\"div.homebox\")?.let {\n            val es = it.select(\"p > strong\")\n            if (es.size == 3) {\n                val current = ParserUtils.parseInt(es[0].text(), 0)\n                val maximum = ParserUtils.parseInt(es[1].text(), 0)\n                val resetCost = ParserUtils.parseInt(es[2].text(), 0)\n                return Limits(current, maximum, resetCost)\n            } else if (es.size == 1) {\n                val maximum = if (body.contains(IP_RESTRICTED_STR)) IP_RESTRICTED else IP_NORMAL\n                val resetCost = ParserUtils.parseInt(es[0].text(), 0)\n                return Limits(0, maximum, resetCost)\n            }\n        }\n        throw ParseException(\"Parse image limits error\")\n    }\n\n    fun parseResetLimits(body: String): Limits {\n        if (body.contains(INSUFFICIENT_FUNDS)) {\n            throw InsufficientFundsException()\n        }\n        return parse(body)\n    }\n\n    fun parseFunds(body: String): Funds {\n        PATTERN_FUNDS.find(body)?.groupValues?.run {\n            val fundsC = ParserUtils.parseInt(get(1), 0)\n            val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000\n            return Funds(fundsGP, fundsC)\n        }\n        throw ParseException(\"Parse funds error\")\n    }\n\n    data class Limits(val current: Int = 0, val maximum: Int, val resetCost: Int = 0)\n    data class Funds(val fundsGP: Int, val fundsC: Int)\n    class Result(val limits: Limits, val funds: Funds)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/ParserUtils.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.util.toLocalDateTime\nimport com.hippo.yorozuya.NumberUtils\nimport com.hippo.yorozuya.StringUtils\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.format.char\n\nobject ParserUtils {\n    // yyyy-MM-dd HH:mm\n    private val formatter = LocalDateTime.Format {\n        year()\n        char('-')\n        monthNumber()\n        char('-')\n        day()\n        char(' ')\n        hour()\n        char(':')\n        minute()\n    }\n\n    fun formatDate(time: Long): String = formatter.format(time.toLocalDateTime())\n\n    fun trim(str: String?): String = str?.let { StringUtils.unescapeXml(it).trim() } ?: \"\"\n\n    fun parseInt(str: String?, defValue: Int): Int = NumberUtils.parseIntSafely(trim(str).replace(\",\", \"\"), defValue)\n\n    fun parseLong(str: String?, defValue: Long): Long = NumberUtils.parseLongSafely(trim(str).replace(\",\", \"\"), defValue)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/ProfileParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport android.util.Log\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.client.exception.ParseException\nimport com.hippo.ehviewer.client.parser.SignInParser.ERROR_PATTERN\nimport com.hippo.util.ExceptionUtils\nimport org.jsoup.Jsoup\n\nobject ProfileParser {\n    private val TAG = ProfileParser::class.java.simpleName\n    fun parse(body: String): Result = runCatching {\n        val d = Jsoup.parse(body)\n        val profilename = d.getElementById(\"profilename\")\n        val displayName = profilename!!.child(0).text()\n        val avatar = runCatching {\n            val avatar =\n                profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr(\"src\")\n            if (avatar.isEmpty()) {\n                null\n            } else if (!avatar.startsWith(\"http\")) {\n                EhUrl.URL_FORUMS + avatar\n            } else {\n                avatar\n            }\n        }.getOrElse {\n            ExceptionUtils.throwIfFatal(it)\n            Log.i(TAG, \"No avatar\")\n            null\n        }\n        Result(displayName, avatar)\n    }.getOrElse {\n        val m = ERROR_PATTERN.matcher(body)\n        if (m.find()) {\n            throw EhException(m.group(1) ?: m.group(2))\n        } else {\n            ExceptionUtils.throwIfFatal(it)\n            throw ParseException(\"Parse forums error\")\n        }\n    }\n\n    class Result(val displayName: String?, val avatar: String?)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/RateGalleryParser.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.ParseException\nimport org.json.JSONException\nimport org.json.JSONObject\n\nobject RateGalleryParser {\n    fun parse(body: String): Result = try {\n        val jsonObject = JSONObject(body)\n        val rating = jsonObject.getDouble(\"rating_avg\").toFloat()\n        val ratingCount = jsonObject.getInt(\"rating_cnt\")\n        Result(rating, ratingCount)\n    } catch (e: JSONException) {\n        throw ParseException(\"Can't parse rate gallery\", e)\n    }\n\n    class Result(val rating: Float, val ratingCount: Int)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/SignInParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.client.exception.ParseException\nimport java.util.regex.Pattern\n\nobject SignInParser {\n    private val NAME_PATTERN = Pattern.compile(\"<p>You are now logged in as: (.+?)<\")\n    val ERROR_PATTERN: Pattern = Pattern.compile(\n        \"<h4>The error returned was:</h4>\\\\s*<p>(.+?)</p>\" +\n            \"|<span class=\\\"postcolor\\\">(.+?)</span>\",\n    )\n\n    fun parse(body: String): String {\n        var m = NAME_PATTERN.matcher(body)\n        return if (m.find()) {\n            m.group(1)!!\n        } else {\n            m = ERROR_PATTERN.matcher(body)\n            if (m.find()) {\n                throw EhException(m.group(1) ?: m.group(2))\n            } else {\n                throw ParseException(\"Can't parse sign in\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/TorrentParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.exception.ParseException\nimport java.util.regex.Pattern\nimport org.jsoup.Jsoup\n\nobject TorrentParser {\n    private val PATTERN_TORRENT =\n        Pattern.compile(\">\\\\s?([0-9-]+) [0-9:]+</[\\\\s\\\\S]+>\\\\s?([0-9.]+ [KMGT]iB)</[\\\\s\\\\S]+>\\\\s?([0-9]+)</[\\\\s\\\\S]+>\\\\s?([0-9]+)</[\\\\s\\\\S]+>\\\\s?([0-9]+)</[\\\\s\\\\S]+</[^>]+>\\\\s?([^<]+)</[\\\\s\\\\S]+onclick=\\\"document.location='([^\\\"]+)'[^<]+>([^<]+)</a>\")\n\n    fun parse(body: String): List<Result> {\n        val torrentList = ArrayList<Result>()\n        val d = Jsoup.parse(body)\n        val es = d.select(\"form>div>table\")\n        for (e in es) {\n            val html = e.html()\n            if (!html.contains(\"Expunged\")) {\n                val m = PATTERN_TORRENT.matcher(html)\n                if (m.find()) {\n                    val posted = m.group(1)!!\n                    val size = m.group(2)!!\n                    val seeds = m.group(3)!!.toInt()\n                    val peers = m.group(4)!!.toInt()\n                    val downloads = m.group(5)!!.toInt()\n                    val url = ParserUtils.trim(m.group(7))\n                    val name = ParserUtils.trim(m.group(8))\n                    torrentList.add(Result(posted, size, seeds, peers, downloads, url, name))\n                } else {\n                    throw ParseException(\"Can't parse torrent list\")\n                }\n            }\n        }\n        return torrentList\n    }\n\n    class Result(\n        private val posted: String,\n        private val size: String,\n        private val seeds: Int,\n        private val peers: Int,\n        private val downloads: Int,\n        val url: String,\n        val name: String,\n    ) {\n        fun format() = \"[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/UserConfigParser.kt",
    "content": "package com.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.Settings\nimport com.hippo.yorozuya.unescapeXml\n\nobject UserConfigParser {\n    private const val U_CONFIG_TEXT = \"Selected Profile\"\n    private val FAV_CAT_PATTERN = Regex(\"<input type=\\\"text\\\" name=\\\"favorite_\\\\d\\\" value=\\\"([^\\\"]+)\\\"\")\n\n    fun parse(body: String) {\n        check(U_CONFIG_TEXT in body) { \"Unable to load user config!\" }\n        val iterator = FAV_CAT_PATTERN.findAll(body).iterator()\n        val favCat = Array(10) { iterator.next().groupValues[1].unescapeXml() }\n        Settings.favCat = favCat\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/VoteCommentParser.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.client.parser\n\nimport org.json.JSONObject\n\nobject VoteCommentParser {\n    // {\"comment_id\":1253922,\"comment_score\":-19,\"comment_vote\":0}\n    fun parse(body: String, expectVote: Int): Result {\n        val jo = JSONObject(body)\n        val id = jo.getLong(\"comment_id\")\n        val score = jo.getInt(\"comment_score\")\n        val vote = jo.getInt(\"comment_vote\")\n        return Result(id, score, vote, expectVote)\n    }\n\n    class Result(val id: Long, val score: Int, val vote: Int, val expectVote: Int)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/client/parser/VoteTagParser.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.client.parser\n\nimport com.hippo.ehviewer.client.data.GalleryTagGroup\nimport com.hippo.ehviewer.client.parser.GalleryDetailParser.parseTagGroups\nimport org.json.JSONObject\nimport org.jsoup.Jsoup\n\nobject VoteTagParser {\n    // {\"error\":\"The tag \\\"neko\\\" is not allowed. Use character:neko or artist:neko\"}\n    fun parse(body: String): Pair<String, Array<GalleryTagGroup>?> {\n        val obj = JSONObject(body)\n        val tags = Jsoup.parse(\"<div id=\\\"taglist\\\">${obj.optString(\"tagpane\")}</div>\")\n        return obj.optString(\"error\") to parseTagGroups(tags)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/coil/DiskCache.kt",
    "content": "/*\n * Copyright 2023 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.coil\n\nimport coil3.disk.DiskCache\n\ninline fun DiskCache.edit(key: String, block: DiskCache.Editor.() -> Unit): Boolean {\n    val editor = openEditor(key) ?: return false\n    editor.runCatching {\n        block(this)\n    }.onFailure {\n        editor.abort()\n        throw it\n    }.onSuccess {\n        editor.commit()\n    }\n    return true\n}\n\ninline fun DiskCache.read(key: String, block: DiskCache.Snapshot.() -> Unit): Boolean {\n    (openSnapshot(key) ?: return false).use { block(it) }\n    return true\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/coil/DownloadThumbInterceptor.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.coil\n\nimport coil3.intercept.Interceptor\nimport coil3.request.ImageResult\nimport coil3.request.SuccessResult\nimport com.hippo.ehviewer.EhApplication.Companion.thumbCache\nimport com.hippo.ehviewer.Settings.downloadLocation\nimport com.hippo.ehviewer.spider.DownloadInfoMagics.decodeMagicRequestOrUrl\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.sendTo\nimport com.hippo.util.withIOContext\n\nobject DownloadThumbInterceptor : Interceptor {\n    const val THUMB_FILE = \".thumb\"\n    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {\n        val magicOrUrl = chain.request.data as? String\n        if (magicOrUrl != null) {\n            val (url, location) = decodeMagicRequestOrUrl(magicOrUrl)\n            if (location != null) {\n                return withIOContext {\n                    val thumb = downloadLocation?.subFile(location)?.subFile(THUMB_FILE)\n                    if (thumb?.isFile == true) {\n                        val new = chain.request.newBuilder().data(thumb.uri).build()\n                        val result = chain.withRequest(new).proceed()\n                        if (result is SuccessResult) return@withIOContext result\n                    }\n                    val new = chain.request.newBuilder().data(url).build()\n                    val result = chain.withRequest(new).proceed()\n                    if (result is SuccessResult && thumb?.parentFile?.isDirectory == true) {\n                        // Accessing the recreated file immediately after deleting it throws\n                        // FileNotFoundException, so we just overwrite the existing file.\n                        chain.request.memoryCacheKey?.let {\n                            if (!thumb.exists()) thumb.ensureFile()\n                            thumbCache.read(it) {\n                                UniFile.fromFile(data.toFile())!! sendTo thumb\n                            }\n                        }\n                    }\n                    result\n                }\n            }\n        }\n        return chain.proceed()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/coil/LockPool.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.coil\n\nimport kotlin.contracts.InvocationKind\nimport kotlin.contracts.contract\n\ninterface LockPool<Lock, K> {\n    fun acquire(key: K): Lock\n    fun release(key: K, lock: Lock)\n    suspend fun Lock.lock()\n    fun Lock.tryLock(): Boolean\n    fun Lock.unlock()\n}\n\n@Suppress(\"KotlinUnreachableCode\")\nsuspend inline fun <Lock, K, R> LockPool<Lock, K>.withLock(key: K, action: () -> R): R {\n    contract {\n        callsInPlace(action, InvocationKind.EXACTLY_ONCE)\n    }\n    val lock = acquire(key)\n    return try {\n        lock.lock()\n        return try {\n            action()\n        } finally {\n            lock.unlock()\n        }\n    } finally {\n        release(key, lock)\n    }\n}\n\n@Suppress(\"KotlinUnreachableCode\")\nsuspend inline fun <Lock, K, R> LockPool<Lock, K>.withLockNeedSuspend(key: K, action: () -> R): Pair<R, Boolean> {\n    contract {\n        callsInPlace(action, InvocationKind.EXACTLY_ONCE)\n    }\n    val lock = acquire(key)\n    return try {\n        val mustSuspend = !lock.tryLock()\n        if (mustSuspend) lock.lock()\n        return try {\n            action() to mustSuspend\n        } finally {\n            lock.unlock()\n        }\n    } finally {\n        release(key, lock)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/coil/MergeInterceptor.kt",
    "content": "/*\n * Copyright 2023 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.coil\n\nimport coil3.decode.DataSource\nimport coil3.intercept.Interceptor\nimport coil3.request.ImageResult\nimport coil3.request.SuccessResult\nimport com.hippo.ehviewer.client.isNormalPreviewKey\n\nobject MergeInterceptor : Interceptor {\n    private val mutex = NamedMutex<String>()\n\n    override suspend fun intercept(chain: Interceptor.Chain): ImageResult {\n        val req = chain.request\n        val key = req.memoryCacheKey?.takeIf { it.isNormalPreviewKey }\n        return if (key != null) {\n            val (result, suspended) = mutex.withLockNeedSuspend(key) { chain.proceed() }\n            when (result) {\n                is SuccessResult if (suspended) -> result.copy(dataSource = DataSource.MEMORY)\n                else -> result\n            }\n        } else {\n            chain.proceed()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/coil/NamedMutex.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.coil\n\nimport androidx.collection.MutableScatterMap\nimport androidx.collection.mutableScatterMapOf\nimport io.ktor.utils.io.pool.DefaultPool\nimport kotlinx.coroutines.sync.Mutex\n\nclass MutexTracker(mutex: Mutex = Mutex(), private var count: Int = 0) : Mutex by mutex {\n    operator fun inc() = apply { count++ }\n    operator fun dec() = apply { count-- }\n    val isFree\n        get() = count == 0\n}\n\nobject MutexPool : DefaultPool<MutexTracker>(capacity = 32) {\n    override fun produceInstance() = MutexTracker()\n    override fun validateInstance(instance: MutexTracker) {\n        check(!instance.isLocked)\n        check(instance.isFree)\n    }\n}\n\nclass NamedMutex<K>(val active: MutableScatterMap<K, MutexTracker> = mutableScatterMapOf()) : LockPool<MutexTracker, K> {\n    override fun acquire(key: K) = synchronized(active) { active.getOrPut(key) { MutexPool.borrow() }.inc() }\n    override fun release(key: K, lock: MutexTracker) = synchronized(active) {\n        lock.dec()\n        if (lock.isFree) {\n            active.remove(key)\n            MutexPool.recycle(lock)\n        }\n    }\n    override suspend fun MutexTracker.lock() = lock()\n    override fun MutexTracker.tryLock() = tryLock()\n    override fun MutexTracker.unlock() = unlock()\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/BasicDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\ninterface BasicDao<T> {\n    fun list(): List<T>\n    fun insert(t: T): Long\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/BookmarkInfo.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport android.annotation.SuppressLint\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport com.hippo.ehviewer.client.data.BaseGalleryInfo\n\n@SuppressLint(\"ParcelCreator\")\n@Entity(tableName = \"BOOKMARKS\")\nclass BookmarkInfo : BaseGalleryInfo() {\n    @ColumnInfo(name = \"PAGE\")\n    var page = 0\n\n    @ColumnInfo(name = \"TIME\")\n    var time: Long = 0\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/BookmarksDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\n\n@Dao\ninterface BookmarksDao : BasicDao<BookmarkInfo> {\n    @Insert\n    override fun insert(t: BookmarkInfo): Long\n\n    @Delete\n    fun delete(bookmark: BookmarkInfo)\n\n    @Query(\"SELECT * FROM BOOKMARKS ORDER BY TIME DESC\")\n    override fun list(): List<BookmarkInfo>\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/DownloadDirname.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"DOWNLOAD_DIRNAME\")\ndata class DownloadDirname(\n    @PrimaryKey\n    @ColumnInfo(name = \"GID\")\n    var gid: Long = 0,\n\n    @ColumnInfo(name = \"DIRNAME\")\n    var dirname: String? = null,\n)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/DownloadDirnameDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\n\n@Dao\ninterface DownloadDirnameDao : BasicDao<DownloadDirname> {\n    @Query(\"SELECT * FROM DOWNLOAD_DIRNAME WHERE GID = :gid\")\n    fun load(gid: Long): DownloadDirname?\n\n    @Update\n    fun update(downloadDirname: DownloadDirname)\n\n    @Insert\n    override fun insert(t: DownloadDirname): Long\n\n    @Query(\"DELETE FROM DOWNLOAD_DIRNAME WHERE GID = :gid\")\n    fun deleteByKey(gid: Long)\n\n    @Query(\"DELETE FROM DOWNLOAD_DIRNAME\")\n    fun deleteAll()\n\n    @Query(\"SELECT * FROM DOWNLOAD_DIRNAME\")\n    override fun list(): List<DownloadDirname>\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/DownloadInfo.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport android.annotation.SuppressLint\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.Ignore\nimport com.hippo.ehviewer.client.data.BaseGalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryInfo\n\n@SuppressLint(\"ParcelCreator\")\n@Entity(tableName = \"DOWNLOADS\")\nclass DownloadInfo() : BaseGalleryInfo() {\n    @ColumnInfo(name = \"STATE\")\n    var state = 0\n\n    @ColumnInfo(name = \"LEGACY\")\n    var legacy = 0\n\n    @ColumnInfo(name = \"TIME\")\n    var time: Long = 0\n\n    @ColumnInfo(name = \"LABEL\")\n    var label: String? = null\n\n    @Ignore\n    var speed: Long = 0\n\n    @Ignore\n    var remaining: Long = 0\n\n    @Ignore\n    var finished = 0\n\n    @Ignore\n    var downloaded = 0\n\n    @Ignore\n    var total = 0\n\n    constructor(galleryInfo: GalleryInfo) : this() {\n        gid = galleryInfo.gid\n        token = galleryInfo.token\n        title = galleryInfo.title\n        titleJpn = galleryInfo.titleJpn\n        thumb = galleryInfo.thumb\n        this.category = galleryInfo.category\n        posted = galleryInfo.posted\n        uploader = galleryInfo.uploader\n        rating = galleryInfo.rating\n        simpleTags = galleryInfo.simpleTags\n        simpleLanguage = galleryInfo.simpleLanguage\n    }\n\n    companion object {\n        const val STATE_INVALID = -1\n        const val STATE_NONE = 0\n        const val STATE_WAIT = 1\n        const val STATE_DOWNLOAD = 2\n        const val STATE_FINISH = 3\n        const val STATE_FAILED = 4\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/DownloadLabel.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"DOWNLOAD_LABELS\")\ndata class DownloadLabel(\n    @PrimaryKey\n    @ColumnInfo(name = \"_id\")\n    var id: Long? = null,\n\n    @ColumnInfo(name = \"LABEL\")\n    var label: String? = null,\n\n    @ColumnInfo(name = \"TIME\")\n    var time: Long = 0,\n)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/DownloadLabelDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\n\n@Dao\ninterface DownloadLabelDao : BasicDao<DownloadLabel> {\n    @Query(\"SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC\")\n    override fun list(): List<DownloadLabel>\n\n    @Query(\"SELECT * FROM DOWNLOAD_LABELS ORDER BY TIME ASC LIMIT :limit OFFSET :offset\")\n    fun list(offset: Int, limit: Int): List<DownloadLabel>\n\n    @Update\n    fun update(downloadLabels: List<DownloadLabel>)\n\n    @Update\n    fun update(downloadLabel: DownloadLabel)\n\n    @Insert\n    override fun insert(t: DownloadLabel): Long\n\n    @Delete\n    fun delete(downloadLabel: DownloadLabel)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/DownloadsDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\n\n@Dao\ninterface DownloadsDao : BasicDao<DownloadInfo> {\n    @Query(\"SELECT * FROM DOWNLOADS ORDER BY TIME DESC\")\n    override fun list(): List<DownloadInfo>\n\n    @Query(\"SELECT * FROM DOWNLOADS WHERE GID = :gid\")\n    fun load(gid: Long): DownloadInfo?\n\n    @Update\n    fun update(downloadInfos: List<DownloadInfo>)\n\n    @Update\n    fun update(downloadInfo: DownloadInfo)\n\n    @Insert\n    override fun insert(t: DownloadInfo): Long\n\n    @Delete\n    fun delete(downloadInfo: DownloadInfo)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/EhDatabase.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport android.content.Context\nimport androidx.room.Database\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\n\n@Database(\n    entities = [BookmarkInfo::class, DownloadInfo::class, DownloadLabel::class, DownloadDirname::class, Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, QuickSearch::class],\n    version = 4,\n    exportSchema = false,\n)\nabstract class EhDatabase : RoomDatabase() {\n    abstract fun bookmarksBao(): BookmarksDao\n    abstract fun downloadDirnameDao(): DownloadDirnameDao\n    abstract fun downloadLabelDao(): DownloadLabelDao\n    abstract fun downloadsDao(): DownloadsDao\n    abstract fun filterDao(): FilterDao\n    abstract fun historyDao(): HistoryDao\n    abstract fun localFavoritesDao(): LocalFavoritesDao\n    abstract fun quickSearchDao(): QuickSearchDao\n}\n\nfun buildMainDB(context: Context): EhDatabase {\n    // TODO: Remove allowMainThreadQueries\n    return Room.databaseBuilder(context, EhDatabase::class.java, \"eh.db\").allowMainThreadQueries()\n        .build()\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/Filter.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"FILTER\")\ndata class Filter(\n    @ColumnInfo(name = \"MODE\")\n    var mode: Int = 0,\n\n    @ColumnInfo(name = \"TEXT\")\n    var text: String? = null,\n\n    @ColumnInfo(name = \"ENABLE\")\n    var enable: Boolean? = null,\n\n    @PrimaryKey\n    @ColumnInfo(name = \"_id\")\n    var id: Long? = null,\n)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/FilterDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\n\n@Dao\ninterface FilterDao : BasicDao<Filter> {\n    @Query(\"SELECT * FROM `FILTER`\")\n    override fun list(): List<Filter>\n\n    @Update\n    fun update(filter: Filter)\n\n    @Insert\n    override fun insert(t: Filter): Long\n\n    @Delete\n    fun delete(filter: Filter)\n\n    @Query(\"SELECT * FROM `FILTER` WHERE TEXT = :text AND MODE = :mode\")\n    fun load(text: String, mode: Int): Filter?\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/HistoryDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.paging.PagingSource\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\n\n@Dao\ninterface HistoryDao : BasicDao<HistoryInfo> {\n    @Query(\"SELECT * FROM HISTORY WHERE GID = :gid\")\n    fun load(gid: Long): HistoryInfo?\n\n    @Query(\"SELECT * FROM HISTORY ORDER BY TIME DESC\")\n    override fun list(): List<HistoryInfo>\n\n    @Query(\"SELECT * FROM HISTORY ORDER BY TIME DESC LIMIT :limit OFFSET :offset\")\n    fun list(offset: Int, limit: Int): List<HistoryInfo>\n\n    @Query(\"SELECT * FROM HISTORY ORDER BY TIME DESC\")\n    fun listLazy(): PagingSource<Int, HistoryInfo>\n\n    @Update\n    fun update(historyInfo: HistoryInfo)\n\n    @Insert\n    override fun insert(t: HistoryInfo): Long\n\n    @Delete\n    fun delete(historyInfo: HistoryInfo)\n\n    @Delete\n    fun delete(historyInfo: List<HistoryInfo>)\n\n    @Query(\"DELETE FROM HISTORY\")\n    fun deleteAll()\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/HistoryInfo.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport android.annotation.SuppressLint\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport com.hippo.ehviewer.client.data.BaseGalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryInfo\n\n@SuppressLint(\"ParcelCreator\")\n@Entity(tableName = \"HISTORY\")\nclass HistoryInfo() : BaseGalleryInfo() {\n    @ColumnInfo(name = \"TIME\")\n    var time: Long = 0\n\n    // Trick: Use MODE for favoriteSlot\n    @ColumnInfo(name = \"MODE\")\n    var favoriteSlotBackingField: Int = 0\n\n    override var favoriteSlot: Int\n        get() = favoriteSlotBackingField - 2\n        set(value) {\n            favoriteSlotBackingField = value + 2\n        }\n    // Trick end\n\n    constructor(galleryInfo: GalleryInfo) : this() {\n        gid = galleryInfo.gid\n        token = galleryInfo.token\n        title = galleryInfo.title\n        titleJpn = galleryInfo.titleJpn\n        thumb = galleryInfo.thumb\n        this.category = galleryInfo.category\n        posted = galleryInfo.posted\n        uploader = galleryInfo.uploader\n        rating = galleryInfo.rating\n        simpleTags = galleryInfo.simpleTags\n        simpleLanguage = galleryInfo.simpleLanguage\n        favoriteSlot = galleryInfo.favoriteSlot\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/LocalFavoriteInfo.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport android.annotation.SuppressLint\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport com.hippo.ehviewer.client.data.BaseGalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryInfo\n\n@SuppressLint(\"ParcelCreator\")\n@Entity(tableName = \"LOCAL_FAVORITES\")\nclass LocalFavoriteInfo() : BaseGalleryInfo() {\n    @ColumnInfo(name = \"TIME\")\n    var time: Long = 0\n\n    constructor(galleryInfo: GalleryInfo) : this() {\n        gid = galleryInfo.gid\n        token = galleryInfo.token\n        title = galleryInfo.title\n        titleJpn = galleryInfo.titleJpn\n        thumb = galleryInfo.thumb\n        this.category = galleryInfo.category\n        posted = galleryInfo.posted\n        uploader = galleryInfo.uploader\n        rating = galleryInfo.rating\n        simpleTags = galleryInfo.simpleTags\n        simpleLanguage = galleryInfo.simpleLanguage\n    }\n\n    init {\n        favoriteSlot = -1\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/LocalFavoritesDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\n\n@Dao\ninterface LocalFavoritesDao : BasicDao<LocalFavoriteInfo> {\n    @Query(\"SELECT * FROM LOCAL_FAVORITES ORDER BY TIME DESC\")\n    override fun list(): List<LocalFavoriteInfo>\n\n    @Query(\"SELECT * FROM LOCAL_FAVORITES WHERE TITLE LIKE :title ORDER BY TIME DESC\")\n    fun list(title: String): List<LocalFavoriteInfo>\n\n    @Query(\"SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid\")\n    fun load(gid: Long): LocalFavoriteInfo?\n\n    @Query(\"SELECT EXISTS(SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid)\")\n    fun contains(gid: Long): Boolean\n\n    @Insert\n    override fun insert(t: LocalFavoriteInfo): Long\n\n    @Delete\n    fun delete(localFavoriteInfo: LocalFavoriteInfo)\n\n    @Query(\"DELETE FROM LOCAL_FAVORITES WHERE GID = :gid\")\n    fun deleteByKey(gid: Long)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/QuickSearch.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"QUICK_SEARCH\")\ndata class QuickSearch(\n    @PrimaryKey\n    @ColumnInfo(name = \"_id\")\n    var id: Long? = null,\n\n    @ColumnInfo(name = \"NAME\")\n    var name: String? = null,\n\n    @ColumnInfo(name = \"MODE\")\n    var mode: Int = 0,\n\n    @ColumnInfo(name = \"CATEGORY\")\n    var category: Int = 0,\n\n    @ColumnInfo(name = \"KEYWORD\")\n    var keyword: String? = null,\n\n    @ColumnInfo(name = \"ADVANCE_SEARCH\")\n    var advanceSearch: Int = 0,\n\n    @ColumnInfo(name = \"MIN_RATING\")\n    var minRating: Int = 0,\n\n    @ColumnInfo(name = \"PAGE_FROM\")\n    var pageFrom: Int = 0,\n\n    @ColumnInfo(name = \"PAGE_TO\")\n    var pageTo: Int = 0,\n\n    @ColumnInfo(name = \"TIME\")\n    var time: Long = 0,\n)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/dao/QuickSearchDao.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.dao\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.Query\nimport androidx.room.Update\n\n@Dao\ninterface QuickSearchDao : BasicDao<QuickSearch> {\n    @Query(\"SELECT * FROM QUICK_SEARCH ORDER BY TIME ASC\")\n    override fun list(): List<QuickSearch>\n\n    @Query(\"SELECT * FROM QUICK_SEARCH ORDER BY TIME ASC LIMIT :limit OFFSET :offset\")\n    fun list(offset: Int, limit: Int): List<QuickSearch>\n\n    @Update\n    fun update(downloadLabels: List<QuickSearch>)\n\n    @Update\n    fun update(quickSearch: QuickSearch)\n\n    @Insert\n    override fun insert(t: QuickSearch): Long\n\n    @Delete\n    fun delete(quickSearch: QuickSearch)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/download/DownloadManager.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.download\n\nimport android.util.Log\nimport android.util.SparseLongArray\nimport androidx.collection.LongSparseArray\nimport androidx.collection.keyIterator\nimport androidx.core.util.size\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.dao.DownloadLabel\nimport com.hippo.ehviewer.spider.SpiderDen\nimport com.hippo.ehviewer.spider.SpiderQueen\nimport com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener\nimport com.hippo.ehviewer.spider.readFromUniFile\nimport com.hippo.ehviewer.spider.saveToUniFile\nimport com.hippo.image.Image\nimport com.hippo.yorozuya.ConcurrentPool\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.ObjectUtils\nimport com.hippo.yorozuya.SimpleHandler\nimport com.hippo.yorozuya.collect.LongList\nimport java.util.LinkedList\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\n\nobject DownloadManager : OnSpiderListener {\n    // All download info list\n    private val mAllInfoList: LinkedList<DownloadInfo>\n\n    // All download info map\n    private val mAllInfoMap: LongSparseArray<DownloadInfo>\n\n    // label and info list map, without default label info list\n    private val mMap: MutableMap<String?, LinkedList<DownloadInfo>>\n\n    // All download dirname\n    private val mAllDownloadDirname = mutableMapOf<Long, String>()\n\n    // All labels without default label\n    private val mLabelList: MutableList<DownloadLabel>\n\n    // Store download info with default label\n    private val mDefaultInfoList: LinkedList<DownloadInfo>\n\n    // Store download info wait to start\n    private val mWaitList: LinkedList<DownloadInfo>\n    private val mSpeedReminder: SpeedReminder\n    private val mDownloadInfoListeners: MutableList<DownloadInfoListener?>\n    private val mNotifyTaskPool = ConcurrentPool<NotifyTask?>(5)\n    private var mDownloadListener: DownloadListener? = null\n    private var mCurrentTask: DownloadInfo? = null\n    private var mCurrentSpider: SpiderQueen? = null\n\n    init {\n        // Get all download dirname\n        val allDownloadDirname = EhDB.allDownloadDirname\n        for ((gid, dirname) in allDownloadDirname) {\n            if (dirname != null) {\n                mAllDownloadDirname.put(gid, dirname)\n            }\n        }\n\n        // Get all labels\n        val labels = EhDB.allDownloadLabelList.toMutableList()\n        mLabelList = labels\n\n        // Create list for each label\n        val map = HashMap<String?, LinkedList<DownloadInfo>>()\n        mMap = map\n        for ((_, label1) in labels) {\n            map[label1] = LinkedList()\n        }\n\n        // Create default for non tag\n        mDefaultInfoList = LinkedList()\n\n        // Get all info\n        val allInfoList = EhDB.allDownloadInfo\n        mAllInfoList = LinkedList(allInfoList)\n\n        // Create all info map\n        val allInfoMap = LongSparseArray<DownloadInfo>(allInfoList.size + 10)\n        mAllInfoMap = allInfoMap\n        for (info in allInfoList) {\n\n            // Add to all info map\n            allInfoMap.put(info.gid, info)\n\n            // Add to each label list\n            val label = info.label\n\n            var list = getInfoListForLabel(label)\n            if (list == null) {\n                // Can't find the label in label list\n                list = LinkedList()\n                map[label] = list\n                if (!containLabel(label)) {\n                    // Add label to DB and list\n                    labels.add(EhDB.addDownloadLabel(label!!))\n                }\n            }\n            list.add(info)\n        }\n        mWaitList = LinkedList()\n        mSpeedReminder = SpeedReminder()\n        mDownloadInfoListeners = ArrayList()\n    }\n\n    fun getDownloadDirname(gid: Long): String? = mAllDownloadDirname[gid]\n\n    fun putDownloadDirname(gid: Long, dirname: String) {\n        mAllDownloadDirname.put(gid, dirname)\n        EhDB.putDownloadDirname(gid, dirname)\n    }\n\n    fun removeDownloadDirname(gid: Long) {\n        mAllDownloadDirname.remove(gid)\n        EhDB.removeDownloadDirname(gid)\n    }\n\n    private fun getInfoListForLabel(label: String?): LinkedList<DownloadInfo>? = if (label == null) {\n        mDefaultInfoList\n    } else {\n        mMap[label]\n    }\n\n    fun containLabel(label: String?): Boolean {\n        if (label == null) {\n            return false\n        }\n        for ((_, label1) in mLabelList) {\n            if (label == label1) {\n                return true\n            }\n        }\n        return false\n    }\n\n    fun containDownloadInfo(gid: Long): Boolean = mAllInfoMap.indexOfKey(gid) >= 0\n\n    val labelList: List<DownloadLabel>\n        get() = mLabelList\n    val allDownloadInfoList: MutableList<DownloadInfo>\n        get() = mAllInfoList\n    val defaultDownloadInfoList: MutableList<DownloadInfo>\n        get() = mDefaultInfoList\n\n    fun getLabelDownloadInfoList(label: String?): MutableList<DownloadInfo>? = mMap[label]\n\n    fun getDownloadInfo(gid: Long): DownloadInfo? = mAllInfoMap[gid]\n\n    fun getDownloadState(gid: Long): Int {\n        val info = mAllInfoMap[gid]\n        return info?.state ?: DownloadInfo.STATE_INVALID\n    }\n\n    fun addDownloadInfoListener(downloadInfoListener: DownloadInfoListener?) {\n        mDownloadInfoListeners.add(downloadInfoListener)\n    }\n\n    fun removeDownloadInfoListener(downloadInfoListener: DownloadInfoListener?) {\n        mDownloadInfoListeners.remove(downloadInfoListener)\n    }\n\n    fun setDownloadListener(listener: DownloadListener?) {\n        mDownloadListener = listener\n    }\n\n    private fun ensureDownload() {\n        if (mCurrentTask != null) {\n            // Only one download\n            return\n        }\n\n        // Get download from wait list\n        if (!mWaitList.isEmpty()) {\n            val info = mWaitList.removeFirst()\n            val spider = SpiderQueen.obtainSpiderQueen(info, SpiderQueen.MODE_DOWNLOAD)\n            mCurrentTask = info\n            mCurrentSpider = spider\n            spider.addOnSpiderListener(this)\n            info.state = DownloadInfo.STATE_DOWNLOAD\n            info.speed = -1\n            info.remaining = -1\n            info.total = -1\n            info.finished = 0\n            info.downloaded = 0\n            info.legacy = -1\n            // Update in DB\n            EhDB.putDownloadInfo(info)\n            // Start speed count\n            mSpeedReminder.start()\n            // Notify start downloading\n            if (mDownloadListener != null) {\n                mDownloadListener!!.onStart(info)\n            }\n            // Notify state update\n            val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n            if (list != null) {\n                for (l in mDownloadInfoListeners) {\n                    l!!.onUpdate(info, list)\n                }\n            }\n        }\n    }\n\n    fun startDownload(galleryInfo: GalleryInfo, label: String?) {\n        if (mCurrentTask != null && mCurrentTask!!.gid == galleryInfo.gid) {\n            // It is current task\n            return\n        }\n\n        // Check in download list\n        var info = mAllInfoMap[galleryInfo.gid]\n        if (info != null) { // Get it in download list\n            if (info.state != DownloadInfo.STATE_WAIT) {\n                // Set state DownloadInfo.STATE_WAIT\n                info.state = DownloadInfo.STATE_WAIT\n                // Add to wait list\n                mWaitList.add(info)\n                // Update in DB\n                EhDB.putDownloadInfo(info)\n                // Notify state update\n                val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n                if (list != null) {\n                    for (l in mDownloadInfoListeners) {\n                        l!!.onUpdate(info, list)\n                    }\n                }\n                // Make sure download is running\n                ensureDownload()\n            }\n        } else {\n            // It is new download info\n            info = DownloadInfo(galleryInfo)\n            info.label = label\n            info.state = DownloadInfo.STATE_WAIT\n            info.time = System.currentTimeMillis()\n\n            // Add to label download list\n            val list = getInfoListForLabel(info.label)\n            if (list == null) {\n                Log.e(TAG, \"Can't find download info list with label: $label\")\n                return\n            }\n            list.addFirst(info)\n\n            // Add to all download list and map\n            mAllInfoList.addFirst(info)\n            mAllInfoMap.put(galleryInfo.gid, info)\n\n            // Add to wait list\n            mWaitList.add(info)\n\n            // Save to\n            EhDB.putDownloadInfo(info)\n\n            // Notify\n            for (l in mDownloadInfoListeners) {\n                l!!.onAdd(info, list, list.size - 1)\n            }\n            // Make sure download is running\n            ensureDownload()\n\n            // Add it to history\n            EhDB.putHistoryInfo(info)\n        }\n    }\n\n    fun startRangeDownload(gidList: LongList) {\n        var update = false\n        for (i in 0 until gidList.size) {\n            val gid = gidList[i]\n            val info = mAllInfoMap[gid]\n            if (null == info) {\n                Log.d(TAG, \"Can't get download info with gid: $gid\")\n                continue\n            }\n            if (info.state == DownloadInfo.STATE_NONE || info.state == DownloadInfo.STATE_FAILED || info.state == DownloadInfo.STATE_FINISH) {\n                update = true\n                // Set state DownloadInfo.STATE_WAIT\n                info.state = DownloadInfo.STATE_WAIT\n                // Add to wait list\n                mWaitList.add(info)\n                // Update in DB\n                EhDB.putDownloadInfo(info)\n            }\n        }\n        if (update) {\n            // Notify Listener\n            for (l in mDownloadInfoListeners) {\n                l!!.onUpdateAll()\n            }\n            // Ensure download\n            ensureDownload()\n        }\n    }\n\n    fun startAllDownload() {\n        var update = false\n        // Start all STATE_NONE and STATE_FAILED item\n        for (info in mAllInfoList) {\n            if (info.state == DownloadInfo.STATE_NONE || info.state == DownloadInfo.STATE_FAILED) {\n                update = true\n                // Set state DownloadInfo.STATE_WAIT\n                info.state = DownloadInfo.STATE_WAIT\n                // Add to wait list\n                mWaitList.add(info)\n                // Update in DB\n                EhDB.putDownloadInfo(info)\n            }\n        }\n        if (update) {\n            // Notify Listener\n            for (l in mDownloadInfoListeners) {\n                l!!.onUpdateAll()\n            }\n            // Ensure download\n            ensureDownload()\n        }\n    }\n\n    fun addDownload(downloadInfoList: List<DownloadInfo>, notify: Boolean = true) {\n        for (info in downloadInfoList) {\n            if (containDownloadInfo(info.gid)) {\n                // Contain\n                return\n            }\n\n            // Ensure download state\n            if (DownloadInfo.STATE_WAIT == info.state ||\n                DownloadInfo.STATE_DOWNLOAD == info.state\n            ) {\n                info.state = DownloadInfo.STATE_NONE\n            }\n\n            // Add to label download list\n            var list = getInfoListForLabel(info.label)\n            if (null == list) {\n                // Can't find the label in label list\n                list = LinkedList()\n                mMap[info.label] = list\n                if (!containLabel(info.label)) {\n                    // Add label to DB and list\n                    mLabelList.add(EhDB.addDownloadLabel(info.label!!))\n                }\n            }\n            list.add(info)\n            // Sort\n            list.sortByDateDescending()\n\n            // Add to all download list and map\n            mAllInfoList.add(info)\n            mAllInfoMap.put(info.gid, info)\n\n            // Save to\n            EhDB.putDownloadInfo(info)\n        }\n\n        // Sort all download list\n        mAllInfoList.sortByDateDescending()\n\n        // Notify\n        if (notify) {\n            for (l in mDownloadInfoListeners) {\n                l!!.onReload()\n            }\n        }\n    }\n\n    fun addDownloadLabel(downloadLabelList: List<DownloadLabel>) {\n        for (label in downloadLabelList) {\n            val labelString = label.label\n            if (!containLabel(labelString)) {\n                mMap[labelString] = LinkedList()\n                mLabelList.add(EhDB.addDownloadLabel(label))\n            }\n        }\n    }\n\n    fun addDownload(galleryInfo: GalleryInfo, label: String?) {\n        if (containDownloadInfo(galleryInfo.gid)) {\n            // Contain\n            return\n        }\n\n        // It is new download info\n        val info = DownloadInfo(galleryInfo)\n        info.label = label\n        info.state = DownloadInfo.STATE_NONE\n        info.time = System.currentTimeMillis()\n\n        // Add to label download list\n        val list = getInfoListForLabel(info.label)\n        if (list == null) {\n            Log.e(TAG, \"Can't find download info list with label: $label\")\n            return\n        }\n        list.addFirst(info)\n\n        // Add to all download list and map\n        mAllInfoList.addFirst(info)\n        mAllInfoMap.put(galleryInfo.gid, info)\n\n        // Save to\n        EhDB.putDownloadInfo(info)\n\n        // Notify\n        for (l in mDownloadInfoListeners) {\n            l!!.onAdd(info, list, list.size - 1)\n        }\n    }\n\n    fun stopDownload(gid: Long) {\n        val info = stopDownloadInternal(gid)\n        if (info != null) {\n            // Update listener\n            val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n            if (list != null) {\n                for (l in mDownloadInfoListeners) {\n                    l!!.onUpdate(info, list)\n                }\n            }\n            // Ensure download\n            ensureDownload()\n        }\n    }\n\n    fun stopCurrentDownload() {\n        val info = stopCurrentDownloadInternal()\n        if (info != null) {\n            // Update listener\n            val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n            if (list != null) {\n                for (l in mDownloadInfoListeners) {\n                    l!!.onUpdate(info, list)\n                }\n            }\n            // Ensure download\n            ensureDownload()\n        }\n    }\n\n    fun stopRangeDownload(gidList: LongList) {\n        stopRangeDownloadInternal(gidList)\n\n        // Update listener\n        for (l in mDownloadInfoListeners) {\n            l!!.onUpdateAll()\n        }\n\n        // Ensure download\n        ensureDownload()\n    }\n\n    fun stopAllDownload() {\n        // Stop all in wait list\n        for (info in mWaitList) {\n            info.state = DownloadInfo.STATE_NONE\n            // Update in DB\n            EhDB.putDownloadInfo(info)\n        }\n        mWaitList.clear()\n\n        // Stop current\n        stopCurrentDownloadInternal()\n\n        // Notify mDownloadInfoListener\n        for (l in mDownloadInfoListeners) {\n            l!!.onUpdateAll()\n        }\n    }\n\n    fun deleteDownload(gid: Long) {\n        stopDownloadInternal(gid)\n        val info = mAllInfoMap[gid]\n        if (info != null) {\n            // Remove from DB\n            EhDB.removeDownloadInfo(info)\n\n            // Remove all list and map\n            mAllInfoList.remove(info)\n            mAllInfoMap.remove(info.gid)\n\n            // Remove label list\n            val list = getInfoListForLabel(info.label)\n            if (list != null) {\n                val index = list.indexOf(info)\n                if (index >= 0) {\n                    list.remove(info)\n                    // Update listener\n                    for (l in mDownloadInfoListeners) {\n                        l!!.onRemove(info, list, index)\n                    }\n                }\n            }\n\n            // Ensure download\n            ensureDownload()\n        }\n    }\n\n    fun deleteRangeDownload(gidList: LongList) {\n        stopRangeDownloadInternal(gidList)\n        for (i in 0 until gidList.size) {\n            val gid = gidList[i]\n            val info = mAllInfoMap[gid]\n            if (null == info) {\n                Log.d(TAG, \"Can't get download info with gid: $gid\")\n                continue\n            }\n\n            // Remove from DB\n            EhDB.removeDownloadInfo(info)\n\n            // Remove from all info map\n            mAllInfoList.remove(info)\n            mAllInfoMap.remove(info.gid)\n\n            // Remove from label list\n            val list = getInfoListForLabel(info.label)\n            list?.remove(info)\n        }\n\n        // Update listener\n        for (l in mDownloadInfoListeners) {\n            l!!.onReload()\n        }\n\n        // Ensure download\n        ensureDownload()\n    }\n\n    fun moveDownload(fromPosition: Int, toPosition: Int) {\n        if (fromPosition > toPosition) {\n            val time = mAllInfoList[toPosition].time\n            for (i in toPosition until fromPosition) {\n                val aTime = mAllInfoList[i].time\n                val bTime = mAllInfoList[i + 1].time\n                mAllInfoList[i].time = if (aTime == bTime) bTime + 1 else bTime\n            }\n            mAllInfoList[fromPosition].time = time\n            EhDB.updateDownloadInfo(mAllInfoList.slice(toPosition..fromPosition))\n        } else {\n            val time = mAllInfoList[fromPosition].time\n            for (i in fromPosition until toPosition) {\n                val aTime = mAllInfoList[i].time\n                val bTime = mAllInfoList[i + 1].time\n                mAllInfoList[i].time = if (aTime == bTime) bTime - 1 else bTime\n            }\n            mAllInfoList[toPosition].time = time\n            EhDB.updateDownloadInfo(mAllInfoList.slice(fromPosition..toPosition))\n        }\n        val label = mAllInfoList[fromPosition].label\n        mAllInfoList.sortByDateDescending()\n        val list = getInfoListForLabel(label)!!\n        list.sortByDateDescending()\n    }\n\n    fun moveDownload(label: String?, fromPosition: Int, toPosition: Int) {\n        val list = getInfoListForLabel(label)\n        list?.let {\n            val absFromPosition = mAllInfoList.indexOf(it[fromPosition])\n            val absToPosition = mAllInfoList.indexOf(it[toPosition])\n            moveDownload(absFromPosition, absToPosition)\n        }\n    }\n\n    suspend fun resetAllReadingProgress() = coroutineScope {\n        mAllInfoMap.keyIterator().forEach { gid ->\n            launch {\n                runCatching {\n                    resetReadingProgress(gid)\n                }.onFailure {\n                    Log.e(TAG, \"Can't write SpiderInfo\", it)\n                }\n            }\n        }\n    }\n\n    private fun resetReadingProgress(gid: Long) {\n        val downloadDir = SpiderDen.getGalleryDownloadDir(gid) ?: return\n        val file = downloadDir.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return\n        val spiderInfo = readFromUniFile(file) ?: return\n        spiderInfo.startPage = 0\n        spiderInfo.saveToUniFile(file)\n    }\n\n    // Update in DB\n    // Update listener\n    // No ensureDownload\n    private fun stopDownloadInternal(gid: Long): DownloadInfo? {\n        // Check current task\n        if (mCurrentTask != null && mCurrentTask!!.gid == gid) {\n            // Stop current\n            return stopCurrentDownloadInternal()\n        }\n        val iterator = mWaitList.iterator()\n        while (iterator.hasNext()) {\n            val info = iterator.next()\n            if (info.gid == gid) {\n                // Remove from wait list\n                iterator.remove()\n                // Update state\n                info.state = DownloadInfo.STATE_NONE\n                // Update in DB\n                EhDB.putDownloadInfo(info)\n                return info\n            }\n        }\n        return null\n    }\n\n    // Update in DB\n    // Update mDownloadListener\n    private fun stopCurrentDownloadInternal(): DownloadInfo? {\n        val info = mCurrentTask\n        val spider = mCurrentSpider\n        // Release spider\n        if (spider != null) {\n            spider.removeOnSpiderListener(this@DownloadManager)\n            SpiderQueen.releaseSpiderQueen(spider, SpiderQueen.MODE_DOWNLOAD)\n        }\n        mCurrentTask = null\n        mCurrentSpider = null\n        // Stop speed reminder\n        mSpeedReminder.stop()\n        if (info == null) {\n            return null\n        }\n\n        // Update state\n        info.state = DownloadInfo.STATE_NONE\n        // Update in DB\n        EhDB.putDownloadInfo(info)\n        // Listener\n        if (mDownloadListener != null) {\n            mDownloadListener!!.onCancel(info)\n        }\n        return info\n    }\n\n    // Update in DB\n    // Update mDownloadListener\n    private fun stopRangeDownloadInternal(gidList: LongList) {\n        // Two way\n        if (gidList.size < mWaitList.size) {\n            for (i in 0 until gidList.size) {\n                stopDownloadInternal(gidList[i])\n            }\n        } else {\n            // Check current task\n            if (mCurrentTask != null && gidList.contains(mCurrentTask!!.gid)) {\n                // Stop current\n                stopCurrentDownloadInternal()\n            }\n\n            // Check all in wait list\n            val iterator = mWaitList.iterator()\n            while (iterator.hasNext()) {\n                val info = iterator.next()\n                if (gidList.contains(info.gid)) {\n                    // Remove from wait list\n                    iterator.remove()\n                    // Update state\n                    info.state = DownloadInfo.STATE_NONE\n                    // Update in DB\n                    EhDB.putDownloadInfo(info)\n                }\n            }\n        }\n    }\n\n    /**\n     * @param label Not allow new label\n     */\n    fun changeLabel(list: List<DownloadInfo>, label: String?) {\n        if (null != label && !containLabel(label)) {\n            Log.e(TAG, \"Not exits label: $label\")\n            return\n        }\n        val dstList: MutableList<DownloadInfo>? = getInfoListForLabel(label)\n        if (dstList == null) {\n            Log.e(TAG, \"Can't find label with label: $label\")\n            return\n        }\n        for (info in list) {\n            if (ObjectUtils.equal(info.label, label)) {\n                continue\n            }\n            val srcList: MutableList<DownloadInfo>? = getInfoListForLabel(info.label)\n            if (srcList == null) {\n                Log.e(TAG, \"Can't find label with label: \" + info.label)\n                continue\n            }\n            srcList.remove(info)\n            dstList.add(info)\n            info.label = label\n            dstList.sortByDateDescending()\n\n            // Save to DB\n            EhDB.putDownloadInfo(info)\n        }\n        for (l in mDownloadInfoListeners) {\n            l!!.onReload()\n        }\n    }\n\n    fun addLabel(label: String?) {\n        if (label == null || containLabel(label)) {\n            return\n        }\n        mLabelList.add(EhDB.addDownloadLabel(label))\n        mMap[label] = LinkedList()\n        for (l in mDownloadInfoListeners) {\n            l!!.onUpdateLabels()\n        }\n    }\n\n    fun moveLabel(fromPosition: Int, toPosition: Int) {\n        val item = mLabelList.removeAt(fromPosition)\n        mLabelList.add(toPosition, item)\n        EhDB.moveDownloadLabel(fromPosition, toPosition)\n        for (l in mDownloadInfoListeners) {\n            l!!.onUpdateLabels()\n        }\n    }\n\n    fun renameLabel(from: String, to: String) {\n        // Find in label list\n        var found = false\n        for (raw in mLabelList) {\n            if (from == raw.label) {\n                found = true\n                raw.label = to\n                // Update in DB\n                EhDB.updateDownloadLabel(raw)\n                break\n            }\n        }\n        if (!found) {\n            return\n        }\n        val list = mMap.remove(from) ?: return\n\n        // Update info label\n        for (info in list) {\n            info.label = to\n            // Update in DB\n            EhDB.putDownloadInfo(info)\n        }\n        // Put list back with new label\n        mMap[to] = list\n\n        // Notify listener\n        for (l in mDownloadInfoListeners) {\n            l!!.onRenameLabel(from, to)\n        }\n    }\n\n    fun deleteLabel(label: String) {\n        // Find in label list and remove\n        var found = false\n        val iterator = mLabelList.iterator()\n        while (iterator.hasNext()) {\n            val raw = iterator.next()\n            if (label == raw.label) {\n                found = true\n                iterator.remove()\n                EhDB.removeDownloadLabel(raw)\n                break\n            }\n        }\n        if (!found) {\n            return\n        }\n        val list = mMap.remove(label) ?: return\n\n        // Update info label\n        for (info in list) {\n            info.label = null\n            // Update in DB\n            EhDB.putDownloadInfo(info)\n            mDefaultInfoList.add(info)\n        }\n\n        // Sort\n        mDefaultInfoList.sortByDateDescending()\n\n        // Notify listener\n        for (l in mDownloadInfoListeners) {\n            l!!.onChange()\n        }\n    }\n\n    val isIdle: Boolean\n        get() = mCurrentTask == null && mWaitList.isEmpty()\n\n    override fun onGetPages(pages: Int) {\n        var task = mNotifyTaskPool.pop()\n        if (task == null) {\n            task = NotifyTask()\n        }\n        task.setOnGetPagesData(pages)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onGet509(index: Int) {\n        var task = mNotifyTaskPool.pop()\n        if (task == null) {\n            task = NotifyTask()\n        }\n        task.setOnGet509Data(index)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onPageDownload(\n        index: Int,\n        contentLength: Long,\n        receivedSize: Long,\n        bytesRead: Int,\n    ) {\n        var task = mNotifyTaskPool.pop()\n        if (task == null) {\n            task = NotifyTask()\n        }\n        task.setOnPageDownloadData(index, contentLength, receivedSize, bytesRead)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {\n        var task = mNotifyTaskPool.pop()\n        if (task == null) {\n            task = NotifyTask()\n        }\n        task.setOnPageSuccessData(index, finished, downloaded, total)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onPageFailure(\n        index: Int,\n        error: String?,\n        finished: Int,\n        downloaded: Int,\n        total: Int,\n    ) {\n        var task = mNotifyTaskPool.pop()\n        if (task == null) {\n            task = NotifyTask()\n        }\n        task.setOnPageFailureDate(index, error, finished, downloaded, total)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onFinish(finished: Int, downloaded: Int, total: Int) {\n        var task = mNotifyTaskPool.pop()\n        if (task == null) {\n            task = NotifyTask()\n        }\n        task.setOnFinishDate(finished, downloaded, total)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onGetImageSuccess(index: Int, image: Image?) {\n        // Ignore\n    }\n\n    override fun onGetImageFailure(index: Int, error: String?) {\n        // Ignore\n    }\n\n    interface DownloadInfoListener {\n        /**\n         * Add the special info to the special position\n         */\n        fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int)\n\n        /**\n         * The special info is changed\n         */\n        fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>)\n\n        /**\n         * Maybe all data is changed, but size is the same\n         */\n        fun onUpdateAll()\n\n        /**\n         * Maybe all data is changed, maybe list is changed\n         */\n        fun onReload()\n\n        /**\n         * The list is gone, use default list please\n         */\n        fun onChange()\n\n        /**\n         * Rename label\n         */\n        fun onRenameLabel(from: String, to: String)\n\n        /**\n         * Remove the special info from the special position\n         */\n        fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int)\n        fun onUpdateLabels()\n    }\n\n    interface DownloadListener {\n        /**\n         * Get 509 error\n         */\n        fun onGet509()\n\n        /**\n         * Start download\n         */\n        fun onStart(info: DownloadInfo)\n\n        /**\n         * Update download speed\n         */\n        fun onDownload(info: DownloadInfo)\n\n        /**\n         * Update page downloaded\n         */\n        fun onGetPage(info: DownloadInfo)\n\n        /**\n         * Download done\n         */\n        fun onFinish(info: DownloadInfo)\n\n        /**\n         * Download done\n         */\n        fun onCancel(info: DownloadInfo)\n    }\n\n    private class NotifyTask : Runnable {\n        private var mType = 0\n        private var mPages = 0\n        private var mIndex = 0\n        private var mContentLength: Long = 0\n        private var mReceivedSize: Long = 0\n        private var mBytesRead = 0\n        private var mError: String? = null\n        private var mFinished = 0\n        private var mDownloaded = 0\n        private var mTotal = 0\n        fun setOnGetPagesData(pages: Int) {\n            mType = TYPE_ON_GET_PAGES\n            mPages = pages\n        }\n\n        fun setOnGet509Data(index: Int) {\n            mType = TYPE_ON_GET_509\n            mIndex = index\n        }\n\n        fun setOnPageDownloadData(\n            index: Int,\n            contentLength: Long,\n            receivedSize: Long,\n            bytesRead: Int,\n        ) {\n            mType = TYPE_ON_PAGE_DOWNLOAD\n            mIndex = index\n            mContentLength = contentLength\n            mReceivedSize = receivedSize\n            mBytesRead = bytesRead\n        }\n\n        fun setOnPageSuccessData(index: Int, finished: Int, downloaded: Int, total: Int) {\n            mType = TYPE_ON_PAGE_SUCCESS\n            mIndex = index\n            mFinished = finished\n            mDownloaded = downloaded\n            mTotal = total\n        }\n\n        fun setOnPageFailureDate(\n            index: Int,\n            error: String?,\n            finished: Int,\n            downloaded: Int,\n            total: Int,\n        ) {\n            mType = TYPE_ON_PAGE_FAILURE\n            mIndex = index\n            mError = error\n            mFinished = finished\n            mDownloaded = downloaded\n            mTotal = total\n        }\n\n        fun setOnFinishDate(finished: Int, downloaded: Int, total: Int) {\n            mType = TYPE_ON_FINISH\n            mFinished = finished\n            mDownloaded = downloaded\n            mTotal = total\n        }\n\n        override fun run() {\n            when (mType) {\n                TYPE_ON_GET_PAGES -> {\n                    val info = mCurrentTask\n                    if (info == null) {\n                        Log.e(TAG, \"Current task is null, but it should not be\")\n                    } else {\n                        info.total = mPages\n                        val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n                        if (list != null) {\n                            for (l in mDownloadInfoListeners) {\n                                l!!.onUpdate(info, list)\n                            }\n                        }\n                    }\n                }\n                TYPE_ON_GET_509 -> {\n                    if (mDownloadListener != null) {\n                        mDownloadListener!!.onGet509()\n                    }\n                }\n                TYPE_ON_PAGE_DOWNLOAD -> mSpeedReminder.onDownload(\n                    mIndex,\n                    mContentLength,\n                    mReceivedSize,\n                    mBytesRead,\n                )\n                TYPE_ON_PAGE_SUCCESS -> {\n                    mSpeedReminder.onDone(mIndex)\n                    val info = mCurrentTask\n                    if (info == null) {\n                        Log.e(TAG, \"Current task is null, but it should not be\")\n                    } else {\n                        info.finished = mFinished\n                        info.downloaded = mDownloaded\n                        info.total = mTotal\n                        if (mDownloadListener != null) {\n                            mDownloadListener!!.onGetPage(info)\n                        }\n                        val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n                        if (list != null) {\n                            for (l in mDownloadInfoListeners) {\n                                l!!.onUpdate(info, list)\n                            }\n                        }\n                    }\n                }\n                TYPE_ON_PAGE_FAILURE -> {\n                    mSpeedReminder.onDone(mIndex)\n                    val info = mCurrentTask\n                    if (info == null) {\n                        Log.e(TAG, \"Current task is null, but it should not be\")\n                    } else {\n                        info.finished = mFinished\n                        info.downloaded = mDownloaded\n                        info.total = mTotal\n                        val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n                        if (list != null) {\n                            for (l in mDownloadInfoListeners) {\n                                l!!.onUpdate(info, list)\n                            }\n                        }\n                    }\n                }\n                TYPE_ON_FINISH -> {\n                    mSpeedReminder.onFinish()\n                    // Download done\n                    val info = mCurrentTask\n                    mCurrentTask = null\n                    val spider = mCurrentSpider\n                    mCurrentSpider = null\n                    // Release spider\n                    if (spider != null) {\n                        spider.removeOnSpiderListener(DownloadManager)\n                        SpiderQueen.releaseSpiderQueen(spider, SpiderQueen.MODE_DOWNLOAD)\n                    }\n                    // Check null\n                    if (info == null || spider == null) {\n                        Log.e(TAG, \"Current stuff is null, but it should not be\")\n                    } else {\n                        // Stop speed count\n                        mSpeedReminder.stop()\n                        // Update state\n                        info.finished = mFinished\n                        info.downloaded = mDownloaded\n                        info.total = mTotal\n                        info.legacy = mTotal - mFinished\n                        if (info.legacy == 0) {\n                            info.state = DownloadInfo.STATE_FINISH\n                        } else {\n                            info.state = DownloadInfo.STATE_FAILED\n                        }\n                        // Update in DB\n                        EhDB.putDownloadInfo(info)\n                        // Notify\n                        if (mDownloadListener != null) {\n                            mDownloadListener!!.onFinish(info)\n                        }\n                        val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n                        if (list != null) {\n                            for (l in mDownloadInfoListeners) {\n                                l!!.onUpdate(info, list)\n                            }\n                        }\n                        // Start next download\n                        ensureDownload()\n                    }\n                }\n            }\n            mNotifyTaskPool.push(this)\n        }\n    }\n\n    internal class SpeedReminder : Runnable {\n        private val mContentLengthMap = SparseLongArray()\n        private val mReceivedSizeMap = SparseLongArray()\n        private var mStop = true\n        private var mBytesRead: Long = 0\n        private var oldSpeed: Long = -1\n        fun start() {\n            if (mStop) {\n                mStop = false\n                SimpleHandler.getInstance().post(this)\n            }\n        }\n\n        fun stop() {\n            if (!mStop) {\n                mStop = true\n                mBytesRead = 0\n                oldSpeed = -1\n                mContentLengthMap.clear()\n                mReceivedSizeMap.clear()\n                SimpleHandler.getInstance().removeCallbacks(this)\n            }\n        }\n\n        fun onDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {\n            mContentLengthMap.put(index, contentLength)\n            mReceivedSizeMap.put(index, receivedSize)\n            mBytesRead += bytesRead.toLong()\n        }\n\n        fun onDone(index: Int) {\n            mContentLengthMap.delete(index)\n            mReceivedSizeMap.delete(index)\n        }\n\n        fun onFinish() {\n            mContentLengthMap.clear()\n            mReceivedSizeMap.clear()\n        }\n\n        override fun run() {\n            val info = mCurrentTask\n            if (info != null) {\n                var newSpeed = mBytesRead / 2\n                if (oldSpeed != -1L) {\n                    newSpeed =\n                        MathUtils.lerp(oldSpeed.toFloat(), newSpeed.toFloat(), 0.75f).toLong()\n                }\n                oldSpeed = newSpeed\n                info.speed = newSpeed\n\n                // Calculate remaining\n                if (info.total <= 0) {\n                    info.remaining = -1\n                } else if (newSpeed == 0L) {\n                    info.remaining = 300L * 24L * 60L * 60L * 1000L // 300 days\n                } else {\n                    var downloadingCount = 0\n                    var downloadingContentLengthSum: Long = 0\n                    var totalSize: Long = 0\n                    for (i in 0 until maxOf(mContentLengthMap.size, mReceivedSizeMap.size)) {\n                        val contentLength = mContentLengthMap.valueAt(i)\n                        val receivedSize = mReceivedSizeMap.valueAt(i)\n                        downloadingCount++\n                        downloadingContentLengthSum += contentLength\n                        totalSize += contentLength - receivedSize\n                    }\n                    if (downloadingCount != 0) {\n                        totalSize += downloadingContentLengthSum * (info.total - info.downloaded - downloadingCount) / downloadingCount\n                        info.remaining = totalSize / newSpeed * 1000\n                    }\n                }\n                if (mDownloadListener != null) {\n                    mDownloadListener!!.onDownload(info)\n                }\n                val list: List<DownloadInfo>? = getInfoListForLabel(info.label)\n                if (list != null) {\n                    for (l in mDownloadInfoListeners) {\n                        l!!.onUpdate(info, list)\n                    }\n                }\n            }\n            mBytesRead = 0\n            if (!mStop) {\n                SimpleHandler.getInstance().postDelayed(this, 2000)\n            }\n        }\n    }\n\n    private val TAG = DownloadManager::class.java.simpleName\n    private const val TYPE_ON_GET_PAGES = 0\n    private const val TYPE_ON_GET_509 = 1\n    private const val TYPE_ON_PAGE_DOWNLOAD = 2\n    private const val TYPE_ON_PAGE_SUCCESS = 3\n    private const val TYPE_ON_PAGE_FAILURE = 4\n    private const val TYPE_ON_FINISH = 5\n\n    private fun MutableList<DownloadInfo>.sortByDateDescending() {\n        sortByDescending { it.time }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/download/DownloadService.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.download\n\nimport android.Manifest\nimport android.app.PendingIntent\nimport android.app.Service\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Bundle\nimport android.os.IBinder\nimport android.os.SystemClock\nimport android.util.Log\nimport androidx.annotation.IntDef\nimport androidx.collection.LongSparseArray\nimport androidx.core.app.ActivityCompat\nimport androidx.core.app.NotificationChannelCompat\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.app.ServiceCompat\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.ui.MainActivity\nimport com.hippo.ehviewer.ui.scene.DownloadsScene\nimport com.hippo.scene.StageActivity\nimport com.hippo.util.ReadableTime\nimport com.hippo.util.getParcelableExtraCompat\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.SimpleHandler\nimport com.hippo.yorozuya.collect.LongList\n\nclass DownloadService :\n    Service(),\n    DownloadManager.DownloadListener {\n    private var mNotifyManager: NotificationManagerCompat? = null\n    private var mDownloadManager: DownloadManager? = null\n    private var mDownloadingBuilder: NotificationCompat.Builder? = null\n    private var mDownloadedBuilder: NotificationCompat.Builder? = null\n    private var m509dBuilder: NotificationCompat.Builder? = null\n    private var mDownloadingDelay: NotificationDelay? = null\n    private var mDownloadedDelay: NotificationDelay? = null\n    private var m509Delay: NotificationDelay? = null\n    private var mChannelID: String? = null\n\n    override fun onCreate() {\n        super.onCreate()\n        mChannelID = \"$packageName.download\"\n        mNotifyManager = NotificationManagerCompat.from(this)\n        mNotifyManager!!.createNotificationChannel(\n            NotificationChannelCompat.Builder(\n                mChannelID!!,\n                NotificationManagerCompat.IMPORTANCE_LOW,\n            )\n                .setName(getString(R.string.download_service))\n                .build(),\n        )\n        mDownloadManager = DownloadManager\n        mDownloadManager!!.setDownloadListener(this)\n        ensureDownloadingBuilder()\n        mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))\n            .setContentText(null)\n            .setSubText(null)\n            .setProgress(0, 0, true)\n        startForeground(ID_DOWNLOADING, mDownloadingBuilder!!.build())\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mNotifyManager = null\n        if (mDownloadManager != null) {\n            mDownloadManager!!.setDownloadListener(null)\n            mDownloadManager = null\n        }\n        mDownloadingBuilder = null\n        mDownloadedBuilder = null\n        m509dBuilder = null\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        handleIntent(intent)\n        return START_STICKY\n    }\n\n    private fun handleIntent(intent: Intent?) {\n        var action: String? = null\n        intent?.action?.let {\n            action = it\n        }\n        if (ACTION_START == action) {\n            val gi = intent!!.getParcelableExtraCompat<GalleryInfo>(KEY_GALLERY_INFO)\n            val label = intent.getStringExtra(KEY_LABEL)\n            if (gi != null && mDownloadManager != null) {\n                mDownloadManager!!.startDownload(gi, label)\n            }\n        } else if (ACTION_START_RANGE == action) {\n            val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)\n            if (gidList != null && mDownloadManager != null) {\n                mDownloadManager!!.startRangeDownload(gidList)\n            }\n        } else if (ACTION_START_ALL == action) {\n            if (mDownloadManager != null) {\n                mDownloadManager!!.startAllDownload()\n            }\n        } else if (ACTION_STOP == action) {\n            val gid = intent!!.getLongExtra(KEY_GID, -1)\n            if (gid != -1L && mDownloadManager != null) {\n                mDownloadManager!!.stopDownload(gid)\n            }\n        } else if (ACTION_STOP_CURRENT == action) {\n            if (mDownloadManager != null) {\n                mDownloadManager!!.stopCurrentDownload()\n            }\n        } else if (ACTION_STOP_RANGE == action) {\n            val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)\n            if (gidList != null && mDownloadManager != null) {\n                mDownloadManager!!.stopRangeDownload(gidList)\n            }\n        } else if (ACTION_STOP_ALL == action) {\n            if (mDownloadManager != null) {\n                mDownloadManager!!.stopAllDownload()\n            }\n        } else if (ACTION_DELETE == action) {\n            val gid = intent!!.getLongExtra(KEY_GID, -1)\n            if (gid != -1L && mDownloadManager != null) {\n                mDownloadManager!!.deleteDownload(gid)\n            }\n        } else if (ACTION_DELETE_RANGE == action) {\n            val gidList = intent!!.getParcelableExtraCompat<LongList>(KEY_GID_LIST)\n            if (gidList != null && mDownloadManager != null) {\n                mDownloadManager!!.deleteRangeDownload(gidList)\n            }\n        } else if (ACTION_CLEAR == action) {\n            clear()\n        }\n        checkStopSelf()\n    }\n\n    override fun onBind(intent: Intent): IBinder? = throw IllegalStateException(\"No bindService\")\n\n    private fun ensureDownloadingBuilder() {\n        if (mDownloadingBuilder != null) {\n            return\n        }\n        val stopAllIntent = Intent(this, DownloadService::class.java)\n        stopAllIntent.action = ACTION_STOP_ALL\n        val piStopAll =\n            PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)\n        mDownloadingBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)\n            .setSmallIcon(android.R.drawable.stat_sys_download)\n            .setOngoing(true)\n            .setAutoCancel(false)\n            .setCategory(NotificationCompat.CATEGORY_PROGRESS)\n            .addAction(\n                R.drawable.v_pause_x24,\n                getString(R.string.stat_download_action_stop_all),\n                piStopAll,\n            )\n            .setShowWhen(false)\n            .setChannelId(mChannelID!!)\n        mDownloadingDelay =\n            NotificationDelay(this, mNotifyManager!!, mDownloadingBuilder!!, ID_DOWNLOADING)\n    }\n\n    private fun ensureDownloadedBuilder() {\n        if (mDownloadedBuilder != null) {\n            return\n        }\n        val clearIntent = Intent(this, DownloadService::class.java)\n        clearIntent.action = ACTION_CLEAR\n        val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)\n        val bundle = Bundle()\n        bundle.putString(DownloadsScene.KEY_ACTION, DownloadsScene.ACTION_CLEAR_DOWNLOAD_SERVICE)\n        val activityIntent = Intent(this, MainActivity::class.java)\n        activityIntent.action = StageActivity.ACTION_START_SCENE\n        activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)\n        activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)\n        val piActivity = PendingIntent.getActivity(\n            this@DownloadService,\n            0,\n            activityIntent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n        )\n        mDownloadedBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)\n            .setSmallIcon(android.R.drawable.stat_sys_download_done)\n            .setContentTitle(getString(R.string.stat_download_done_title))\n            .setDeleteIntent(piClear)\n            .setOngoing(false)\n            .setAutoCancel(true)\n            .setContentIntent(piActivity)\n        mDownloadedDelay =\n            NotificationDelay(this, mNotifyManager!!, mDownloadedBuilder!!, ID_DOWNLOADED)\n    }\n\n    private fun ensure509Builder() {\n        if (m509dBuilder != null) {\n            return\n        }\n        m509dBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)\n            .setSmallIcon(R.drawable.ic_baseline_warning_24)\n            .setContentText(getString(R.string.stat_509_alert_title))\n            .setContentText(getString(R.string.stat_509_alert_text))\n            .setStyle(\n                NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)),\n            )\n            .setAutoCancel(true)\n            .setOngoing(false)\n            .setCategory(NotificationCompat.CATEGORY_ERROR)\n        m509Delay = NotificationDelay(this, mNotifyManager!!, m509dBuilder!!, ID_509)\n    }\n\n    override fun onGet509() {\n        if (mDownloadManager != null) {\n            mDownloadManager!!.stopAllDownload()\n        }\n        if (mNotifyManager == null) {\n            return\n        }\n        ensure509Builder()\n        m509dBuilder!!.setWhen(System.currentTimeMillis())\n        m509Delay!!.show()\n    }\n\n    override fun onStart(info: DownloadInfo) {\n        if (mNotifyManager == null) {\n            return\n        }\n        ensureDownloadingBuilder()\n        val bundle = Bundle()\n        bundle.putLong(DownloadsScene.KEY_GID, info.gid)\n        val activityIntent = Intent(this, MainActivity::class.java)\n        activityIntent.action = StageActivity.ACTION_START_SCENE\n        activityIntent.putExtra(StageActivity.KEY_SCENE_NAME, DownloadsScene::class.java.name)\n        activityIntent.putExtra(StageActivity.KEY_SCENE_ARGS, bundle)\n        val piActivity = PendingIntent.getActivity(\n            this@DownloadService,\n            0,\n            activityIntent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n        )\n        mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))\n            .setContentText(null)\n            .setSubText(null)\n            .setProgress(0, 0, true)\n            .setContentIntent(piActivity)\n        mDownloadingDelay!!.startForeground()\n    }\n\n    private fun onUpdate(info: DownloadInfo) {\n        if (mNotifyManager == null) {\n            return\n        }\n        ensureDownloadingBuilder()\n        var speed = info.speed\n        if (speed < 0) {\n            speed = 0\n        }\n        var text = FileUtils.humanReadableByteCount(speed, false) + \"/s\"\n        val remaining = info.remaining\n        text = if (remaining >= 0) {\n            getString(\n                R.string.download_speed_text_2,\n                text,\n                ReadableTime.getShortTimeInterval(remaining),\n            )\n        } else {\n            getString(R.string.download_speed_text, text)\n        }\n        mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))\n            .setContentText(text)\n            .setStyle(NotificationCompat.BigTextStyle().bigText(text))\n            .setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + \"/\" + info.total)\n            .setProgress(info.total, info.finished, false)\n        mDownloadingDelay!!.startForeground()\n    }\n\n    override fun onDownload(info: DownloadInfo) {\n        onUpdate(info)\n    }\n\n    override fun onGetPage(info: DownloadInfo) {\n        onUpdate(info)\n    }\n\n    override fun onFinish(info: DownloadInfo) {\n        if (mNotifyManager == null) {\n            return\n        }\n        if (null != mDownloadingDelay) {\n            mDownloadingDelay!!.cancel()\n        }\n        ensureDownloadedBuilder()\n        val finish = info.state == DownloadInfo.STATE_FINISH\n        val gid = info.gid\n        val index = sItemStateArray.indexOfKey(gid)\n        if (index < 0) { // Not contain\n            sItemStateArray.put(gid, finish)\n            sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))\n            sDownloadedCount++\n            if (finish) {\n                sFinishedCount++\n            } else {\n                sFailedCount++\n            }\n        } else { // Contain\n            val oldFinish = sItemStateArray.valueAt(index)\n            sItemStateArray.put(gid, finish)\n            sItemTitleArray.put(gid, EhUtils.getSuitableTitle(info))\n            if (oldFinish && !finish) {\n                sFinishedCount--\n                sFailedCount++\n            } else if (!oldFinish && finish) {\n                sFinishedCount++\n                sFailedCount--\n            }\n        }\n        val text: String\n        val needStyle: Boolean\n        if (sFinishedCount != 0 && sFailedCount == 0) {\n            if (sFinishedCount == 1) {\n                text = if (sItemTitleArray.size() >= 1) {\n                    getString(\n                        R.string.stat_download_done_line_succeeded,\n                        sItemTitleArray.valueAt(0),\n                    )\n                } else {\n                    Log.d(\"TAG\", \"WTF, sItemTitleArray is null\")\n                    getString(R.string.error_unknown)\n                }\n                needStyle = false\n            } else {\n                text = getString(R.string.stat_download_done_text_succeeded, sFinishedCount)\n                needStyle = true\n            }\n        } else if (sFinishedCount == 0 && sFailedCount != 0) {\n            if (sFailedCount == 1) {\n                text = if (sItemTitleArray.size() >= 1) {\n                    getString(\n                        R.string.stat_download_done_line_failed,\n                        sItemTitleArray.valueAt(0),\n                    )\n                } else {\n                    Log.d(\"TAG\", \"WTF, sItemTitleArray is null\")\n                    getString(R.string.error_unknown)\n                }\n                needStyle = false\n            } else {\n                text = getString(R.string.stat_download_done_text_failed, sFailedCount)\n                needStyle = true\n            }\n        } else {\n            text = getString(R.string.stat_download_done_text_mix, sFinishedCount, sFailedCount)\n            needStyle = true\n        }\n        val style: NotificationCompat.InboxStyle?\n        if (needStyle) {\n            style = NotificationCompat.InboxStyle()\n            style.setBigContentTitle(getString(R.string.stat_download_done_title))\n            val stateArray = sItemStateArray\n            var i = 0\n            val n = stateArray.size()\n            while (i < n) {\n                val id = stateArray.keyAt(i)\n                val fin = stateArray.valueAt(i)\n                val title = sItemTitleArray[id]\n                if (title == null) {\n                    i++\n                    continue\n                }\n                style.addLine(\n                    getString(\n                        if (fin) R.string.stat_download_done_line_succeeded else R.string.stat_download_done_line_failed,\n                        title,\n                    ),\n                )\n                i++\n            }\n        } else {\n            style = null\n        }\n        mDownloadedBuilder!!.setContentText(text)\n            .setStyle(style)\n            .setWhen(System.currentTimeMillis())\n            .setNumber(sDownloadedCount)\n        mDownloadedDelay!!.show()\n        checkStopSelf()\n    }\n\n    override fun onCancel(info: DownloadInfo) {\n        if (mNotifyManager == null) {\n            return\n        }\n        if (null != mDownloadingDelay) {\n            mDownloadingDelay!!.cancel()\n        }\n        checkStopSelf()\n    }\n\n    private fun checkStopSelf() {\n        if (mDownloadManager == null || mDownloadManager!!.isIdle) {\n            ServiceCompat.stopForeground(this@DownloadService, ServiceCompat.STOP_FOREGROUND_REMOVE)\n            stopSelf()\n        }\n    }\n\n    private class NotificationDelay(\n        private var mService: Service,\n        private val mNotifyManager: NotificationManagerCompat,\n        private val mBuilder: NotificationCompat.Builder,\n        private val mId: Int,\n    ) : Runnable {\n        private var mLastTime: Long = 0\n        private var mPosted = false\n\n        // false for show, true for cancel\n        @Ops\n        private var mOps = 0\n\n        fun show() {\n            if (mPosted) {\n                mOps = OPS_NOTIFY\n            } else {\n                val now = SystemClock.currentThreadTimeMillis()\n                if (now - mLastTime > DELAY) {\n                    // Wait long enough, do it now\n                    if (ActivityCompat.checkSelfPermission(\n                            mService,\n                            Manifest.permission.POST_NOTIFICATIONS,\n                        ) != PackageManager.PERMISSION_GRANTED\n                    ) {\n                        return\n                    }\n                    mNotifyManager.notify(mId, mBuilder.build())\n                } else {\n                    // Too quick, post delay\n                    mOps = OPS_NOTIFY\n                    mPosted = true\n                    SimpleHandler.getInstance().postDelayed(this, DELAY)\n                }\n                mLastTime = now\n            }\n        }\n\n        fun cancel() {\n            if (mPosted) {\n                mOps = OPS_CANCEL\n            } else {\n                val now = SystemClock.currentThreadTimeMillis()\n                if (now - mLastTime > DELAY) {\n                    // Wait long enough, do it now\n                    mNotifyManager.cancel(mId)\n                } else {\n                    // Too quick, post delay\n                    mOps = OPS_CANCEL\n                    mPosted = true\n                    SimpleHandler.getInstance().postDelayed(this, DELAY)\n                }\n            }\n        }\n\n        fun startForeground() {\n            if (mPosted) {\n                mOps = OPS_START_FOREGROUND\n            } else {\n                val now = SystemClock.currentThreadTimeMillis()\n                if (now - mLastTime > DELAY) {\n                    // Wait long enough, do it now\n                    mService.startForeground(mId, mBuilder.build())\n                } else {\n                    // Too quick, post delay\n                    mOps = OPS_START_FOREGROUND\n                    mPosted = true\n                    SimpleHandler.getInstance().postDelayed(this, DELAY)\n                }\n            }\n        }\n\n        override fun run() {\n            mPosted = false\n            when (mOps) {\n                OPS_NOTIFY -> {\n                    if (ActivityCompat.checkSelfPermission(\n                            mService,\n                            Manifest.permission.POST_NOTIFICATIONS,\n                        ) != PackageManager.PERMISSION_GRANTED\n                    ) {\n                        return\n                    }\n                    mNotifyManager.notify(mId, mBuilder.build())\n                }\n                OPS_CANCEL -> mNotifyManager.cancel(mId)\n                OPS_START_FOREGROUND -> mService.startForeground(mId, mBuilder.build())\n            }\n        }\n\n        @IntDef(OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND)\n        @Retention(AnnotationRetention.SOURCE)\n        private annotation class Ops\n        companion object {\n            private const val OPS_NOTIFY = 0\n            private const val OPS_CANCEL = 1\n            private const val OPS_START_FOREGROUND = 2\n            private const val DELAY: Long = 1000 // 1s\n        }\n    }\n\n    companion object {\n        const val ACTION_START = \"start\"\n        const val ACTION_START_RANGE = \"start_range\"\n        const val ACTION_START_ALL = \"start_all\"\n        const val ACTION_STOP = \"stop\"\n        const val ACTION_STOP_RANGE = \"stop_range\"\n        const val ACTION_STOP_CURRENT = \"stop_current\"\n        const val ACTION_STOP_ALL = \"stop_all\"\n        const val ACTION_DELETE = \"delete\"\n        const val ACTION_DELETE_RANGE = \"delete_range\"\n        const val ACTION_CLEAR = \"clear\"\n        const val KEY_GALLERY_INFO = \"gallery_info\"\n        const val KEY_LABEL = \"label\"\n        const val KEY_GID = \"gid\"\n        const val KEY_GID_LIST = \"gid_list\"\n        private const val ID_DOWNLOADING = 1\n        private const val ID_DOWNLOADED = 2\n        private const val ID_509 = 3\n        private val sItemStateArray = LongSparseArray<Boolean>()\n        private val sItemTitleArray = LongSparseArray<String>()\n        private var sFailedCount = 0\n        private var sFinishedCount = 0\n        private var sDownloadedCount = 0\n\n        fun clear() {\n            sFailedCount = 0\n            sFinishedCount = 0\n            sDownloadedCount = 0\n            sItemStateArray.clear()\n            sItemTitleArray.clear()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.kt",
    "content": "/*\n * Copyright 2023 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.gallery\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.ParcelFileDescriptor\nimport android.util.Log\nimport com.hippo.ehviewer.GetText\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.jni.closeArchive\nimport com.hippo.ehviewer.jni.extractToByteBuffer\nimport com.hippo.ehviewer.jni.extractToFd\nimport com.hippo.ehviewer.jni.getFilename\nimport com.hippo.ehviewer.jni.needPassword\nimport com.hippo.ehviewer.jni.openArchive\nimport com.hippo.ehviewer.jni.providePassword\nimport com.hippo.ehviewer.jni.releaseByteBuffer\nimport com.hippo.image.ByteBufferSource\nimport com.hippo.image.Image\nimport com.hippo.unifile.UniFile\nimport com.hippo.yorozuya.FileUtils\nimport java.nio.ByteBuffer\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.CoroutineStart\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.Semaphore\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.sync.withPermit\n\nclass ArchiveGalleryProvider(context: Context, private val uri: Uri, passwdFlow: Flow<String>) :\n    GalleryProvider2(),\n    CoroutineScope {\n    override val coroutineContext = Dispatchers.IO + Job()\n    private lateinit var pfd: ParcelFileDescriptor\n    private val hostJob = launch(start = CoroutineStart.LAZY) {\n        Log.d(DEBUG_TAG, \"Open archive $uri\")\n        pfd = context.contentResolver.openFileDescriptor(uri, \"r\")!!\n        size = openArchive(pfd.fd, pfd.statSize, true)\n        if (size == 0) {\n            return@launch\n        }\n        if (needPassword()) {\n            Settings.archivePasswds?.forEach {\n                if (providePassword(it)) return@launch\n            }\n            passwdFlow.collect {\n                if (providePassword(it)) {\n                    Settings.putPasswdToArchivePasswds(it)\n                    currentCoroutineContext().cancel()\n                }\n            }\n        }\n    }\n\n    override var size = -1\n\n    override fun start() {\n        hostJob.start()\n    }\n\n    override fun stop() {\n        cancel()\n        closeArchive()\n        pfd.close()\n        Log.d(DEBUG_TAG, \"Close archive $uri successfully!\")\n        super.stop()\n    }\n\n    private val mJobMap = hashMapOf<Int, Job>()\n    private val mWorkerMutex by lazy { (0 until size).map { Mutex() } }\n    private val mSemaphore = Semaphore(4)\n\n    override fun onRequest(index: Int) {\n        notifyPageWait(index)\n        synchronized(mJobMap) {\n            val current = mJobMap[index]\n            if (current?.isActive != true) {\n                mJobMap[index] = launch {\n                    mWorkerMutex[index].withLock {\n                        mSemaphore.withPermit {\n                            doRealWork(index)\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private suspend fun doRealWork(index: Int) {\n        val buffer = extractToByteBuffer(index)\n        buffer ?: return\n        check(buffer.isDirect)\n        val src = object : ByteBufferSource {\n            override val source: ByteBuffer = buffer\n            override fun close() {\n                releaseByteBuffer(buffer)\n            }\n        }\n        runCatching {\n            currentCoroutineContext().ensureActive()\n        }.onFailure {\n            src.close()\n            throw it\n        }\n        val image = Image.decode(src) ?: return notifyPageFailed(index, GetText.getString(R.string.error_decoding_failed))\n        runCatching {\n            currentCoroutineContext().ensureActive()\n        }.onFailure {\n            image.recycle()\n            throw it\n        }\n        notifyPageSucceed(index, image)\n    }\n\n    override fun onForceRequest(index: Int) {\n        onRequest(index)\n    }\n\n    override suspend fun awaitReady(): Boolean {\n        hostJob.join()\n        return size != -1\n    }\n\n    override val isReady: Boolean\n        get() = size != -1\n\n    override fun onCancelRequest(index: Int) {\n        mJobMap[index]?.cancel()\n    }\n\n    override fun getImageFilename(index: Int): String = FileUtils.getNameFromFilename(getImageFilenameWithExtension(index))\n\n    override fun getImageFilenameWithExtension(index: Int): String = FileUtils.sanitizeFilename(getFilename(index))\n\n    override fun save(index: Int, file: UniFile) = runCatching {\n        file.openFileDescriptor(\"w\").use {\n            extractToFd(index, it.fd)\n        }\n    }.getOrElse {\n        it.printStackTrace()\n        false\n    }\n\n    override fun save(index: Int, dir: UniFile, filename: String): UniFile {\n        val extension = FileUtils.getExtensionFromFilename(getImageFilenameWithExtension(index))\n        val dst = dir.subFile(if (null != extension) \"$filename.$extension\" else filename)\n        save(index, dst!!)\n        return dst\n    }\n\n    override suspend fun downloadOriginal(index: Int, dir: UniFile, filename: String): UniFile? = null\n\n    override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}\n}\n\nprivate const val DEBUG_TAG = \"ArchiveGalleryProvider\"\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/gallery/EhGalleryProvider.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.gallery\n\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.spider.SpiderQueen\nimport com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen\nimport com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen\nimport com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener\nimport com.hippo.image.Image\nimport com.hippo.unifile.UniFile\nimport com.hippo.yorozuya.SimpleHandler\nimport java.util.Locale\n\nclass EhGalleryProvider(private val mGalleryInfo: GalleryInfo) :\n    GalleryProvider2(),\n    OnSpiderListener {\n    private lateinit var mSpiderQueen: SpiderQueen\n    override fun start() {\n        mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)\n        mSpiderQueen.addOnSpiderListener(this)\n    }\n\n    override fun stop() {\n        super.stop()\n        mSpiderQueen.removeOnSpiderListener(this)\n        // Activity recreate may called, so wait 3000s\n        SimpleHandler.getInstance().postDelayed(ReleaseTask(mSpiderQueen), 3000)\n    }\n\n    override val startPage\n        get() = mSpiderQueen.startPage\n\n    override fun getImageFilename(index: Int): String = String.format(\n        Locale.US,\n        \"%d-%s-%08d\",\n        mGalleryInfo.gid,\n        mGalleryInfo.token,\n        index + 1,\n    )\n\n    override fun getImageFilenameWithExtension(index: Int): String {\n        val extension = mSpiderQueen.getExtension(index)\n        if (extension != null) {\n            return String.format(\n                Locale.US,\n                \"%d-%s-%08d.%s\",\n                mGalleryInfo.gid,\n                mGalleryInfo.token,\n                index + 1,\n                extension,\n            )\n        }\n        return String.format(\n            Locale.US,\n            \"%d-%s-%08d\",\n            mGalleryInfo.gid,\n            mGalleryInfo.token,\n            index + 1,\n        )\n    }\n\n    override fun save(index: Int, file: UniFile): Boolean = mSpiderQueen.save(index, file)\n\n    override fun save(index: Int, dir: UniFile, filename: String): UniFile? = mSpiderQueen.save(index, dir, filename)\n\n    override suspend fun downloadOriginal(index: Int, dir: UniFile, filename: String): UniFile? = mSpiderQueen.downloadOriginal(index, dir, filename)\n\n    override fun putStartPage(page: Int) {\n        mSpiderQueen.putStartPage(page)\n    }\n\n    override val size: Int\n        get() = mSpiderQueen.size\n\n    override fun onRequest(index: Int) {\n        notifyPageWait(index)\n        mSpiderQueen.request(index)\n    }\n\n    override fun onForceRequest(index: Int) {\n        notifyPageWait(index)\n        mSpiderQueen.forceRequest(index)\n    }\n\n    override suspend fun awaitReady(): Boolean = mSpiderQueen.awaitReady()\n\n    override val isReady: Boolean\n        get() = mSpiderQueen.isReady\n\n    override fun onCancelRequest(index: Int) {\n        mSpiderQueen.cancelRequest(index)\n    }\n\n    override fun onGetPages(pages: Int) {}\n\n    override fun onGet509(index: Int) {}\n\n    override fun onPageDownload(\n        index: Int,\n        contentLength: Long,\n        receivedSize: Long,\n        bytesRead: Int,\n    ) {\n        if (contentLength > 0) {\n            notifyPagePercent(index, receivedSize.toFloat() / contentLength)\n        }\n    }\n\n    override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}\n\n    override fun onPageFailure(\n        index: Int,\n        error: String?,\n        finished: Int,\n        downloaded: Int,\n        total: Int,\n    ) {\n        notifyPageFailed(index, error)\n    }\n\n    override fun onFinish(finished: Int, downloaded: Int, total: Int) {}\n    override fun onGetImageSuccess(index: Int, image: Image?) {\n        notifyPageSucceed(index, image!!)\n    }\n\n    override fun onGetImageFailure(index: Int, error: String?) {\n        notifyPageFailed(index, error)\n    }\n\n    override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {\n        mSpiderQueen.preloadPages(pages, pair)\n    }\n\n    private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable {\n        override fun run() {\n            mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) }\n            mSpiderQueen = null\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/gallery/GalleryProvider2.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.gallery\n\nimport com.hippo.glgallery.GalleryProvider\nimport com.hippo.unifile.UniFile\n\nabstract class GalleryProvider2 : GalleryProvider() {\n    open val startPage: Int\n        get() = 0\n\n    open fun putStartPage(page: Int) {}\n\n    /**\n     * @return without extension\n     */\n    abstract fun getImageFilename(index: Int): String\n\n    /**\n     * @return with extension\n     */\n    abstract fun getImageFilenameWithExtension(index: Int): String\n    abstract fun save(index: Int, file: UniFile): Boolean\n\n    /**\n     * @param filename without extension\n     */\n    abstract fun save(index: Int, dir: UniFile, filename: String): UniFile?\n    abstract suspend fun downloadOriginal(index: Int, dir: UniFile, filename: String): UniFile?\n\n    companion object {\n        // With dot\n        val SUPPORT_IMAGE_EXTENSIONS = arrayOf(\n            \".jpg\",\n            \".png\",\n            \".gif\",\n            \".webp\",\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/jni/Archive.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n@file:Suppress(\"unused\")\n\npackage com.hippo.ehviewer.jni\n\nimport java.nio.ByteBuffer\n\nexternal fun releaseByteBuffer(buffer: ByteBuffer)\nexternal fun openArchive(fd: Int, size: Long, sortEntries: Boolean): Int\nexternal fun extractToByteBuffer(index: Int): ByteBuffer?\nexternal fun extractToFd(index: Int, fd: Int): Boolean\nexternal fun getFilename(index: Int): String\nexternal fun needPassword(): Boolean\nexternal fun providePassword(str: String): Boolean\nexternal fun closeArchive()\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/jni/GifUtils.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n@file:Suppress(\"unused\")\n\npackage com.hippo.ehviewer.jni\n\nimport java.nio.ByteBuffer\n\nexternal fun isGif(fd: Int): Boolean\nexternal fun rewriteGifSource(buffer: ByteBuffer)\nexternal fun mmap(fd: Int): ByteBuffer?\nexternal fun munmap(buffer: ByteBuffer)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/jni/Hash.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n@file:Suppress(\"unused\")\n\npackage com.hippo.ehviewer.jni\n\nexternal fun sha1(fd: Int): String\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/jni/Image.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n@file:Suppress(\"unused\")\n\npackage com.hippo.ehviewer.jni\n\nimport android.graphics.Bitmap\n\nexternal fun nativeTexImage(\n    bitmap: Bitmap,\n    init: Boolean,\n    offsetX: Int,\n    offsetY: Int,\n    width: Int,\n    height: Int,\n)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/AccountPreference.kt",
    "content": "/*\n * Copyright 2018 Hippo Seven\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.hippo.ehviewer.preference\n\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.util.AttributeSet\nimport android.view.View\nimport android.view.WindowManager\nimport android.widget.Button\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.lifecycleScope\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhEngine\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.ui.MainActivity\nimport com.hippo.ehviewer.ui.SettingsActivity\nimport com.hippo.ehviewer.ui.scene.BaseScene\nimport com.hippo.preference.DialogPreference\nimport com.hippo.util.ReadableTime\nimport com.hippo.util.addTextToClipboard\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport kotlinx.coroutines.delay\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nclass AccountPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : DialogPreference(context, attrs),\n    View.OnClickListener {\n    private val mActivity = context as SettingsActivity\n    private var mCookie: String? = null\n    private var mMessage: String? = context.getString(R.string.settings_eh_account_name_tourist)\n    private lateinit var mDialog: AlertDialog\n    private lateinit var refreshButton: Button\n\n    private fun updateMessage() {\n        if (EhCookieStore.hasSignedIn()) {\n            var ipbMemberId: String? = null\n            var ipbPassHash: String? = null\n            var igneous: String? = null\n            var igneousExpire = 0L\n\n            EhCookieStore.getCookies(EhUrl.HOST_EX.toHttpUrl()).forEach {\n                when (it.name) {\n                    EhCookieStore.KEY_IPB_MEMBER_ID -> ipbMemberId = it.value\n                    EhCookieStore.KEY_IPB_PASS_HASH -> ipbPassHash = it.value\n                    EhCookieStore.KEY_IGNEOUS -> {\n                        igneous = it.value\n                        igneousExpire = it.expiresAt\n                    }\n                }\n            }\n            mCookie = EhCookieStore.KEY_IPB_MEMBER_ID + \": \" + ipbMemberId +\n                \"\\n\" + EhCookieStore.KEY_IPB_PASS_HASH + \": \" + ipbPassHash\n            igneous?.let { mCookie += \"\\n\" + EhCookieStore.KEY_IGNEOUS + \": \" + it }\n            mMessage = context.getString(R.string.settings_eh_account_identity_cookies, mCookie)\n            if (igneousExpire > 0 && igneousExpire != ReadableTime.MAX_VALUE_MILLIS) {\n                mMessage += \"\\n\\n\" + context.getString(R.string.settings_eh_account_igneous_expire) +\n                    ReadableTime.getShortTime(igneousExpire)\n            }\n            mDialog.setMessage(mMessage)\n        }\n    }\n\n    override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {\n        super.onPrepareDialogBuilder(builder)\n        if (EhCookieStore.hasSignedIn()) {\n            builder.setNeutralButton(R.string.settings_eh_account_identity_cookies_copy) { dialog: DialogInterface, which: Int ->\n                mActivity.addTextToClipboard(mCookie, true)\n                this@AccountPreference.onClick(dialog, which)\n            }\n            builder.setNegativeButton(R.string.settings_eh_account_refresh_igneous, null)\n        }\n        builder.setPositiveButton(R.string.settings_eh_account_sign_out) { _: DialogInterface, _: Int ->\n            mActivity.lifecycleScope.launchIO {\n                EhUtils.signOut()\n                withUIContext {\n                    mActivity.showTip(\n                        R.string.settings_eh_account_sign_out_tip,\n                        BaseScene.LENGTH_SHORT,\n                    )\n                }\n                delay(1500)\n                withUIContext {\n                    val intent = Intent(mActivity, MainActivity::class.java)\n                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n                    mActivity.startActivity(intent)\n                }\n            }\n        }\n        builder.setMessage(mMessage)\n    }\n\n    override fun onDialogCreated(dialog: AlertDialog) {\n        super.onDialogCreated(dialog)\n        mDialog = dialog\n        refreshButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE)\n        refreshButton.setOnClickListener(this)\n        mDialog.window!!.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)\n        updateMessage()\n    }\n\n    override fun onClick(v: View) {\n        refreshButton.isEnabled = false\n        mActivity.lifecycleScope.launchIO {\n            EhCookieStore.deleteCookie(\n                EhUrl.HOST_EX.toHttpUrl(),\n                EhCookieStore.KEY_IGNEOUS,\n            )\n            runCatching { EhEngine.getUConfig(EhUrl.URL_UCONFIG_EX) }\n            withUIContext {\n                updateMessage()\n                refreshButton.isEnabled = true\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/CleanRedundancyPreference.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.preference\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.hippo.ehviewer.GetText\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.download.DownloadManager as downloadManager\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.launchUI\nimport com.hippo.util.withUIContext\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\n\nclass CleanRedundancyPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : TaskPreference(context, attrs) {\n    private fun clearFile(file: UniFile): Boolean {\n        var name = file.name ?: return false\n        val index = name.indexOf('-')\n        if (index >= 0) {\n            name = name.substring(0, index)\n        }\n        val gid = name.toLongOrNull() ?: return false\n        if (downloadManager.containDownloadInfo(gid)) {\n            return false\n        }\n        file.delete()\n        return true\n    }\n\n    private fun doRealWork(): Int = Settings.downloadLocation?.listFiles()?.sumOf { clearFile(it).compareTo(false) } ?: 0\n\n    override val jobTitle = JOB_TITLE_CLEAR_REDUNDANCY\n\n    override fun launchJob() {\n        if (singletonJob?.isActive == true) {\n            singletonJob?.invokeOnCompletion {\n                launchUI {\n                    mDialog.dismiss()\n                }\n            }\n        } else {\n            singletonJob = launch {\n                val cnt = doRealWork()\n                withUIContext {\n                    showTip(FINAL_CLEAR_REDUNDANCY_MSG(cnt))\n                    mDialog.dismiss()\n                }\n            }\n        }\n    }\n\n    companion object {\n        private val JOB_TITLE_CLEAR_REDUNDANCY = GetText.getString(R.string.settings_download_clean_redundancy)\n        private val NO_REDUNDANCY = GetText.getString(R.string.settings_download_clean_redundancy_no_redundancy)\n        private val CLEAR_REDUNDANCY_DONE =\n            { cnt: Int -> GetText.getString(R.string.settings_download_clean_redundancy_done, cnt) }\n        private val FINAL_CLEAR_REDUNDANCY_MSG =\n            { cnt: Int -> if (cnt == 0) NO_REDUNDANCY else CLEAR_REDUNDANCY_DONE(cnt) }\n    }\n}\n\nprivate var singletonJob: Job? = null\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/ClearSearchHistoryPreference.kt",
    "content": "/*\n * Copyright 2025 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.preference\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.hippo.ehviewer.EhApplication.Companion.searchDatabase\nimport com.hippo.ehviewer.GetText\nimport com.hippo.ehviewer.R\nimport com.hippo.util.launchUI\nimport kotlinx.coroutines.launch\n\nclass ClearSearchHistoryPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : TaskPreference(context, attrs) {\n    override val jobTitle = JOB_TITLE_CLEAR_SEARCH_HISTORY\n\n    override fun launchJob() {\n        launch {\n            searchDatabase.clearQuery()\n            launchUI {\n                mDialog.dismiss()\n                showTip(SEARCH_HISTORY_CLEARED)\n            }\n        }\n    }\n\n    companion object {\n        private val JOB_TITLE_CLEAR_SEARCH_HISTORY = GetText.getString(R.string.settings_privacy_clear_search_history)\n        private val SEARCH_HISTORY_CLEARED = GetText.getString(R.string.settings_privacy_clear_search_history_cleared)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/ImageLimitsPreference.kt",
    "content": "/*\n * Copyright 2018 Hippo Seven\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.hippo.ehviewer.preference\n\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.Button\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.lifecycleScope\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhEngine\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.parser.HomeParser\nimport com.hippo.ehviewer.ui.SettingsActivity\nimport com.hippo.ehviewer.ui.scene.BaseScene\nimport com.hippo.preference.DialogPreference\nimport com.hippo.util.ReadableTime\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport okhttp3.HttpUrl.Companion.toHttpUrl\n\nclass ImageLimitsPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : DialogPreference(context, attrs),\n    View.OnClickListener {\n    private val mActivity = context as SettingsActivity\n    private val placeholder = context.getString(R.string.please_wait)\n    private val coroutineScope = mActivity.lifecycleScope\n    private lateinit var resetButton: Button\n    private lateinit var mDialog: AlertDialog\n    private lateinit var mLimits: HomeParser.Limits\n    private lateinit var mFunds: HomeParser.Funds\n\n    init {\n        if (EhCookieStore.hasSignedIn()) {\n            coroutineScope.launchIO {\n                getImageLimits { updateSummary() }\n            }\n        }\n    }\n\n    override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {\n        super.onPrepareDialogBuilder(builder)\n        builder.setMessage(placeholder)\n    }\n\n    override fun onDialogCreated(dialog: AlertDialog) {\n        super.onDialogCreated(dialog)\n        mDialog = dialog\n        resetButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)\n        resetButton.setOnClickListener(this)\n        resetButton.isEnabled = false\n        if (this::mLimits.isInitialized) {\n            bind()\n        } else {\n            coroutineScope.launchIO {\n                getImageLimits(true) { bind() }\n            }\n        }\n    }\n\n    private fun formatCurrent(): String {\n        val (current, maximum, _) = mLimits\n        return when (maximum) {\n            HomeParser.IP_NORMAL -> mActivity.getString(R.string.settings_eh_image_limits_summary_ip) +\n                mActivity.getString(R.string.settings_eh_image_limits_summary_ip_ok)\n            HomeParser.IP_RESTRICTED -> mActivity.getString(R.string.settings_eh_image_limits_summary_ip) +\n                mActivity.getString(R.string.settings_eh_image_limits_summary_ip_restricted)\n            else -> mActivity.getString(R.string.settings_eh_image_limits_summary_acc, current, maximum)\n        }\n    }\n\n    private fun updateSummary() {\n        summary = formatCurrent()\n    }\n\n    private suspend fun getImageLimits(showError: Boolean = false, onSuccess: () -> Unit) {\n        runCatching {\n            EhEngine.getImageLimits()\n        }.onFailure {\n            it.printStackTrace()\n            if (showError) {\n                withUIContext {\n                    mDialog.setMessage(it.message)\n                }\n            }\n        }.onSuccess {\n            mLimits = it.limits\n            mFunds = it.funds\n            withUIContext {\n                onSuccess()\n            }\n        }\n    }\n\n    private fun bind() {\n        val (_, maximum, resetCost) = mLimits\n        val (fundsGP, fundsC) = mFunds\n        var quotaExpire = 0L\n        EhCookieStore.getCookies(EhUrl.HOST_E.toHttpUrl()).forEach {\n            if (it.name == EhCookieStore.KEY_QUOTA) {\n                quotaExpire = it.expiresAt\n            }\n        }\n        var message = formatCurrent()\n        if (quotaExpire > 0) {\n            message += \"  (~${ReadableTime.getShortTime(quotaExpire)})\"\n        }\n        message += \"\\n\" + if (maximum < 0) {\n            mActivity.getString(R.string.settings_eh_unlock_cost, resetCost)\n        } else {\n            mActivity.getString(R.string.settings_eh_reset_cost, resetCost)\n        } +\n            \"\\n\" + mActivity.getString(R.string.current_funds, \"$fundsGP+\", fundsC)\n        mDialog.setMessage(message)\n        resetButton.text = if (maximum < 0) {\n            mActivity.getString(R.string.settings_eh_unlock)\n        } else {\n            mActivity.getString(R.string.settings_eh_reset)\n        }\n        resetButton.isEnabled = resetCost != 0\n        updateSummary()\n    }\n\n    override fun onClick(v: View) {\n        resetButton.isEnabled = false\n        mDialog.setMessage(placeholder)\n        coroutineScope.launchIO {\n            runCatching {\n                EhEngine.resetImageLimits(mLimits.maximum < 0)\n            }.onFailure {\n                it.printStackTrace()\n                withUIContext {\n                    mDialog.setMessage(it.message)\n                }\n            }.onSuccess {\n                EhCookieStore.copyCookie(EhUrl.DOMAIN_E, EhUrl.DOMAIN_EX, EhCookieStore.KEY_QUOTA)\n                withUIContext {\n                    mLimits = it\n                    mActivity.showTip(\n                        R.string.settings_eh_reset_limits_succeed,\n                        BaseScene.LENGTH_SHORT,\n                    )\n                    bind()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/ProxyPreference.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.preference\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.text.TextUtils\nimport android.util.AttributeSet\nimport android.view.View\nimport android.widget.EditText\nimport android.widget.Spinner\nimport androidx.appcompat.app.AlertDialog\nimport com.google.android.material.textfield.TextInputLayout\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.EhProxySelector\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.network.InetValidator\nimport com.hippo.preference.DialogPreference\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.ViewUtils\n\nclass ProxyPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : DialogPreference(context, attrs),\n    View.OnClickListener {\n    private var mType: Spinner? = null\n    private var mIpInputLayout: TextInputLayout? = null\n    private var mIp: EditText? = null\n    private var mPortInputLayout: TextInputLayout? = null\n    private var mPort: EditText? = null\n    private val mArray: Array<String> = context.resources.getStringArray(R.array.proxy_types)\n\n    init {\n        dialogLayoutResource = R.layout.preference_dialog_proxy\n        updateSummary(Settings.proxyType, Settings.proxyIp, Settings.proxyPort)\n    }\n\n    private fun getProxyTypeText(type: Int): String = mArray[MathUtils.clamp(type, 0, mArray.size - 1)]\n\n    private fun updateSummary(type: Int, ip: String?, port: Int) {\n        var type1 = type\n        if ((type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS) &&\n            (TextUtils.isEmpty(ip) || !InetValidator.isValidInetPort(port))\n        ) {\n            type1 = EhProxySelector.TYPE_SYSTEM\n        }\n        summary = if (type1 == EhProxySelector.TYPE_HTTP || type1 == EhProxySelector.TYPE_SOCKS) {\n            val context = context\n            context.getString(\n                R.string.settings_advanced_proxy_summary_1,\n                getProxyTypeText(type1),\n                ip,\n                port,\n            )\n        } else {\n            val context = context\n            context.getString(\n                R.string.settings_advanced_proxy_summary_2,\n                getProxyTypeText(type1),\n            )\n        }\n    }\n\n    override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {\n        super.onPrepareDialogBuilder(builder)\n        builder.setPositiveButton(android.R.string.ok, null)\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    override fun onDialogCreated(dialog: AlertDialog) {\n        super.onDialogCreated(dialog)\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(this)\n        mType = ViewUtils.`$$`(dialog, R.id.type) as Spinner\n        mIpInputLayout = ViewUtils.`$$`(dialog, R.id.ip_input_layout) as TextInputLayout\n        mIp = ViewUtils.`$$`(dialog, R.id.ip) as EditText\n        mPortInputLayout = ViewUtils.`$$`(dialog, R.id.port_input_layout) as TextInputLayout\n        mPort = ViewUtils.`$$`(dialog, R.id.port) as EditText\n        val type = Settings.proxyType\n        mType!!.setSelection(\n            MathUtils.clamp(\n                type,\n                0,\n                mArray.size,\n            ),\n        )\n        mIp!!.setText(Settings.proxyIp)\n        val portString: String?\n        val port = Settings.proxyPort\n        portString = if (!InetValidator.isValidInetPort(port)) {\n            null\n        } else {\n            port.toString()\n        }\n        mPort!!.setText(portString)\n    }\n\n    override fun onDialogClosed(positiveResult: Boolean) {\n        super.onDialogClosed(positiveResult)\n        mType = null\n        mIpInputLayout = null\n        mIp = null\n        mPortInputLayout = null\n        mPort = null\n    }\n\n    override fun onClick(v: View) {\n        val dialog = dialog\n        val context: Context = context\n        if (null == dialog || null == mType || null == mIpInputLayout || null == mIp || null == mPortInputLayout || null == mPort) {\n            return\n        }\n        val type = mType!!.selectedItemPosition\n        val ip = mIp!!.text.toString().trim { it <= ' ' }\n        if (ip.isEmpty()) {\n            if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {\n                mIpInputLayout!!.error = context.getString(R.string.text_is_empty)\n                return\n            }\n        }\n        mIpInputLayout!!.error = null\n        val port: Int\n        val portString = mPort!!.text.toString().trim { it <= ' ' }\n        if (portString.isEmpty()) {\n            if (type == EhProxySelector.TYPE_HTTP || type == EhProxySelector.TYPE_SOCKS) {\n                mPortInputLayout!!.error = context.getString(R.string.text_is_empty)\n                return\n            } else {\n                port = -1\n            }\n        } else {\n            port = try {\n                portString.toInt()\n            } catch (_: NumberFormatException) {\n                -1\n            }\n            if (!InetValidator.isValidInetPort(port)) {\n                mPortInputLayout!!.error = context.getString(R.string.proxy_invalid_port)\n                return\n            }\n        }\n        mPortInputLayout!!.error = null\n        Settings.putProxyType(type)\n        Settings.putProxyIp(ip)\n        Settings.putProxyPort(port)\n        updateSummary(type, ip, port)\n        EhApplication.ehProxySelector.updateProxy()\n        dialog.dismiss()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/RestoreDownloadPreference.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.preference\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.hippo.ehviewer.GetText\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhEngine.fillGalleryListByApi\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.data.BaseGalleryInfo\nimport com.hippo.ehviewer.client.parser.GalleryDetailUrlParser\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.ehviewer.spider.SpiderDen.Companion.getGalleryDownloadDir\nimport com.hippo.ehviewer.spider.SpiderInfo\nimport com.hippo.ehviewer.spider.SpiderQueen.Companion.SPIDER_INFO_FILENAME\nimport com.hippo.ehviewer.spider.readFromUniFile\nimport com.hippo.ehviewer.spider.saveToUniFile\nimport com.hippo.unifile.UniFile\nimport com.hippo.unifile.openInputStream\nimport com.hippo.util.launchUI\nimport com.hippo.util.runSuspendCatching\nimport com.hippo.util.withUIContext\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport okio.buffer\nimport okio.source\n\nclass RestoreDownloadPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : TaskPreference(context, attrs) {\n    private var restoreDirCount = 0\n    private val nonSpiderInfoItemList = mutableListOf<Long>()\n\n    @SuppressLint(\"ParcelCreator\")\n    private class RestoreItem(val dirname: String, gid: Long, token: String) : BaseGalleryInfo(gid, token)\n\n    private fun getRestoreItem(dir: UniFile): RestoreItem? {\n        if (!dir.isDirectory) return null\n        return runCatching {\n            val result = dir.findFile(SPIDER_INFO_FILENAME)?.let {\n                readFromUniFile(it)?.run {\n                    GalleryDetailUrlParser.Result(gid, token!!)\n                }\n            } ?: dir.findFile(COMIC_INFO_FILE)?.let { file ->\n                file.openInputStream().source().buffer().use {\n                    GalleryDetailUrlParser.parse(it.readUtf8())\n                }.also {\n                    it?.apply { nonSpiderInfoItemList.add(gid) }\n                }\n            } ?: return null\n            val gid = result.gid\n            val dirname = dir.name!!\n            if (DownloadManager.containDownloadInfo(gid)) {\n                // Restore download dir to avoid re-download\n                val dbDirname = DownloadManager.getDownloadDirname(gid)\n                if (null == dbDirname || dirname != dbDirname) {\n                    DownloadManager.putDownloadDirname(gid, dirname)\n                    restoreDirCount++\n                }\n                return null\n            }\n            RestoreItem(dirname, gid, result.token)\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()\n    }\n\n    private suspend fun doRealWork(): List<RestoreItem>? {\n        val dir = Settings.downloadLocation ?: return null\n        val files = dir.listFiles() ?: return null\n        return runSuspendCatching {\n            files.mapNotNull { getRestoreItem(it) }.also {\n                fillGalleryListByApi(it, EhUrl.referer)\n            }\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()\n    }\n\n    override val jobTitle = JOB_TITLE_RESTORE_DOWNLOAD\n\n    override fun launchJob() {\n        if (singletonJob?.isActive == true) {\n            singletonJob?.invokeOnCompletion {\n                launchUI {\n                    mDialog.dismiss()\n                }\n            }\n        } else {\n            singletonJob = launch {\n                val result = doRealWork()\n                withUIContext {\n                    if (result == null) {\n                        showTip(RESTORE_FAILED)\n                    } else {\n                        if (result.isEmpty()) {\n                            showTip(RESTORE_COUNT_MSG(restoreDirCount))\n                        } else {\n                            var count = 0\n                            result.forEach { item ->\n                                if (null != item.title) {\n                                    val gid = item.gid\n                                    // Put to download\n                                    DownloadManager.addDownload(item, null)\n                                    // Put download dir to DB\n                                    DownloadManager.putDownloadDirname(gid, item.dirname)\n                                    // Create missing .ehviewer file\n                                    if (gid in nonSpiderInfoItemList) {\n                                        getGalleryDownloadDir(gid)?.run {\n                                            createFile(SPIDER_INFO_FILENAME)?.also {\n                                                SpiderInfo(gid, item.token, item.pages).saveToUniFile(it)\n                                            }\n                                        }\n                                    }\n                                    count++\n                                }\n                            }\n                            showTip(RESTORE_COUNT_MSG(count + restoreDirCount))\n                        }\n                    }\n                    mDialog.dismiss()\n                }\n            }\n        }\n    }\n\n    companion object {\n        private val JOB_TITLE_RESTORE_DOWNLOAD = GetText.getString(R.string.settings_download_restore_download_items)\n        private val RESTORE_NOT_FOUND = GetText.getString(R.string.settings_download_restore_not_found)\n        private val RESTORE_FAILED = GetText.getString(R.string.settings_download_restore_failed)\n        private val RESTORE_COUNT_MSG =\n            { cnt: Int -> if (cnt == 0) RESTORE_NOT_FOUND else GetText.getString(R.string.settings_download_restore_successfully, cnt) }\n    }\n}\n\nprivate const val COMIC_INFO_FILE = \"ComicInfo.xml\"\nprivate var singletonJob: Job? = null\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/TaskPreference.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.preference\n\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.util.AttributeSet\nimport androidx.appcompat.app.AlertDialog\nimport com.google.android.material.snackbar.Snackbar\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.ui.SettingsActivity\nimport com.hippo.preference.DialogPreference\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\n\nabstract class TaskPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : DialogPreference(context, attrs),\n    CoroutineScope {\n    lateinit var mDialog: AlertDialog\n\n    override val coroutineContext = Dispatchers.IO + Job()\n\n    override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {\n        builder.setTitle(jobTitle)\n            .setMessage(R.string.settings_download_task_confirm)\n            .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->\n                mDialog = AlertDialog.Builder(context)\n                    .setTitle(null)\n                    .setView(R.layout.preference_dialog_task)\n                    .setCancelable(false)\n                    .show()\n                launchJob()\n            }\n            .setNegativeButton(android.R.string.cancel, null)\n    }\n\n    abstract val jobTitle: String?\n\n    abstract fun launchJob()\n\n    protected fun showTip(msg: String) = (context as SettingsActivity).showTip(msg, Snackbar.LENGTH_SHORT)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/UserAgentPreference.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.preference\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.util.AttributeSet\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.Button\nimport android.widget.EditText\nimport android.widget.TextView\nimport android.widget.TextView.OnEditorActionListener\nimport androidx.appcompat.app.AlertDialog\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.okhttp.CHROME_USER_AGENT\nimport com.hippo.preference.DialogPreference\n\n@SuppressLint(\"InflateParams\")\nclass UserAgentPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : DialogPreference(context, attrs),\n    View.OnClickListener,\n    OnEditorActionListener {\n    private lateinit var mDialog: AlertDialog\n    private lateinit var mButton: Button\n    private var mUserAgent: String? = Settings.userAgent\n    private val view: View =\n        LayoutInflater.from(context).inflate(R.layout.dialog_edittext_builder, null)\n    private val editText: EditText = view.findViewById(R.id.edit_text)\n\n    init {\n        updateSummary()\n    }\n\n    private fun updateSummary() {\n        summary = mUserAgent\n    }\n\n    override fun onCreateDialogView(): View = view\n\n    override fun onDialogCreated(dialog: AlertDialog) {\n        super.onDialogCreated(dialog)\n        mDialog = dialog\n        mButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)\n        mButton.setOnClickListener(this)\n        editText.isSingleLine = false\n        editText.setText(mUserAgent)\n        editText.setSelection(editText.text.length)\n        editText.setOnEditorActionListener(this)\n    }\n\n    override fun onClick(v: View) {\n        mUserAgent = editText.text.toString().trim().ifBlank { null } ?: CHROME_USER_AGENT\n        Settings.putUserAgent(mUserAgent)\n        updateSummary()\n        mDialog.dismiss()\n    }\n\n    override fun onEditorAction(v: TextView?, p1: Int, event: KeyEvent?): Boolean {\n        mButton.performClick()\n        return true\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/preference/VersionPreference.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.preference\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.BuildConfig\nimport com.hippo.ehviewer.R\n\nclass VersionPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : Preference(context, attrs) {\n    init {\n        setTitle(R.string.settings_about_version)\n        summary = \"${BuildConfig.VERSION_NAME} (${BuildConfig.COMMIT_SHA})\\n\" +\n            context.getString(R.string.settings_about_build_time, BuildConfig.BUILD_TIME)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/shortcuts/ShortcutsActivity.kt",
    "content": "/*\n * Copyright 2018 Hippo Seven\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.hippo.ehviewer.shortcuts\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport com.hippo.ehviewer.download.DownloadService\n\n/**\n * Created by onlymash on 3/25/18.\n */\nclass ShortcutsActivity : AppCompatActivity() {\n    public override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val action: String? = intent?.action\n        if (action != null && (action == DownloadService.ACTION_START_ALL || action == DownloadService.ACTION_STOP_ALL)) {\n            ContextCompat.startForegroundService(\n                this,\n                Intent(this, DownloadService::class.java).setAction(action),\n            )\n        }\n        finish()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/spider/DownloadInfoMagics.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.spider\n\nimport com.hippo.ehviewer.client.thumbUrl\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.download.DownloadManager\n\nobject DownloadInfoMagics {\n    private const val DOWNLOAD_INFO_DIRNAME_URL_MAGIC = \"$\"\n    private const val DOWNLOAD_INFO_DIRNAME_URL_SEPARATOR = \"|\"\n\n    fun encodeMagicRequest(info: DownloadInfo): String {\n        val url = info.thumbUrl!!\n        val location = DownloadManager.getDownloadDirname(info.gid)\n        return if (location.isNullOrBlank()) {\n            url\n        } else {\n            DOWNLOAD_INFO_DIRNAME_URL_MAGIC + url + DOWNLOAD_INFO_DIRNAME_URL_SEPARATOR + location\n        }\n    }\n\n    fun decodeMagicRequestOrUrl(encoded: String): Pair<String, String?> = if (encoded.startsWith(DOWNLOAD_INFO_DIRNAME_URL_MAGIC)) {\n        val (a, b) = encoded.removePrefix(DOWNLOAD_INFO_DIRNAME_URL_MAGIC).split(DOWNLOAD_INFO_DIRNAME_URL_SEPARATOR)\n        a to b\n    } else {\n        encoded to null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.spider\n\nimport com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache\nimport com.hippo.ehviewer.EhApplication.Companion.nonCacheOkHttpClient\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhRequestBuilder\nimport com.hippo.ehviewer.client.EhUtils.getSuitableTitle\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.getImageKey\nimport com.hippo.ehviewer.coil.edit\nimport com.hippo.ehviewer.coil.read\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.ehviewer.gallery.GalleryProvider2.Companion.SUPPORT_IMAGE_EXTENSIONS\nimport com.hippo.image.UniFileSource\nimport com.hippo.unifile.UniFile\nimport com.hippo.unifile.openOutputStream\nimport com.hippo.unifile.sha1\nimport com.hippo.util.runInterruptibleOkio\nimport com.hippo.util.runSuspendCatching\nimport com.hippo.util.sendTo\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.cleanAsDirname\nimport java.io.IOException\nimport java.util.Locale\nimport okhttp3.Response\nimport okhttp3.coroutines.executeAsync\nimport okio.buffer\nimport okio.sink\n\nclass SpiderDen(private val mGalleryInfo: GalleryInfo) {\n    private val fileHashRegex = Regex(\"/h/([0-9a-f]{40})\")\n    private val mGid = mGalleryInfo.gid\n    var downloadDir: UniFile? = null\n\n    @Volatile\n    @SpiderQueen.Mode\n    var mode = SpiderQueen.MODE_READ\n        set(value) {\n            field = value\n            if (field == SpiderQueen.MODE_DOWNLOAD && downloadDir == null) {\n                val title = getSuitableTitle(mGalleryInfo)\n                val dirname = FileUtils.sanitizeFilename(\"$mGid-$title\")\n                downloadDir = perSafeDownloadDir(mGid, dirname)\n            }\n        }\n\n    private fun containInCache(index: Int): Boolean {\n        val key = getImageKey(mGid, index)\n        return sCache.openSnapshot(key)?.use { true } == true\n    }\n\n    private fun containInDownloadDir(index: Int): Boolean {\n        val dir = downloadDir ?: return false\n        return findImageFile(dir, index) != null\n    }\n\n    private fun copyFromCacheToDownloadDir(index: Int, skip: Boolean): Boolean {\n        val dir = downloadDir ?: return false\n        // Find image file in cache\n        val key = getImageKey(mGid, index)\n        return runCatching {\n            sCache.read(key) {\n                // Get extension\n                val extension = fixExtension(\".\" + metadata.toFile().readText())\n                // Don't copy from cache if `download original image` enabled, ignore gif\n                if (skip && extension != GIF_IMAGE_EXTENSION) {\n                    return false\n                }\n                // Copy from cache to download dir\n                val file = dir.createFile(perFilename(index, extension)) ?: return false\n                UniFile.fromFile(data.toFile())!! sendTo file\n            }\n        }.getOrElse {\n            it.printStackTrace()\n            false\n        }\n    }\n\n    fun copyFromUniFileToDownloadDir(oldDir: UniFile, oldIndex: Int, index: Int): Boolean {\n        val dir = downloadDir ?: return false\n        return runCatching {\n            val oldFile = findImageFile(oldDir, oldIndex) ?: return false\n            val extension = oldFile.name.let { name -> FileUtils.getExtensionFromFilename(name) }\n            val file = dir.createFile(perFilename(index, \".$extension\")) ?: return false\n            oldFile sendTo file\n            true\n        }.getOrElse {\n            it.printStackTrace()\n            false\n        }\n    }\n\n    operator fun contains(index: Int): Boolean = when (mode) {\n        SpiderQueen.MODE_READ -> {\n            containInCache(index) || containInDownloadDir(index)\n        }\n        SpiderQueen.MODE_DOWNLOAD -> {\n            containInDownloadDir(index) || copyFromCacheToDownloadDir(index, Settings.skipCopyImage)\n        }\n        else -> {\n            false\n        }\n    }\n\n    private fun removeFromCache(index: Int): Boolean {\n        val key = getImageKey(mGid, index)\n        return sCache.remove(key)\n    }\n\n    private fun removeFromDownloadDir(index: Int): Boolean = downloadDir?.let { findImageFile(it, index)?.delete() } == true\n\n    fun remove(index: Int): Boolean = removeFromCache(index) or removeFromDownloadDir(index)\n\n    private fun findDownloadFileForIndex(index: Int, extension: String): UniFile? {\n        val dir = downloadDir ?: return null\n        val ext = fixExtension(\".$extension\")\n        return dir.createFile(perFilename(index, ext))\n    }\n\n    @Throws(IOException::class)\n    suspend fun saveImageFromUrl(\n        url: String,\n        referer: String?,\n        dir: UniFile,\n        filename: String,\n    ): UniFile? {\n        nonCacheOkHttpClient.newCall(EhRequestBuilder(url, referer).build()).executeAsync().use {\n            if (it.code >= 400) return null\n            val ext = it.body.contentType()?.subtype ?: \"jpg\"\n            val dst = dir.subFile(\"$filename.$ext\") ?: return null\n            return runSuspendCatching {\n                var ret = 0L\n                runInterruptibleOkio {\n                    dst.openOutputStream().sink().buffer().use { sink ->\n                        it.body.source().use { source ->\n                            while (true) {\n                                val bytesRead = source.read(sink.buffer, 8192)\n                                if (bytesRead == -1L) break\n                                ret += bytesRead\n                                sink.emitCompleteSegments()\n                            }\n                        }\n                    }\n                }\n                if (ret == it.body.contentLength()) dst else null\n            }.onFailure { e ->\n                e.printStackTrace()\n            }.getOrNull()\n        }\n    }\n\n    @Throws(IOException::class)\n    suspend fun makeHttpCallAndSaveImage(\n        index: Int,\n        url: String,\n        referer: String?,\n        notifyProgress: (Long, Long, Int) -> Unit,\n    ): Boolean {\n        // TODO: Use HttpEngine[https://developer.android.com/reference/android/net/http/HttpEngine] directly here if available\n        // Since we don't want unnecessary copy between jvm heap & native heap\n        nonCacheOkHttpClient.newCall(EhRequestBuilder(url, referer).build()).executeAsync().use {\n            if (it.code >= 400) return false\n            return saveFromHttpResponse(index, it, notifyProgress)\n        }\n    }\n\n    private suspend fun saveFromHttpResponse(index: Int, response: Response, notifyProgress: (Long, Long, Int) -> Unit): Boolean {\n        val url = response.request.url.toString()\n        val extension = response.body.contentType()?.subtype ?: \"jpg\"\n        val length = response.body.contentLength()\n\n        suspend fun doSave(outFile: UniFile): Long {\n            var ret = 0L\n            runInterruptibleOkio {\n                outFile.openOutputStream().sink().buffer().use { sink ->\n                    response.body.source().use { source ->\n                        while (true) {\n                            val bytesRead = source.read(sink.buffer, 8192)\n                            if (bytesRead == -1L) break\n                            ret += bytesRead\n                            sink.emitCompleteSegments()\n                            notifyProgress(length, ret, bytesRead.toInt())\n                        }\n                    }\n                }\n                fileHashRegex.find(url)?.let {\n                    val expected = it.groupValues[1]\n                    val actual = outFile.sha1()\n                    check(expected == actual) { \"File hash mismatch: expected $expected, but got $actual\\nURL: $url\" }\n                }\n            }\n            return ret\n        }\n\n        findDownloadFileForIndex(index, extension)?.runSuspendCatching {\n            return doSave(this) == length\n        }?.onFailure {\n            it.printStackTrace()\n            return false\n        }\n\n        // Read Mode, allow save to cache\n        if (mode == SpiderQueen.MODE_READ) {\n            val key = getImageKey(mGid, index)\n            var received: Long = 0\n            runSuspendCatching {\n                sCache.edit(key) {\n                    metadata.toFile().writeText(extension)\n                    received = doSave(UniFile.fromFile(data.toFile())!!)\n                }\n            }.onFailure {\n                it.printStackTrace()\n            }.onSuccess {\n                return received == length\n            }\n        }\n\n        return false\n    }\n\n    fun saveToUniFile(index: Int, file: UniFile): Boolean {\n        val key = getImageKey(mGid, index)\n\n        // Read from diskCache first\n        sCache.read(key) {\n            runCatching {\n                UniFile.fromFile(data.toFile())!! sendTo file\n                return true\n            }.onFailure {\n                it.printStackTrace()\n                return false\n            }\n        }\n\n        // Read from download dir\n        runCatching {\n            findImageFile(downloadDir!!, index)!! sendTo file\n        }.onFailure {\n            it.printStackTrace()\n            return false\n        }.onSuccess {\n            return true\n        }\n        return false\n    }\n\n    fun getExtension(index: Int): String? {\n        val key = getImageKey(mGid, index)\n        return sCache.openSnapshot(key)?.use { it.metadata.toFile().readText() }\n            ?: downloadDir?.let { findImageFile(it, index) }\n                ?.name.let { FileUtils.getExtensionFromFilename(it) }\n    }\n\n    fun getImageSource(index: Int): UniFileSource? {\n        if (mode == SpiderQueen.MODE_READ) {\n            val key = getImageKey(mGid, index)\n            sCache.openSnapshot(key)?.let {\n                val source = UniFile.fromFile(it.data.toFile())!!\n                return object : UniFileSource, AutoCloseable by it {\n                    override val source = source\n                }\n            }\n        }\n        val dir = downloadDir ?: return null\n        val source = findImageFile(dir, index) ?: return null\n        return object : UniFileSource {\n            override val source = source\n            override fun close() {}\n        }\n    }\n\n    companion object {\n        private val COMPAT_IMAGE_EXTENSIONS = SUPPORT_IMAGE_EXTENSIONS + \".jpeg\"\n        private val GIF_IMAGE_EXTENSION = SUPPORT_IMAGE_EXTENSIONS[2]\n\n        /**\n         * @param extension with dot\n         */\n        private fun fixExtension(extension: String): String = extension.takeIf { SUPPORT_IMAGE_EXTENSIONS.contains(it) }\n            ?: SUPPORT_IMAGE_EXTENSIONS[0]\n\n        fun findImageFile(dir: UniFile, index: Int): UniFile? = COMPAT_IMAGE_EXTENSIONS.firstNotNullOfOrNull { dir.findFile(perFilename(index, it)) }\n\n        /**\n         * @param extension with dot\n         */\n        fun perFilename(index: Int, extension: String?): String = String.format(Locale.US, \"%08d%s\", index + 1, extension)\n\n        fun getGalleryDownloadDir(gid: Long): UniFile? {\n            val dir = Settings.downloadLocation ?: return null\n            val dirname = DownloadManager.getDownloadDirname(gid) ?: return null\n            return dir.subFile(dirname)\n        }\n\n        private fun perDownloadDir(gid: Long, dirname: String): UniFile? {\n            DownloadManager.putDownloadDirname(gid, dirname)\n            return getGalleryDownloadDir(gid)?.takeIf { it.ensureDir() }\n        }\n\n        fun perSafeDownloadDir(gid: Long, dirname: String): UniFile? = perDownloadDir(gid, dirname) ?: perDownloadDir(gid, dirname.cleanAsDirname())\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/spider/SpiderInfo.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.spider\n\nimport coil3.disk.DiskCache\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.coil.edit\nimport com.hippo.unifile.UniFile\nimport com.hippo.unifile.openInputStream\nimport com.hippo.unifile.openOutputStream\nimport com.hippo.util.runSuspendCatching\nimport com.hippo.yorozuya.NumberUtils\nimport java.io.InputStream\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.cbor.Cbor\nimport kotlinx.serialization.decodeFromByteArray\nimport kotlinx.serialization.encodeToByteArray\nimport okio.buffer\nimport okio.source\n\n@Serializable\nclass SpiderInfo(\n    val gid: Long,\n    var token: String? = null,\n    val pages: Int,\n    val pTokenMap: MutableMap<Int, String> = hashMapOf(),\n    var startPage: Int = 0,\n    var previewPages: Int = -1,\n    var previewPerPage: Int = -1,\n    var upgradeFrom: Long? = null,\n)\n\nprivate val cbor = Cbor { ignoreUnknownKeys = true }\n\nprivate val spiderInfoCache by lazy {\n    DiskCache.Builder()\n        .directory(EhApplication.cacheDir / \"spider_info_v2\")\n        .maxSizeBytes(20 * 1024 * 1024)\n        .build()\n}\n\nfun readFromUniFile(file: UniFile): SpiderInfo? = runCatching {\n    file.openInputStream().use { cbor.decodeFromByteArray<SpiderInfo>(it.readBytes()) }\n}.getOrNull() ?: runCatching {\n    file.openInputStream().use { readLegacySpiderInfo(it) }\n}.onFailure {\n    it.printStackTrace()\n}.getOrNull()\n\nfun SpiderInfo.saveToUniFile(file: UniFile) = runSuspendCatching {\n    file.openOutputStream().use { it.write(cbor.encodeToByteArray(this@saveToUniFile)) }\n}.onFailure {\n    it.printStackTrace()\n}\n\nfun readFromCache(gid: Long): SpiderInfo? = runCatching {\n    spiderInfoCache.openSnapshot(gid.toString())?.use {\n        cbor.decodeFromByteArray<SpiderInfo>(it.data.toFile().readBytes())\n    }\n}.onFailure {\n    it.printStackTrace()\n}.getOrNull()\n\nfun SpiderInfo.saveToCache() = runSuspendCatching {\n    spiderInfoCache.edit(gid.toString()) {\n        data.toFile().writeBytes(cbor.encodeToByteArray(this@saveToCache))\n    }\n}.onFailure {\n    it.printStackTrace()\n}\n\nprivate fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo? {\n    val source = inputStream.source().buffer()\n    fun read(): String = source.readUtf8LineStrict()\n    fun readInt(): Int = read().toInt()\n    fun readLong(): Long = read().toLong()\n    fun getVersion(str: String): Int = if (str.startsWith(VERSION_STR)) {\n        NumberUtils.parseIntSafely(str.substring(VERSION_STR.length), -1)\n    } else {\n        1\n    }\n    val version = getVersion(read())\n    var startPage = 0\n    when (version) {\n        VERSION -> {\n            // Read next line\n            startPage = read().toInt(16).coerceAtLeast(0)\n        }\n        1 -> {\n            // pass\n        }\n        else -> {\n            // Invalid version\n            return null\n        }\n    }\n    val gid = readLong()\n    val token = read()\n    read() // Deprecated, mode, skip it\n    val previewPages = readInt()\n    val previewPerPage = if (version == 1) 0 else readInt()\n    val pages = readInt()\n    if (gid == -1L || pages <= 0) return null\n    val pTokenMap = hashMapOf<Int, String>()\n    runCatching {\n        while (true) {\n            val line = read()\n            val pos = line.indexOf(\" \")\n            if (pos > 0) {\n                val index = line.substring(0, pos).toInt()\n                val pToken = line.substring(pos + 1)\n                if (pToken.isNotEmpty()) {\n                    pTokenMap[index] = pToken\n                }\n            }\n        }\n    }\n    return SpiderInfo(gid, token, pages, pTokenMap, startPage, previewPages, previewPerPage)\n}\n\nprivate const val VERSION_STR = \"VERSION\"\nprivate const val VERSION = 2\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\n * Rewrite with Kotlin coroutines, Tarsin Norbin 2023\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.hippo.ehviewer.spider\n\nimport android.util.Log\nimport androidx.annotation.IntDef\nimport androidx.collection.LongSparseArray\nimport androidx.collection.set\nimport com.hippo.ehviewer.EhApplication.Companion.okHttpClient as plainTextOkHttpClient\nimport com.hippo.ehviewer.GetText\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhEngine\nimport com.hippo.ehviewer.client.EhRequestBuilder\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils.isMPVAvailable\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.exception.QuotaExceededException\nimport com.hippo.ehviewer.client.parser.GalleryDetailParser\nimport com.hippo.ehviewer.client.parser.GalleryPageUrlParser\nimport com.hippo.image.Image\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.launchIO\nimport com.hippo.util.runSuspendCatching\nimport java.util.concurrent.atomic.AtomicInteger\nimport kotlin.time.Duration.Companion.milliseconds\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.TimeSource\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.TimeoutCancellationException\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.Semaphore\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.sync.withPermit\nimport kotlinx.coroutines.withTimeout\nimport okhttp3.coroutines.executeAsync\n\nclass SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineScope {\n    override val coroutineContext = Dispatchers.IO + Job()\n\n    @Volatile\n    lateinit var mPageStateArray: IntArray\n    lateinit var mSpiderInfo: SpiderInfo\n\n    val mSpiderDen: SpiderDen = SpiderDen(galleryInfo)\n    private val mPageStateLock = Any()\n    private val mDownloadedPages = AtomicInteger(0)\n    private val mFinishedPages = AtomicInteger(0)\n    private val mSpiderListeners: MutableList<OnSpiderListener> = ArrayList()\n\n    private var mOldDownloadDir: UniFile? = null\n    private var mOldHashMap: MutableMap<String, Int>? = null\n    private var mReadReference = 0\n    private var mDownloadReference = 0\n\n    fun addOnSpiderListener(listener: OnSpiderListener) {\n        synchronized(mSpiderListeners) { mSpiderListeners.add(listener) }\n    }\n\n    fun removeOnSpiderListener(listener: OnSpiderListener) {\n        synchronized(mSpiderListeners) { mSpiderListeners.remove(listener) }\n    }\n\n    private fun notifyGetPages(pages: Int) {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach { it.onGetPages(pages) }\n        }\n    }\n\n    fun notifyGet509(index: Int) {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach { it.onGet509(index) }\n        }\n    }\n\n    fun notifyPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach {\n                it.onPageDownload(\n                    index,\n                    contentLength,\n                    receivedSize,\n                    bytesRead,\n                )\n            }\n        }\n    }\n\n    private fun notifyPageSuccess(index: Int) {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach {\n                it.onPageSuccess(\n                    index,\n                    mFinishedPages.get(),\n                    mDownloadedPages.get(),\n                    mPageStateArray.size,\n                )\n            }\n        }\n    }\n\n    private fun notifyPageFailure(index: Int, error: String?) {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach {\n                it.onPageFailure(\n                    index,\n                    error,\n                    mFinishedPages.get(),\n                    mDownloadedPages.get(),\n                    mPageStateArray.size,\n                )\n            }\n        }\n    }\n\n    private fun notifyAllPageDownloaded() {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach {\n                it.onFinish(\n                    mFinishedPages.get(),\n                    mDownloadedPages.get(),\n                    mPageStateArray.size,\n                )\n            }\n        }\n        mSpiderInfo.upgradeFrom = null\n    }\n\n    fun notifyGetImageSuccess(index: Int, image: Image) {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach {\n                it.onGetImageSuccess(index, image)\n            }\n        }\n    }\n\n    fun notifyGetImageFailure(index: Int, error: String) {\n        synchronized(mSpiderListeners) {\n            mSpiderListeners.forEach {\n                it.onGetImageFailure(index, error)\n            }\n        }\n    }\n\n    private var downloadMode = false\n    val isReady\n        get() = this::mSpiderInfo.isInitialized && this::mPageStateArray.isInitialized\n\n    @Synchronized\n    private fun updateMode() {\n        if (!isReady) return\n        val mode: Int = if (mDownloadReference > 0) {\n            MODE_DOWNLOAD\n        } else {\n            MODE_READ\n        }\n        mSpiderDen.mode = mode\n\n        // Update download page\n        val intoDownloadMode = mode == MODE_DOWNLOAD\n        if (intoDownloadMode && !downloadMode) {\n            // Clear download state\n            synchronized(mPageStateLock) {\n                val temp: IntArray = mPageStateArray\n                var i = 0\n                val n = temp.size\n                while (i < n) {\n                    val oldState = temp[i]\n                    if (STATE_DOWNLOADING != oldState) {\n                        temp[i] = STATE_NONE\n                    }\n                    i++\n                }\n                mDownloadedPages.lazySet(0)\n                mFinishedPages.lazySet(0)\n            }\n            mWorkerScope.enterDownloadMode()\n        }\n        downloadMode = intoDownloadMode\n    }\n\n    private fun resetStates() {\n        synchronized(mPageStateLock) {\n            if (this::mPageStateArray.isInitialized) {\n                mPageStateArray.fill(STATE_NONE)\n            }\n            mDownloadedPages.set(0)\n            mFinishedPages.set(0)\n        }\n        mWorkerScope.clearRAList()\n    }\n\n    private fun setMode(@Mode mode: Int) {\n        when (mode) {\n            MODE_READ -> mReadReference++\n            MODE_DOWNLOAD -> mDownloadReference++\n        }\n        check(mDownloadReference <= 1) { \"mDownloadReference can't more than 1\" }\n    }\n\n    private fun clearMode(@Mode mode: Int) {\n        when (mode) {\n            MODE_READ -> mReadReference--\n            MODE_DOWNLOAD -> mDownloadReference--\n        }\n        check(!(mReadReference < 0 || mDownloadReference < 0)) { \"Mode reference < 0\" }\n    }\n\n    private val prepareJob = launchIO { doPrepare() }\n\n    private suspend fun doPrepare() {\n        mSpiderDen.downloadDir = SpiderDen.getGalleryDownloadDir(galleryInfo.gid)?.takeIf { it.isDirectory }\n        mSpiderInfo = readSpiderInfoFromLocal() ?: readSpiderInfoFromInternet() ?: return\n        mPageStateArray = IntArray(mSpiderInfo.pages)\n        prepareUpgrade()\n        notifyGetPages(mSpiderInfo.pages)\n    }\n\n    private suspend fun prepareUpgrade() {\n        mOldDownloadDir = mSpiderInfo.upgradeFrom?.let { gid ->\n            SpiderDen.getGalleryDownloadDir(gid)?.takeIf { it.isDirectory }\n        }\n        mOldDownloadDir?.findFile(SPIDER_INFO_FILENAME)?.let { oldSpiderInfoFile ->\n            val oldSpiderInfo = readFromUniFile(oldSpiderInfoFile)\n            if (oldSpiderInfo != null && oldSpiderInfo.gid == mSpiderInfo.upgradeFrom) {\n                if (oldSpiderInfo.pTokenMap.size != oldSpiderInfo.pages) {\n                    getPTokenFromMultiPageViewer(\n                        oldSpiderInfo.gid,\n                        oldSpiderInfo.token!!,\n                        oldSpiderInfo,\n                    )\n                    if (oldSpiderInfo.pTokenMap.size == oldSpiderInfo.pages) {\n                        oldSpiderInfo.saveToUniFile(oldSpiderInfoFile)\n                    }\n                }\n                mOldHashMap = oldSpiderInfo.pTokenMap.entries.associateBy({ it.value }, { it.key }).toMutableMap()\n            }\n        }\n    }\n\n    suspend fun awaitReady(): Boolean {\n        prepareJob.join()\n        return isReady\n    }\n\n    suspend fun awaitStartPage(): Int {\n        prepareJob.join()\n        if (!isReady) return 0\n        return mSpiderInfo.startPage\n    }\n\n    private fun stop() {\n        val queenScope = this\n        launchIO {\n            queenScope.cancel()\n            runCatching {\n                writeSpiderInfoToLocal()\n            }.onFailure {\n                it.printStackTrace()\n            }\n        }\n    }\n\n    val size\n        get() = mPageStateArray.size\n\n    fun forceRequest(index: Int) {\n        request(index, true)\n    }\n\n    fun request(index: Int) {\n        request(index, false)\n    }\n\n    private fun getPageState(index: Int): Int {\n        synchronized(mPageStateLock) {\n            return if (index >= 0 && index < mPageStateArray.size) {\n                mPageStateArray[index]\n            } else {\n                STATE_NONE\n            }\n        }\n    }\n\n    fun cancelRequest(index: Int) {\n        mWorkerScope.cancelDecode(index)\n    }\n\n    fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {\n        mWorkerScope.updateRAList(pages, pair)\n    }\n\n    private fun request(index: Int, force: Boolean) {\n        // Get page state\n        val state = getPageState(index)\n\n        // Fix state for force\n        if (force && state == STATE_FINISHED || state == STATE_FAILED) {\n            // Update state to none at once\n            updatePageState(index, STATE_NONE)\n        }\n        mWorkerScope.launch(index, force)\n    }\n\n    suspend fun downloadOriginal(index: Int, dir: UniFile, filename: String): UniFile? {\n        val pToken = getPToken(index) ?: return null\n        val pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)\n        val originImageUrl = EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token)\n            .originImageUrl ?: return save(index, dir, filename)\n        return runSuspendCatching {\n            val targetImageUrl = EhEngine.getOriginalImageUrl(originImageUrl, pageUrl)\n            mSpiderDen.saveImageFromUrl(targetImageUrl, pageUrl, dir, filename)\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()\n    }\n\n    fun save(index: Int, file: UniFile): Boolean {\n        val state = getPageState(index)\n        return if (STATE_FINISHED != state) {\n            false\n        } else {\n            mSpiderDen.saveToUniFile(index, file)\n        }\n    }\n\n    fun save(index: Int, dir: UniFile, filename: String): UniFile? {\n        val state = getPageState(index)\n        if (STATE_FINISHED != state) {\n            return null\n        }\n        val ext = mSpiderDen.getExtension(index)\n        val dst = dir.subFile(if (null != ext) \"$filename.$ext\" else filename) ?: return null\n        return if (!mSpiderDen.saveToUniFile(index, dst)) null else dst\n    }\n\n    fun getExtension(index: Int): String? {\n        val state = getPageState(index)\n        return if (STATE_FINISHED != state) {\n            null\n        } else {\n            mSpiderDen.getExtension(index)\n        }\n    }\n\n    val startPage: Int\n        get() = mSpiderInfo.startPage\n\n    fun putStartPage(page: Int) {\n        mSpiderInfo.startPage = page\n    }\n\n    private fun readSpiderInfoFromLocal(): SpiderInfo? = mSpiderDen.downloadDir?.run {\n        findFile(SPIDER_INFO_FILENAME)?.let { file ->\n            readFromUniFile(file)?.takeIf {\n                it.gid == galleryInfo.gid && it.token == galleryInfo.token\n            }\n        }\n    }\n        ?: readFromCache(galleryInfo.gid)?.takeIf { it.gid == galleryInfo.gid && it.token == galleryInfo.token }\n\n    private suspend fun readSpiderInfoFromInternet(): SpiderInfo? {\n        val url = EhUrl.getGalleryDetailUrl(\n            galleryInfo.gid,\n            galleryInfo.token,\n            0,\n            false,\n            GET_FULL_HASH,\n        )\n        val request = EhRequestBuilder(url, EhUrl.referer).build()\n        return runSuspendCatching {\n            plainTextOkHttpClient.newCall(request).executeAsync().use { response ->\n                val body = response.body.string()\n                val pages = GalleryDetailParser.parsePages(body)\n                val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token, pages)\n                readPreviews(body, 0, spiderInfo)\n                spiderInfo\n            }\n        }.onFailure {\n            it.printStackTrace()\n        }.getOrNull()\n    }\n\n    private suspend fun getPTokenFromMultiPageViewer(gid: Long, token: String, spiderInfo: SpiderInfo) {\n        if (!isMPVAvailable) return\n        runSuspendCatching {\n            EhEngine.getPTokenFromMultiPageViewer(\n                gid,\n                token,\n                GET_FULL_HASH,\n            ).forEachIndexed { index, pToken ->\n                spiderInfo.pTokenMap[index] = pToken\n            }\n        }.onFailure {\n            it.printStackTrace()\n        }\n    }\n\n    private suspend fun getPTokenFromMultiPageViewer(index: Int): String? {\n        getPTokenFromMultiPageViewer(galleryInfo.gid, galleryInfo.token!!, mSpiderInfo)\n        return mSpiderInfo.pTokenMap[index]\n    }\n\n    private suspend fun getPTokenFromInternet(index: Int): String? {\n        // Check previewIndex\n        val previewIndex = if (mSpiderInfo.previewPerPage >= 0) {\n            (index / mSpiderInfo.previewPerPage).coerceAtMost(mSpiderInfo.previewPages.takeIf { it > 0 }?.minus(1) ?: Int.MAX_VALUE)\n        } else {\n            0\n        }\n        val url = EhUrl.getGalleryDetailUrl(\n            galleryInfo.gid,\n            galleryInfo.token,\n            previewIndex,\n            false,\n            GET_FULL_HASH,\n        )\n        val request = EhRequestBuilder(url, EhUrl.referer).build()\n        return runSuspendCatching {\n            plainTextOkHttpClient.newCall(request).executeAsync().use { response ->\n                val body = response.body.string()\n                readPreviews(body, previewIndex, mSpiderInfo)\n                mSpiderInfo.pTokenMap[index]\n            }\n        }.getOrElse {\n            it.printStackTrace()\n            null\n        }\n    }\n\n    suspend fun getPToken(index: Int): String? {\n        if (index !in 0 until size) return null\n        return mSpiderInfo.pTokenMap[index]\n            ?: getPTokenFromMultiPageViewer(index)\n            ?: getPTokenFromInternet(index)\n            // Preview size may changed, so try to get pToken twice\n            ?: getPTokenFromInternet(index)\n    }\n\n    @Synchronized\n    private fun writeSpiderInfoToLocal() {\n        if (!isReady) return\n        mSpiderDen.downloadDir?.run { createFile(SPIDER_INFO_FILENAME)?.also { mSpiderInfo.saveToUniFile(it) } }\n        mSpiderInfo.saveToCache()\n    }\n\n    private fun isStateDone(state: Int): Boolean = state == STATE_FINISHED || state == STATE_FAILED\n\n    fun updatePageState(index: Int, @State state: Int, error: String? = null) {\n        var oldState: Int\n        synchronized<Unit>(mPageStateLock) {\n            oldState = mPageStateArray[index]\n            mPageStateArray[index] = state\n            if (!isStateDone(oldState) && isStateDone(state)) {\n                mDownloadedPages.incrementAndGet()\n            } else if (isStateDone(oldState) && !isStateDone(state)) {\n                mDownloadedPages.decrementAndGet()\n            }\n            if (oldState != STATE_FINISHED && state == STATE_FINISHED) {\n                mFinishedPages.incrementAndGet()\n            } else if (oldState == STATE_FINISHED && state != STATE_FINISHED) {\n                mFinishedPages.decrementAndGet()\n            }\n        }\n\n        // Notify listeners\n        if (state == STATE_FAILED) {\n            notifyPageFailure(index, error)\n        } else if (state == STATE_FINISHED) {\n            notifyPageSuccess(index)\n        }\n        if (mDownloadedPages.get() == size) notifyAllPageDownloaded()\n    }\n\n    @IntDef(MODE_READ, MODE_DOWNLOAD)\n    @Retention\n    annotation class Mode\n\n    @IntDef(STATE_NONE, STATE_DOWNLOADING, STATE_FINISHED, STATE_FAILED)\n    @Retention(AnnotationRetention.SOURCE)\n    annotation class State\n    interface OnSpiderListener {\n        fun onGetPages(pages: Int)\n        fun onGet509(index: Int)\n        fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int)\n        fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int)\n        fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int)\n        fun onFinish(finished: Int, downloaded: Int, total: Int)\n        fun onGetImageSuccess(index: Int, image: Image?)\n        fun onGetImageFailure(index: Int, error: String?)\n    }\n\n    private val mWorkerScope = object {\n        private val mFetcherJobMap = hashMapOf<Int, Job>()\n        private val mSemaphore = Semaphore(Settings.downloadThreadCount)\n        private val pTokenLock = Mutex()\n        private var showKey: String? = null\n        private val showKeyLock = Mutex()\n        private val mDownloadDelay = Settings.downloadDelay.milliseconds\n        private val downloadTimeout = Settings.downloadTimeout.seconds\n        private var lastRequestTime = TimeSource.Monotonic.markNow()\n        private var isDownloadMode = false\n\n        fun cancelDecode(index: Int) {\n            decoder.cancel(index)\n        }\n\n        @Synchronized\n        fun enterDownloadMode() {\n            if (isDownloadMode) return\n            updateRAList((0 until size).toList())\n            isDownloadMode = true\n        }\n\n        fun updateRAList(list: List<Int>, cancelBounds: Pair<Int, Int> = 0 to Int.MAX_VALUE) {\n            if (isDownloadMode) return\n            synchronized(mFetcherJobMap) {\n                mFetcherJobMap.forEach { (i, job) ->\n                    if (i < cancelBounds.first || i > cancelBounds.second) {\n                        job.cancel()\n                    }\n                }\n                list.forEach {\n                    if (mFetcherJobMap[it]?.isActive != true) {\n                        doLaunchDownloadJob(it, false)\n                    }\n                }\n            }\n        }\n\n        fun clearRAList() {\n            synchronized(mFetcherJobMap) {\n                mFetcherJobMap.forEach { (_, job) ->\n                    job.cancel()\n                }\n                mFetcherJobMap.clear()\n            }\n        }\n\n        private fun doLaunchDownloadJob(index: Int, force: Boolean) {\n            val state = mPageStateArray[index]\n            if (!force && state == STATE_FINISHED) return\n            val currentJob = mFetcherJobMap[index]\n            val skipHath = force && currentJob?.isActive == true\n            if (force) currentJob?.cancel(CancellationException(FORCE_RETRY))\n            if (currentJob?.isActive != true) {\n                mFetcherJobMap[index] = launch {\n                    runCatching {\n                        mSemaphore.withPermit {\n                            doInJob(index, force, skipHath)\n                        }\n                    }.onFailure {\n                        if (it is CancellationException) {\n                            if (mReadReference > 0) {\n                                Log.d(WORKER_DEBUG_TAG, \"Download image $index cancelled\")\n                                if (it.message != FORCE_RETRY) {\n                                    updatePageState(index, STATE_FAILED, \"Cancelled\")\n                                }\n                            }\n                            throw it\n                        }\n                        updatePageState(index, STATE_FAILED, ExceptionUtils.getReadableString(it))\n                    }\n                }\n            }\n        }\n\n        fun launch(index: Int, force: Boolean = false) {\n            check(index in 0 until size)\n            if (!isDownloadMode) synchronized(mFetcherJobMap) { doLaunchDownloadJob(index, force) }\n            if (force) decoder.cancel(index)\n            decoder.launch(index)\n        }\n\n        private suspend fun doInJob(index: Int, force: Boolean, skipHath: Boolean) {\n            val previousPToken: String?\n            val pToken: String\n            pTokenLock.withLock {\n                if (!force && index in mSpiderDen) {\n                    return updatePageState(index, STATE_FINISHED)\n                }\n                pToken = getPToken(index) ?: return updatePageState(index, STATE_FAILED, PTOKEN_FAILED_MESSAGE)\n                previousPToken = getPToken(index - 1)\n\n                mOldDownloadDir?.let { oldDir ->\n                    (mOldHashMap?.get(pToken) ?: mOldHashMap?.get(pToken.take(10)))?.let { oldIndex ->\n                        if (mSpiderDen.copyFromUniFileToDownloadDir(oldDir, oldIndex, index)) {\n                            return updatePageState(index, STATE_FINISHED)\n                        }\n                    }\n                }\n\n                // The lock for delay should be acquired before anything else to maintain FIFO order\n                delay(mDownloadDelay - lastRequestTime.elapsedNow())\n                lastRequestTime = TimeSource.Monotonic.markNow()\n            }\n            updatePageState(index, STATE_DOWNLOADING)\n\n            var skipHathKey: String? = null\n            var originImageUrl: String? = null\n            var error: String? = null\n            var forceHtml = false\n            runSuspendCatching {\n                repeat(2) { retries ->\n                    var imageUrl: String? = null\n                    var localShowKey: String?\n\n                    showKeyLock.withLock {\n                        localShowKey = showKey\n                        if (localShowKey == null || forceHtml) {\n                            var pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)\n                            // Skipping H@H costs 50 points, only use it as last resort\n                            if (skipHathKey != null) {\n                                pageUrl += if (\"?\" in pageUrl) {\n                                    \"&nl=$skipHathKey\"\n                                } else {\n                                    \"?nl=$skipHathKey\"\n                                }\n                            }\n                            EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token)\n                                .let { result ->\n                                    check509(result.imageUrl)\n                                    imageUrl = result.imageUrl\n                                    skipHathKey = result.skipHathKey\n                                    originImageUrl = result.originImageUrl\n                                    localShowKey = result.showKey\n                                    showKey = result.showKey\n                                }\n                        }\n                    }\n\n                    if (imageUrl == null) {\n                        runSuspendCatching {\n                            EhEngine.getGalleryPageApi(\n                                mSpiderInfo.gid,\n                                index,\n                                pToken,\n                                localShowKey,\n                                previousPToken,\n                            )\n                        }.getOrElse {\n                            forceHtml = true\n                            return@repeat\n                        }.let {\n                            check509(it.imageUrl)\n                            imageUrl = it.imageUrl\n                            skipHathKey = it.skipHathKey\n                            originImageUrl = it.originImageUrl\n                        }\n                    }\n\n                    if (retries == 0 && skipHath) {\n                        forceHtml = true\n                        return@repeat\n                    }\n\n                    val targetImageUrl: String?\n                    val referer: String?\n\n                    if (Settings.getDownloadOriginImage(mSpiderDen.downloadDir != null) && originImageUrl != null) {\n                        if (retries == 1 && skipHathKey != null) {\n                            originImageUrl += if (\"?\" in originImageUrl!!) {\n                                \"&nl=$skipHathKey\"\n                            } else {\n                                \"?nl=$skipHathKey\"\n                            }\n                        }\n                        val pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken)\n                        targetImageUrl = EhEngine.getOriginalImageUrl(originImageUrl!!, pageUrl)\n                        referer = EhUrl.referer\n                    } else {\n                        // Original image url won't change, so only set forceHtml in this case\n                        forceHtml = true\n                        targetImageUrl = imageUrl\n                        referer = null\n                    }\n                    checkNotNull(targetImageUrl)\n\n                    repeat(3) { times ->\n                        runCatching {\n                            Log.d(WORKER_DEBUG_TAG, \"Start download image $index attempt #$times\")\n                            val success = withTimeout(downloadTimeout) {\n                                mSpiderDen.makeHttpCallAndSaveImage(\n                                    index,\n                                    targetImageUrl,\n                                    referer,\n                                ) { contentLength: Long, receivedSize: Long, bytesRead: Int ->\n                                    notifyPageDownload(index, contentLength, receivedSize, bytesRead)\n                                }\n                            }\n\n                            check(success)\n                            Log.d(WORKER_DEBUG_TAG, \"Download image $index succeed\")\n                            updatePageState(index, STATE_FINISHED)\n                            return\n                        }.onFailure {\n                            mSpiderDen.remove(index)\n                            Log.d(WORKER_DEBUG_TAG, \"Download image $index attempt #$times failed\")\n                            error = when (it) {\n                                is TimeoutCancellationException -> ERROR_TIMEOUT\n                                is CancellationException -> throw it\n                                else -> ExceptionUtils.getReadableString(it)\n                            }\n                        }\n                    }\n                }\n            }.onFailure {\n                when (it) {\n                    is QuotaExceededException -> notifyGet509(index)\n                }\n                error = ExceptionUtils.getReadableString(it)\n            }\n            updatePageState(index, STATE_FAILED, error)\n        }\n\n        private val decoder = object {\n            private val mSemaphore = Semaphore(4)\n            private val mDecodeJobMap = hashMapOf<Int, Job>()\n\n            fun cancel(index: Int) {\n                synchronized(mDecodeJobMap) {\n                    mDecodeJobMap.remove(index)?.cancel()\n                }\n            }\n\n            fun launch(index: Int) {\n                synchronized(mDecodeJobMap) {\n                    val currentJob = mDecodeJobMap[index]\n                    if (currentJob?.isActive != true) {\n                        mDecodeJobMap[index] = launch {\n                            doInJob(index)\n                        }\n                    }\n                }\n            }\n\n            private suspend fun doInJob(index: Int) {\n                mFetcherJobMap[index]?.takeIf { it.isActive }?.join()\n                val src = mSpiderDen.getImageSource(index) ?: return\n                val image = mSemaphore.withPermit { Image.decode(src) }\n                runCatching {\n                    currentCoroutineContext().ensureActive()\n                }.onFailure {\n                    image?.recycle()\n                    throw it\n                }\n                if (image == null) {\n                    notifyGetImageFailure(index, DECODE_ERROR)\n                } else {\n                    notifyGetImageSuccess(index, image)\n                }\n            }\n        }\n    }\n\n    companion object {\n        const val MODE_READ = 0\n        const val MODE_DOWNLOAD = 1\n        const val STATE_NONE = 0\n        const val STATE_DOWNLOADING = 1\n        const val STATE_FINISHED = 2\n        const val STATE_FAILED = 3\n        const val SPIDER_INFO_FILENAME = \".ehviewer\"\n        const val GET_FULL_HASH = true\n        private val sQueenMap = LongSparseArray<SpiderQueen>()\n        private val PTOKEN_FAILED_MESSAGE = GetText.getString(R.string.error_get_ptoken_error)\n        private val ERROR_TIMEOUT = GetText.getString(R.string.error_timeout)\n        private val DECODE_ERROR = GetText.getString(R.string.error_decoding_failed)\n        private val URL_509_PATTERN = Regex(\"\\\\.org/.+/509s?\\\\.gif\")\n        private const val FORCE_RETRY = \"Force retry\"\n        private const val WORKER_DEBUG_TAG = \"SpiderQueenWorker\"\n\n        fun reset(gid: Long) {\n            sQueenMap[gid]?.resetStates()\n        }\n\n        private fun check509(url: String) {\n            if (URL_509_PATTERN in url) throw QuotaExceededException()\n        }\n\n        fun obtainSpiderQueen(galleryInfo: GalleryInfo, @Mode mode: Int): SpiderQueen {\n            val gid = galleryInfo.gid\n            return (sQueenMap[gid] ?: SpiderQueen(galleryInfo).also { sQueenMap[gid] = it }).apply {\n                setMode(mode)\n                launchIO { if (awaitReady()) updateMode() }\n            }\n        }\n\n        fun releaseSpiderQueen(queen: SpiderQueen, @Mode mode: Int) {\n            queen.run {\n                clearMode(mode)\n                if (mReadReference == 0 && mDownloadReference == 0) {\n                    stop()\n                    sQueenMap.remove(galleryInfo.gid)\n                } else {\n                    launchIO { if (awaitReady()) updateMode() }\n                }\n            }\n        }\n\n        fun readPreviews(body: String, index: Int, spiderInfo: SpiderInfo) {\n            spiderInfo.previewPages = GalleryDetailParser.parsePreviewPages(body)\n            val previewSet = GalleryDetailParser.parsePreviewSet(body)\n            if (previewSet.size() > 0) {\n                if (index == 0) {\n                    spiderInfo.previewPerPage = previewSet.size()\n                } else {\n                    spiderInfo.previewPerPage = previewSet.getPosition(0) / index\n                }\n            }\n            for (i in 0 until previewSet.size()) {\n                if (GET_FULL_HASH) {\n                    spiderInfo.pTokenMap[previewSet.getPosition(i)] = previewSet.getSha1At(i)\n                } else {\n                    GalleryPageUrlParser.parse(previewSet.getPageUrlAt(i))?.let {\n                        spiderInfo.pTokenMap[it.page] = it.pToken\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/CommonOperations.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui\n\nimport android.app.Activity\nimport android.content.DialogInterface\nimport android.content.Intent\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.content.ContextCompat\nimport com.hippo.app.EditTextCheckBoxDialogBuilder\nimport com.hippo.app.ListCheckBoxDialogBuilder\nimport com.hippo.ehviewer.EhApplication.Companion.application\nimport com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.ehviewer.download.DownloadService\nimport com.hippo.ehviewer.ui.scene.BaseScene\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.isAtLeastT\nimport com.hippo.yorozuya.collect.LongList\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\n\nobject CommonOperations {\n    private fun doAddToFavorites(\n        activity: Activity,\n        galleryInfo: GalleryInfo,\n        slot: Int,\n        note: String,\n        listener: EhClient.Callback<Unit>,\n    ) {\n        val request = EhRequest()\n        request.setMethod(EhClient.METHOD_ADD_FAVORITES)\n        request.setArgs(galleryInfo.gid, galleryInfo.token, slot, note)\n        request.setCallback(listener)\n        request.enqueue(activity)\n    }\n\n    private fun doAddToFavorites(\n        activity: Activity,\n        galleryInfo: GalleryInfo,\n        slot: Int,\n        listener: EhClient.Callback<Unit>,\n        foreEdit: Boolean,\n    ) {\n        when (slot) {\n            -1 -> {\n                EhDB.putLocalFavorites(galleryInfo)\n                listener.onSuccess(Unit)\n            }\n            in 0..9 -> {\n                if (!foreEdit && Settings.neverAddFavNotes) {\n                    doAddToFavorites(activity, galleryInfo, slot, \"\", listener)\n                } else {\n                    val builder = EditTextCheckBoxDialogBuilder(\n                        activity,\n                        null,\n                        activity.getString(R.string.favorite_note),\n                        activity.getString(R.string.favorite_note_never_show),\n                        Settings.neverAddFavNotes,\n                    )\n                    builder.setTitle(R.string.add_favorite_note_dialog_title)\n                    builder.setPositiveButton(android.R.string.ok, null)\n                    val dialog = builder.show()\n                    dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {\n                        val text = builder.text.trim { it <= ' ' }\n                        Settings.putNeverAddFavNotes(builder.isChecked)\n                        dialog.dismiss()\n                        doAddToFavorites(activity, galleryInfo, slot, text, listener)\n                    }\n                    dialog.setOnCancelListener { listener.onCancel() }\n                }\n            }\n            else -> {\n                listener.onFailure(Exception()) // TODO Add text\n            }\n        }\n    }\n\n    fun addToFavorites(\n        activity: Activity,\n        galleryInfo: GalleryInfo,\n        listener: EhClient.Callback<Unit>,\n        foreSelect: Boolean = false,\n    ) {\n        val slot = Settings.defaultFavSlot\n        val localFav = activity.getString(R.string.local_favorites)\n        val items = Settings.favCat.toMutableList().apply { add(0, localFav) }\n        if (!foreSelect && slot in -1..9) {\n            val newFavoriteName = if (slot >= 0) items[slot + 1] else null\n            doAddToFavorites(\n                activity,\n                galleryInfo,\n                slot,\n                DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot),\n                false,\n            )\n        } else {\n            ListCheckBoxDialogBuilder(\n                activity,\n                items,\n                { builder: ListCheckBoxDialogBuilder?, _: AlertDialog?, position: Int ->\n                    val slot1 = position - 1\n                    val newFavoriteName = if (slot1 in 0..9) items[slot1 + 1] else null\n                    doAddToFavorites(\n                        activity,\n                        galleryInfo,\n                        slot1,\n                        DelegateFavoriteCallback(listener, galleryInfo, newFavoriteName, slot1),\n                        foreSelect,\n                    )\n                    if (builder?.isChecked == true) {\n                        Settings.putDefaultFavSlot(slot1)\n                    } else {\n                        Settings.putDefaultFavSlot(Settings.INVALID_DEFAULT_FAV_SLOT)\n                    }\n                },\n                activity.getString(R.string.remember_favorite_collection),\n                slot != Settings.INVALID_DEFAULT_FAV_SLOT,\n            )\n                .setTitle(R.string.add_favorites_dialog_title)\n                .setOnCancelListener { listener.onCancel() }\n                .show()\n        }\n    }\n\n    fun removeFromFavorites(\n        activity: Activity?,\n        galleryInfo: GalleryInfo,\n        listener: EhClient.Callback<Unit>,\n        isLocal: Boolean = false,\n    ) {\n        EhDB.removeLocalFavorites(galleryInfo.gid)\n        if (isLocal) {\n            EhDB.updateHistoryFavSlot(galleryInfo.gid, -2)\n            listener.onSuccess(Unit)\n        } else {\n            val request = EhRequest()\n            request.setMethod(EhClient.METHOD_ADD_FAVORITES)\n            request.setArgs(galleryInfo.gid, galleryInfo.token, -1, \"\")\n            request.setCallback(DelegateFavoriteCallback(listener, galleryInfo, null, -2))\n            request.enqueue(activity!!)\n        }\n    }\n\n    fun startDownload(activity: MainActivity, galleryInfo: GalleryInfo, forceDefault: Boolean) {\n        startDownload(activity, listOf(galleryInfo), forceDefault)\n    }\n\n    fun startDownload(\n        activity: MainActivity,\n        galleryInfos: List<GalleryInfo>,\n        forceDefault: Boolean,\n    ) {\n        if (isAtLeastT) {\n            application.topActivity?.checkAndRequestNotificationPermission()\n        }\n        doStartDownload(activity, galleryInfos, forceDefault)\n    }\n\n    private fun doStartDownload(\n        activity: MainActivity,\n        galleryInfos: List<GalleryInfo>,\n        forceDefault: Boolean,\n    ) {\n        val toStart = LongList()\n        val toAdd: MutableList<GalleryInfo> = ArrayList()\n        for (gi in galleryInfos) {\n            if (DownloadManager.containDownloadInfo(gi.gid)) {\n                toStart.add(gi.gid)\n            } else {\n                toAdd.add(gi)\n            }\n        }\n        if (!toStart.isEmpty()) {\n            val intent = Intent(activity, DownloadService::class.java)\n            intent.action = DownloadService.ACTION_START_RANGE\n            intent.putExtra(DownloadService.KEY_GID_LIST, toStart)\n            ContextCompat.startForegroundService(activity, intent)\n        }\n        if (toAdd.isEmpty()) {\n            activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)\n            return\n        }\n        var justStart = forceDefault\n        var label: String? = null\n        // Get default download label\n        if (!justStart && Settings.hasDefaultDownloadLabel) {\n            label = Settings.defaultDownloadLabel\n            justStart = label == null || DownloadManager.containLabel(label)\n        }\n        // If there is no other label, just use null label\n        if (!justStart && DownloadManager.labelList.isEmpty()) {\n            justStart = true\n            label = null\n        }\n        if (justStart) {\n            // Got default label\n            for (gi in toAdd) {\n                val intent = Intent(activity, DownloadService::class.java)\n                intent.action = DownloadService.ACTION_START\n                intent.putExtra(DownloadService.KEY_LABEL, label)\n                intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)\n                ContextCompat.startForegroundService(activity, intent)\n            }\n            // Notify\n            activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)\n        } else {\n            // Let use chose label\n            val list = DownloadManager.labelList\n            val items = mutableListOf<String>()\n            items.add(activity.getString(R.string.default_download_label_name))\n            items.addAll(list.mapNotNull { it.label })\n            ListCheckBoxDialogBuilder(\n                activity,\n                items,\n                { builder: ListCheckBoxDialogBuilder?, _: AlertDialog?, position: Int ->\n                    var label1: String?\n                    if (position == 0) {\n                        label1 = null\n                    } else {\n                        label1 = items[position]\n                        if (!DownloadManager.containLabel(label1)) {\n                            label1 = null\n                        }\n                    }\n                    // Start download\n                    for (gi in toAdd) {\n                        val intent = Intent(activity, DownloadService::class.java)\n                        intent.action = DownloadService.ACTION_START\n                        intent.putExtra(DownloadService.KEY_LABEL, label1)\n                        intent.putExtra(DownloadService.KEY_GALLERY_INFO, gi)\n                        ContextCompat.startForegroundService(activity, intent)\n                    }\n                    // Save settings\n                    if (builder?.isChecked == true) {\n                        Settings.putHasDefaultDownloadLabel(true)\n                        Settings.putDefaultDownloadLabel(label1)\n                    } else {\n                        Settings.putHasDefaultDownloadLabel(false)\n                    }\n                    // Notify\n                    activity.showTip(R.string.added_to_download_list, BaseScene.LENGTH_SHORT)\n                },\n                activity.getString(R.string.remember_download_label),\n                false,\n            )\n                .setTitle(R.string.download)\n                .show()\n        }\n    }\n\n    private class DelegateFavoriteCallback(\n        private val delegate: EhClient.Callback<Unit>,\n        private val info: GalleryInfo,\n        private val newFavoriteName: String?,\n        private val slot: Int,\n    ) : EhClient.Callback<Unit> {\n        override fun onSuccess(result: Unit) {\n            EhDB.updateHistoryFavSlot(info.gid, slot)\n            info.favoriteName = newFavoriteName\n            info.favoriteSlot = slot\n            delegate.onSuccess(result)\n            favouriteStatusRouter.modifyFavourites(info.gid, slot)\n        }\n\n        override fun onFailure(e: Exception) {\n            delegate.onFailure(e)\n        }\n\n        override fun onCancel() {\n            delegate.onCancel()\n        }\n    }\n}\n\nprivate fun removeNoMediaFile(downloadDir: UniFile) {\n    val noMedia = downloadDir.subFile(\".nomedia\") ?: return\n    noMedia.delete()\n}\n\nprivate fun ensureNoMediaFile(downloadDir: UniFile) {\n    downloadDir.createFile(\".nomedia\") ?: return\n}\n\nprivate val lck = Mutex()\n\nsuspend fun keepNoMediaFileStatus() {\n    lck.withLock {\n        val downloadLocation = Settings.downloadLocation ?: return\n        if (Settings.mediaScan) {\n            removeNoMediaFile(downloadLocation)\n        } else {\n            ensureNoMediaFile(downloadLocation)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/EhActivity.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui\n\nimport android.Manifest\nimport android.content.pm.PackageManager\nimport android.content.res.Configuration\nimport android.content.res.Resources.Theme\nimport android.graphics.Color\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.WindowManager\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.annotation.RequiresApi\nimport androidx.annotation.StyleRes\nimport androidx.appcompat.app.AppCompatActivity\nimport androidx.core.content.ContextCompat\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.util.isAtLeastQ\nimport rikka.core.res.resolveColor\nimport rikka.insets.WindowInsetsHelper\nimport rikka.layoutinflater.view.LayoutInflaterFactory\n\nabstract class EhActivity : AppCompatActivity() {\n    @StyleRes\n    fun getThemeStyleRes(): Int = if (Settings.blackDarkTheme && (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0)) R.style.ThemeOverlay_Black else R.style.ThemeOverlay\n\n    override fun onApplyThemeResource(theme: Theme, resid: Int, first: Boolean) {\n        theme.applyStyle(resid, true)\n        theme.applyStyle(getThemeStyleRes(), true)\n    }\n\n    override fun onNightModeChanged(mode: Int) {\n        theme.applyStyle(getThemeStyleRes(), true)\n    }\n\n    @Suppress(\"DEPRECATION\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        layoutInflater.factory2 =\n            LayoutInflaterFactory(delegate).addOnViewCreatedListener(WindowInsetsHelper.LISTENER)\n        super.onCreate(savedInstanceState)\n        window.statusBarColor = Color.TRANSPARENT\n        window.decorView.post {\n            window.navigationBarColor =\n                theme.resolveColor(android.R.attr.navigationBarColor) and 0x00ffffff or -0x20000000\n            if (isAtLeastQ) {\n                window.isNavigationBarContrastEnforced = false\n            }\n        }\n        (application as EhApplication).registerActivity(this)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        (application as EhApplication).unregisterActivity(this)\n    }\n\n    override fun onResume() {\n        super.onResume()\n        if (Settings.enabledSecurity) {\n            window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)\n        } else {\n            window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)\n        }\n    }\n\n    private val requestPermissionLauncher =\n        registerForActivityResult(ActivityResultContracts.RequestPermission()) { Settings.putNotificationRequired() }\n\n    @RequiresApi(Build.VERSION_CODES.TIRAMISU)\n    fun checkAndRequestNotificationPermission() {\n        if (Settings.notificationRequired || ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) return\n        requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/GalleryActivity.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui\n\nimport android.Manifest\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.animation.ValueAnimator.AnimatorUpdateListener\nimport android.annotation.SuppressLint\nimport android.app.assist.AssistContent\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.ContentValues\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.content.pm.ActivityInfo\nimport android.content.pm.PackageManager\nimport android.graphics.Typeface\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.Environment\nimport android.os.ParcelFileDescriptor\nimport android.os.ParcelFileDescriptor.MODE_READ_ONLY\nimport android.provider.MediaStore\nimport android.text.TextUtils\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.WindowManager\nimport android.webkit.MimeTypeMap\nimport android.widget.CompoundButton\nimport android.widget.FrameLayout\nimport android.widget.ImageView\nimport android.widget.SeekBar\nimport android.widget.SeekBar.OnSeekBarChangeListener\nimport android.widget.Spinner\nimport android.widget.Switch\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.activity.result.contract.ActivityResultContracts.CreateDocument\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.FileProvider\nimport androidx.core.net.toUri\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.core.view.WindowInsetsControllerCompat\nimport androidx.core.view.isVisible\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.lifecycleScope\nimport androidx.lifecycle.repeatOnLifecycle\nimport com.hippo.app.EditTextDialogBuilder\nimport com.hippo.ehviewer.AppConfig\nimport com.hippo.ehviewer.BuildConfig\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.gallery.ArchiveGalleryProvider\nimport com.hippo.ehviewer.gallery.EhGalleryProvider\nimport com.hippo.ehviewer.gallery.GalleryProvider2\nimport com.hippo.ehviewer.widget.GalleryGuideView\nimport com.hippo.ehviewer.widget.GalleryHeader\nimport com.hippo.ehviewer.widget.ReversibleSeekBar\nimport com.hippo.glgallery.GalleryProvider\nimport com.hippo.glgallery.GalleryView\nimport com.hippo.glgallery.SimpleAdapter\nimport com.hippo.glview.view.GLRootView\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.util.getParcelableExtraCompat\nimport com.hippo.util.isAtLeastP\nimport com.hippo.util.isAtLeastQ\nimport com.hippo.util.launchIO\nimport com.hippo.util.sendTo\nimport com.hippo.util.withUIContext\nimport com.hippo.widget.ColorView\nimport com.hippo.yorozuya.AnimationUtils\nimport com.hippo.yorozuya.ConcurrentPool\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.ResourcesUtils\nimport com.hippo.yorozuya.SimpleAnimatorListener\nimport com.hippo.yorozuya.SimpleHandler\nimport com.hippo.yorozuya.ViewUtils\nimport java.io.File\nimport java.io.IOException\nimport java.util.concurrent.atomic.AtomicReference\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport rikka.core.res.isNight\nimport rikka.core.res.resolveColor\n\nclass GalleryActivity :\n    EhActivity(),\n    OnSeekBarChangeListener,\n    GalleryView.Listener {\n    private val requestStoragePermissionLauncher = registerForActivityResult(\n        ActivityResultContracts.RequestPermission(),\n    ) { result ->\n        if (result && mSavingPage != -1) {\n            saveImage(mSavingPage)\n        } else {\n            Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n        }\n        mSavingPage = -1\n    }\n    private val saveImageToLauncher = registerForActivityResult(\n        CreateDocument(\"todo/todo\"),\n    ) { uri ->\n        if (uri != null) {\n            val filepath = AppConfig.getExternalTempDir().toString() + File.separator + mCacheFileName\n            val cacheFile = File(filepath)\n            lifecycleScope.launchIO {\n                try {\n                    ParcelFileDescriptor.open(cacheFile, MODE_READ_ONLY).use { from ->\n                        contentResolver.openFileDescriptor(uri, \"w\")!!.use {\n                            from sendTo it\n                        }\n                    }\n                } catch (e: IOException) {\n                    e.printStackTrace()\n                } finally {\n                    runOnUiThread {\n                        Toast.makeText(\n                            this@GalleryActivity,\n                            getString(R.string.image_saved, uri.path),\n                            Toast.LENGTH_SHORT,\n                        ).show()\n                    }\n                }\n                cacheFile.delete()\n            }\n        }\n    }\n    private val mHideSliderRunnable = Runnable {\n        mSeekBarPanel?.let { hideSlider(it) }\n    }\n    private val mHideSliderListener: SimpleAnimatorListener = object : SimpleAnimatorListener() {\n        override fun onAnimationEnd(animation: Animator) {\n            mSeekBarPanelAnimator = null\n            mSeekBarPanel?.visibility = View.INVISIBLE\n        }\n    }\n    private val mUpdateSliderListener = AnimatorUpdateListener {\n        mSeekBarPanel?.requestLayout()\n    }\n    private val mShowSliderListener: SimpleAnimatorListener = object : SimpleAnimatorListener() {\n        override fun onAnimationEnd(animation: Animator) {\n            mSeekBarPanelAnimator = null\n        }\n    }\n    private val mNotifyTaskPool = ConcurrentPool<NotifyTask>(3)\n    private var mAction: String? = null\n    private var mFilename: String? = null\n    private var mUri: Uri? = null\n    private var mGalleryInfo: GalleryInfo? = null\n    private var mPage = 0\n    private var mCacheFileName: String? = null\n    private var mGLRootView: GLRootView? = null\n    private var mGalleryView: GalleryView? = null\n    private var mGalleryProvider: GalleryProvider2? = null\n    private var mGalleryAdapter: GalleryAdapter? = null\n    private var insetsController: WindowInsetsControllerCompat? = null\n    private var mMaskView: ColorView? = null\n    private var mClock: View? = null\n    private var mProgress: TextView? = null\n    private var mBattery: View? = null\n    private var mSeekBarPanel: View? = null\n    private var mGLLoading: View? = null\n    private var mLeftText: TextView? = null\n    private var mRightText: TextView? = null\n    private var mSeekBar: ReversibleSeekBar? = null\n    private var mAutoTransfer: ImageView? = null\n    private var mSeekBarPanelAnimator: ObjectAnimator? = null\n    private var mLayoutMode = 0\n    private var mSize = 0\n    private var mCurrentIndex = 0\n    private var mSavingPage = -1\n    private lateinit var builder: EditTextDialogBuilder\n    private lateinit var dialog: AlertDialog\n    private var dialogShown = false\n    private var mAutoTransferJob: Job? = null\n    private var mTurnPageIntervalVal = Settings.turnPageInterval\n\n    private val galleryDetailUrl: String?\n        get() {\n            val gid: Long\n            val token: String\n            if (mGalleryInfo != null) {\n                gid = mGalleryInfo!!.gid\n                token = mGalleryInfo!!.token!!\n            } else {\n                return null\n            }\n            return EhUrl.getGalleryDetailUrl(gid, token, 0, false)\n        }\n\n    private fun buildProvider(replace: Boolean = false) {\n        if (mGalleryProvider != null) {\n            if (replace) mGalleryProvider!!.stop() else return\n        }\n        if (ACTION_EH == mAction) {\n            mGalleryInfo?.let { mGalleryProvider = EhGalleryProvider(it) }\n        } else if (Intent.ACTION_VIEW == mAction) {\n            if (mUri != null) {\n                try {\n                    grantUriPermission(\n                        BuildConfig.APPLICATION_ID,\n                        mUri,\n                        Intent.FLAG_GRANT_READ_URI_PERMISSION,\n                    )\n                } catch (_: Exception) {\n                    Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show()\n                }\n                val continuation: AtomicReference<Continuation<String>?> = AtomicReference(null)\n                mGalleryProvider = ArchiveGalleryProvider(\n                    this,\n                    mUri!!,\n                    flow {\n                        if (!dialogShown) {\n                            withUIContext {\n                                dialogShown = true\n                                dialog.run {\n                                    show()\n                                    getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {\n                                        val passwd = builder.text\n                                        if (passwd.isEmpty()) {\n                                            builder.setError(getString(R.string.passwd_cannot_be_empty))\n                                        } else {\n                                            continuation.getAndSet(null)?.resume(passwd)\n                                        }\n                                    }\n                                    setOnCancelListener {\n                                        finish()\n                                    }\n                                }\n                            }\n                        }\n                        while (true) {\n                            currentCoroutineContext().ensureActive()\n                            val r = suspendCancellableCoroutine {\n                                continuation.set(it)\n                                it.invokeOnCancellation { dialog.dismiss() }\n                            }\n                            emit(r)\n                            withUIContext {\n                                builder.setError(getString(R.string.passwd_wrong))\n                            }\n                        }\n                    },\n                )\n            }\n        }\n    }\n\n    private fun handleIntent(intent: Intent?) {\n        intent ?: return\n        mAction = intent.action\n        mFilename = intent.getStringExtra(KEY_FILENAME)\n        mUri = intent.data\n        mGalleryInfo = intent.getParcelableExtraCompat(KEY_GALLERY_INFO)\n        mPage = intent.getIntExtra(KEY_PAGE, -1)\n    }\n\n    private fun onInit() {\n        handleIntent(intent)\n        buildProvider()\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        handleIntent(intent)\n        buildProvider(true)\n        mGalleryProvider?.let {\n            lifecycleScope.launchIO {\n                it.start()\n                if (it.awaitReady()) {\n                    withUIContext {\n                        mCurrentIndex = 0\n                        setGallery()\n                    }\n                }\n            }\n        }\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mAction = savedInstanceState.getString(KEY_ACTION)\n        mFilename = savedInstanceState.getString(KEY_FILENAME)\n        mUri = savedInstanceState.getParcelableCompat(KEY_URI)\n        mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)\n        mPage = savedInstanceState.getInt(KEY_PAGE, -1)\n        mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX)\n        buildProvider()\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        outState.putString(KEY_ACTION, mAction)\n        outState.putString(KEY_FILENAME, mFilename)\n        outState.putParcelable(KEY_URI, mUri)\n        if (mGalleryInfo != null) {\n            outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo)\n        }\n        outState.putInt(KEY_PAGE, mPage)\n        outState.putInt(KEY_CURRENT_INDEX, mCurrentIndex)\n    }\n\n    override fun attachBaseContext(newBase: Context) {\n        delegate.localNightMode = when (Settings.readTheme) {\n            1 -> AppCompatDelegate.MODE_NIGHT_YES\n            2 -> AppCompatDelegate.MODE_NIGHT_NO\n            else -> Settings.theme\n        }\n        super.attachBaseContext(newBase)\n    }\n\n    @Suppress(\"DEPRECATION\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        if (Settings.readingFullscreen) {\n            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)\n        }\n        super.onCreate(savedInstanceState)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n        builder = EditTextDialogBuilder(this, null, getString(R.string.archive_passwd))\n        builder.setTitle(getString(R.string.archive_need_passwd))\n        builder.setPositiveButton(getString(android.R.string.ok), null)\n        dialog = builder.create()\n        dialog.setCanceledOnTouchOutside(false)\n\n        mGalleryProvider.let {\n            if (it == null) {\n                finish()\n                return\n            }\n            initializeGallery()\n            lifecycleScope.launchIO {\n                it.start()\n                if (it.awaitReady()) withUIContext { setGallery() }\n            }\n        }\n    }\n\n    private fun initializeGallery() {\n        setContentView(R.layout.activity_gallery)\n        mGLRootView = ViewUtils.`$$`(this, R.id.gl_root_view) as GLRootView\n        mMaskView = ViewUtils.`$$`(this, R.id.mask) as ColorView\n        mClock = ViewUtils.`$$`(this, R.id.clock)\n        mProgress = ViewUtils.`$$`(this, R.id.progress) as TextView\n        mBattery = ViewUtils.`$$`(this, R.id.battery)\n        mSeekBarPanel = ViewUtils.`$$`(this, R.id.seek_bar_panel)\n        mGLLoading = ViewUtils.`$$`(this, R.id.gl_loading)\n        mLeftText = ViewUtils.`$$`(mSeekBarPanel, R.id.left) as TextView\n        mRightText = ViewUtils.`$$`(mSeekBarPanel, R.id.right) as TextView\n        mSeekBar = ViewUtils.`$$`(mSeekBarPanel, R.id.seek_bar) as ReversibleSeekBar\n        mAutoTransfer = ViewUtils.`$$`(mSeekBarPanel, R.id.auto_transfer) as ImageView\n        mClock!!.visibility = if (Settings.showClock) View.VISIBLE else View.GONE\n        mProgress!!.visibility = if (Settings.showProgress) View.VISIBLE else View.GONE\n        mBattery!!.visibility = if (Settings.showBattery) View.VISIBLE else View.GONE\n        mMaskView!!.setOnGenericMotionListener { _: View?, event: MotionEvent ->\n            if (mGalleryView == null) {\n                return@setOnGenericMotionListener false\n            }\n            if (event.action == MotionEvent.ACTION_SCROLL) {\n                val scroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) * 300\n                val isNext = scroll < 0.0f\n\n                when (mLayoutMode) {\n                    GalleryView.LAYOUT_RIGHT_TO_LEFT -> {\n                        mGalleryView?.run { if (isNext) pageLeft() else pageRight() }\n                    }\n                    GalleryView.LAYOUT_LEFT_TO_RIGHT -> {\n                        mGalleryView?.run { if (isNext) pageRight() else pageLeft() }\n                    }\n                    GalleryView.LAYOUT_TOP_TO_BOTTOM -> {\n                        mGalleryView?.onScroll(0f, -scroll, 0f, -scroll, 0f, -scroll)\n                    }\n                }\n            }\n            false\n        }\n        mSeekBar!!.setOnSeekBarChangeListener(this)\n        mAutoTransfer!!.setOnClickListener { autoTransfer() }\n\n        WindowCompat.setDecorFitsSystemWindows(window, false)\n        insetsController = WindowCompat.getInsetsController(window, window.decorView)\n        if (Settings.readingFullscreen) {\n            insetsController!!.systemBarsBehavior =\n                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE\n            insetsController!!.hide(WindowInsetsCompat.Type.systemBars())\n        } else {\n            insetsController!!.systemBarsBehavior =\n                WindowInsetsControllerCompat.BEHAVIOR_DEFAULT\n            insetsController!!.show(WindowInsetsCompat.Type.systemBars())\n        }\n        val night = resources.configuration.isNight()\n        insetsController!!.isAppearanceLightStatusBars = !night\n\n        // Cutout\n        if (isAtLeastP) {\n            window.attributes.layoutInDisplayCutoutMode =\n                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES\n        }\n        val galleryHeader = findViewById<GalleryHeader>(R.id.gallery_header)\n        ViewCompat.setOnApplyWindowInsetsListener(galleryHeader) { _: View?, insets: WindowInsetsCompat ->\n            if (!Settings.readingFullscreen) {\n                galleryHeader.setTopInsets(insets.getInsets(WindowInsetsCompat.Type.statusBars()).top)\n            } else {\n                galleryHeader.setDisplayCutout(insets.displayCutout)\n            }\n            WindowInsetsCompat.CONSUMED\n        }\n\n        // Screen lightness\n        setScreenLightness(Settings.customScreenLightness, Settings.screenLightness)\n\n        // Update keep screen on\n        if (Settings.keepScreenOn) {\n            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n        } else {\n            window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n        }\n\n        // Orientation\n        requestedOrientation = when (Settings.screenRotation) {\n            0 -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED\n            1 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT\n            2 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE\n            3 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR\n            else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED\n        }\n\n        // Guide\n        if (Settings.guideGallery) {\n            val mainLayout = ViewUtils.`$$`(this, R.id.main) as FrameLayout\n            mainLayout.addView(GalleryGuideView(this))\n        }\n    }\n\n    private fun setGallery() {\n        if (mGalleryProvider?.isReady != true) return\n\n        // TODO: Not well place to call it\n        dialog.dismiss()\n\n        mGLLoading?.visibility = View.GONE\n        mGLRootView?.visibility = View.VISIBLE\n        // Get start page\n        if (mCurrentIndex == 0) mCurrentIndex = if (mPage >= 0) mPage else mGalleryProvider!!.startPage\n        mGalleryAdapter = GalleryAdapter(mGLRootView!!, mGalleryProvider!!)\n        val resources = resources\n        mGalleryView = GalleryView.Builder(this, mGalleryAdapter!!)\n            .setListener(this)\n            .setLayoutMode(Settings.readingDirection)\n            .setScaleMode(Settings.pageScaling)\n            .setStartPosition(Settings.startPosition)\n            .setStartPage(mCurrentIndex)\n            .setBackgroundColor(theme.resolveColor(android.R.attr.colorBackground))\n            .setPagerInterval(if (Settings.showPageInterval) resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval) else 0)\n            .setScrollInterval(if (Settings.showPageInterval) resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval) else 0)\n            .setPageMinHeight(resources.getDimensionPixelOffset(R.dimen.gallery_page_min_height))\n            .setPageInfoInterval(resources.getDimensionPixelOffset(R.dimen.gallery_page_info_interval))\n            .setProgressColor(ResourcesUtils.getAttrColor(this, androidx.appcompat.R.attr.colorPrimary))\n            .setProgressSize(resources.getDimensionPixelOffset(R.dimen.gallery_progress_size))\n            .setPageTextColor(theme.resolveColor(android.R.attr.textColorSecondary))\n            .setPageTextSize(resources.getDimensionPixelOffset(R.dimen.gallery_page_text_size))\n            .setPageTextTypeface(Typeface.DEFAULT)\n            .setErrorTextColor(this@GalleryActivity.getColor(R.color.red_500))\n            .setErrorTextSize(resources.getDimensionPixelOffset(R.dimen.gallery_error_text_size))\n            .setEmptyString(resources.getString(R.string.error_empty))\n            .build()\n        mGLRootView!!.setContentPane(mGalleryView)\n        mGalleryProvider!!.setListener(mGalleryAdapter)\n        mGalleryProvider!!.setGLRoot(mGLRootView!!)\n        if (mGalleryView != null) {\n            mLayoutMode = mGalleryView!!.layoutMode\n        }\n        mSize = mGalleryProvider!!.size\n        updateSlider()\n    }\n\n    private fun pageTurn(isPrevious: Boolean) {\n        val isRTL = mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT\n        if (isPrevious xor isRTL) {\n            mGalleryView?.pageLeft()\n        } else {\n            mGalleryView?.pageRight()\n        }\n    }\n\n    private fun autoTransfer() {\n        if (mAutoTransferJob == null && mCurrentIndex + 1 != mSize) {\n            startAutoTransfer()\n        } else {\n            stopAutoTransfer()\n        }\n    }\n\n    private fun startAutoTransfer() {\n        mAutoTransfer?.setImageResource(R.drawable.v_pause_x24)\n        mAutoTransferJob = lifecycleScope.launch {\n            repeatOnLifecycle(Lifecycle.State.RESUMED) {\n                while (true) {\n                    delay(mTurnPageIntervalVal.coerceAtLeast(1) * 1000L)\n                    pageTurn(false)\n                }\n            }\n        }\n    }\n\n    private fun stopAutoTransfer() {\n        mAutoTransfer?.setImageResource(R.drawable.v_play_x24)\n        mAutoTransferJob?.cancel()\n        mAutoTransferJob = null\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mAutoTransferJob?.cancel()\n        mGLRootView = null\n        mGalleryView = null\n        if (mGalleryAdapter != null) {\n            mGalleryAdapter!!.clearUploader()\n            mGalleryAdapter = null\n        }\n        if (mGalleryProvider != null) {\n            mGalleryProvider!!.setListener(null)\n            mGalleryProvider!!.stop()\n            mGalleryProvider = null\n        }\n        mMaskView = null\n        mClock = null\n        mProgress = null\n        mBattery = null\n        mSeekBarPanel = null\n        mGLLoading = null\n        mLeftText = null\n        mRightText = null\n        mSeekBar = null\n        mAutoTransfer = null\n        mAutoTransferJob = null\n        SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable)\n    }\n\n    override fun onPause() {\n        super.onPause()\n        mGLRootView?.onPause()\n    }\n\n    override fun onResume() {\n        super.onResume()\n        mGLRootView?.onResume()\n    }\n\n    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {\n        mGalleryView ?: return super.onKeyDown(keyCode, event)\n\n        return when (keyCode) {\n            KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> {\n                if (!Settings.volumePage) {\n                    false\n                } else {\n                    val isPrevious = Settings.reverseVolumePage.xor(keyCode == KeyEvent.KEYCODE_VOLUME_UP)\n                    val shouldTurn = event.repeatCount % (Settings.volumePageInterval + 1) == 0\n\n                    if (shouldTurn) pageTurn(isPrevious)\n                    true\n                }\n            }\n            KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_DPAD_UP -> pageTurn(true).let { true }\n            KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_DPAD_DOWN -> pageTurn(false).let { true }\n            KeyEvent.KEYCODE_DPAD_LEFT -> mGalleryView!!.pageLeft().let { true }\n            KeyEvent.KEYCODE_DPAD_RIGHT -> mGalleryView!!.pageRight().let { true }\n            KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_MENU -> {\n                onTapMenuArea()\n                true\n            }\n            else -> false\n        } ||\n            super.onKeyDown(keyCode, event)\n    }\n\n    override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {\n        // Check volume\n        if (Settings.volumePage) {\n            if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||\n                keyCode == KeyEvent.KEYCODE_VOLUME_UP\n            ) {\n                return true\n            }\n        }\n\n        // Check keyboard and Dpad\n        return if (keyCode == KeyEvent.KEYCODE_PAGE_UP ||\n            keyCode == KeyEvent.KEYCODE_PAGE_DOWN ||\n            keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||\n            keyCode == KeyEvent.KEYCODE_DPAD_UP ||\n            keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||\n            keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||\n            keyCode == KeyEvent.KEYCODE_DPAD_CENTER ||\n            keyCode == KeyEvent.KEYCODE_SPACE ||\n            keyCode == KeyEvent.KEYCODE_MENU\n        ) {\n            true\n        } else {\n            super.onKeyUp(keyCode, event)\n        }\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    private fun updateProgress() {\n        if (mCurrentIndex + 1 == mSize) autoTransfer()\n        mProgress?.text =\n            if (mSize <= 0 || mCurrentIndex < 0) null else (mCurrentIndex + 1).toString() + \"/\" + mSize\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    private fun updateSlider() {\n        if (mSeekBar == null || mRightText == null || mLeftText == null || mSize <= 0 || mCurrentIndex < 0) {\n            return\n        }\n        val start: TextView\n        val end: TextView\n        if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {\n            start = mRightText!!\n            end = mLeftText!!\n            mSeekBar!!.setReverse(true)\n        } else {\n            start = mLeftText!!\n            end = mRightText!!\n            mSeekBar!!.setReverse(false)\n        }\n        start.text = (mCurrentIndex + 1).toString()\n        end.text = mSize.toString()\n        mSeekBar!!.max = mSize - 1\n        mSeekBar!!.progress = mCurrentIndex\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {\n        val start = if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) {\n            mRightText\n        } else {\n            mLeftText\n        }\n        if (fromUser && null != start) {\n            start.text = (progress + 1).toString()\n        }\n        if (fromUser && null != mGalleryView) {\n            mGalleryView!!.setCurrentPage(progress)\n        }\n    }\n\n    override fun onStartTrackingTouch(seekBar: SeekBar) {\n        SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable)\n    }\n\n    override fun onStopTrackingTouch(seekBar: SeekBar) {\n        SimpleHandler.getInstance().postDelayed(mHideSliderRunnable, HIDE_SLIDER_DELAY)\n    }\n\n    override fun onUpdateCurrentIndex(index: Int) {\n        mGalleryProvider?.putStartPage(index)\n        val task = mNotifyTaskPool.pop() ?: NotifyTask()\n        task.setData(NOTIFY_KEY_CURRENT_INDEX, index)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onTapSliderArea() {\n        val task = mNotifyTaskPool.pop() ?: NotifyTask()\n        task.setData(NOTIFY_KEY_TAP_SLIDER_AREA, 0)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onTapMenuArea() {\n        val task = mNotifyTaskPool.pop() ?: NotifyTask()\n        task.setData(NOTIFY_KEY_TAP_MENU_AREA, 0)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onTapErrorText(index: Int) {\n        val task = mNotifyTaskPool.pop() ?: NotifyTask()\n        task.setData(NOTIFY_KEY_TAP_ERROR_TEXT, index)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    override fun onLongPressPage(index: Int) {\n        val task = mNotifyTaskPool.pop() ?: NotifyTask()\n        task.setData(NOTIFY_KEY_LONG_PRESS_PAGE, index)\n        SimpleHandler.getInstance().post(task)\n    }\n\n    private fun showSlider(sliderPanel: View) {\n        if (null != mSeekBarPanelAnimator) {\n            mSeekBarPanelAnimator!!.cancel()\n            mSeekBarPanelAnimator = null\n        }\n        sliderPanel.translationY = sliderPanel.height.toFloat()\n        sliderPanel.visibility = View.VISIBLE\n        mSeekBarPanelAnimator = ObjectAnimator.ofFloat(sliderPanel, \"translationY\", 0.0f)\n        mSeekBarPanelAnimator!!.duration = SLIDER_ANIMATION_DURING\n        mSeekBarPanelAnimator!!.interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR\n        mSeekBarPanelAnimator!!.addUpdateListener(mUpdateSliderListener)\n        mSeekBarPanelAnimator!!.addListener(mShowSliderListener)\n        mSeekBarPanelAnimator!!.start()\n        if (Settings.readingFullscreen) insetsController?.show(WindowInsetsCompat.Type.systemBars())\n    }\n\n    private fun hideSlider(sliderPanel: View) {\n        if (null != mSeekBarPanelAnimator) {\n            mSeekBarPanelAnimator!!.cancel()\n            mSeekBarPanelAnimator = null\n        }\n        mSeekBarPanelAnimator =\n            ObjectAnimator.ofFloat(sliderPanel, \"translationY\", sliderPanel.height.toFloat())\n        mSeekBarPanelAnimator!!.duration = SLIDER_ANIMATION_DURING\n        mSeekBarPanelAnimator!!.interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR\n        mSeekBarPanelAnimator!!.addUpdateListener(mUpdateSliderListener)\n        mSeekBarPanelAnimator!!.addListener(mHideSliderListener)\n        mSeekBarPanelAnimator!!.start()\n        if (Settings.readingFullscreen) insetsController?.hide(WindowInsetsCompat.Type.systemBars())\n    }\n\n    /**\n     * @param lightness 0 - 200\n     */\n    private fun setScreenLightness(enable: Boolean, lightness: Int) {\n        var mLightness = lightness\n        if (null == mMaskView) {\n            return\n        }\n        val w = window\n        val lp = w.attributes\n        if (enable) {\n            mLightness = MathUtils.clamp(mLightness, 0, 200)\n            if (mLightness > 100) {\n                mMaskView!!.setColor(0)\n                // Avoid BRIGHTNESS_OVERRIDE_OFF,\n                // screen may be off when lp.screenBrightness is 0.0f\n                lp.screenBrightness = ((mLightness - 100) / 100.0f).coerceAtLeast(0.01f)\n            } else {\n                mMaskView!!.setColor(MathUtils.lerp(0xde, 0x00, mLightness / 100.0f) shl 24)\n                lp.screenBrightness = 0.01f\n            }\n        } else {\n            mMaskView!!.setColor(0)\n            lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE\n        }\n        w.attributes = lp\n    }\n\n    private fun shareImage(page: Int) {\n        if (null == mGalleryProvider) {\n            return\n        }\n        val dir = AppConfig.getExternalTempDir()\n        if (null == dir) {\n            Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show()\n            return\n        }\n        val file = mGalleryProvider!!.save(\n            page,\n            UniFile.fromFile(dir)!!,\n            mGalleryProvider!!.getImageFilename(page),\n        )\n        if (file == null) {\n            Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n            return\n        }\n        val filename = file.name\n        if (filename == null) {\n            Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n            return\n        }\n        var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(\n            MimeTypeMap.getFileExtensionFromUrl(filename),\n        )\n        if (TextUtils.isEmpty(mimeType)) {\n            mimeType = \"image/jpeg\"\n        }\n        val uri = FileProvider.getUriForFile(\n            this,\n            BuildConfig.APPLICATION_ID + \".fileprovider\",\n            File(dir, filename),\n        )\n        val intent = Intent()\n        intent.action = Intent.ACTION_SEND\n        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        intent.putExtra(Intent.EXTRA_STREAM, uri)\n        if (mGalleryInfo != null) {\n            intent.putExtra(\n                Intent.EXTRA_TEXT,\n                EhUrl.getGalleryDetailUrl(mGalleryInfo!!.gid, mGalleryInfo!!.token),\n            )\n        }\n        intent.setDataAndType(uri, mimeType)\n        try {\n            startActivity(Intent.createChooser(intent, getString(R.string.share_image)))\n        } catch (e: Throwable) {\n            ExceptionUtils.throwIfFatal(e)\n            Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show()\n        }\n    }\n\n    private fun copyImage(page: Int) {\n        if (null == mGalleryProvider) {\n            return\n        }\n        val dir = AppConfig.getExternalCopyTempDir()\n        if (null == dir) {\n            Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show()\n            return\n        }\n        val file = mGalleryProvider!!.save(\n            page,\n            UniFile.fromFile(dir)!!,\n            mGalleryProvider!!.getImageFilename(page),\n        )\n        if (file == null) {\n            Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n            return\n        }\n        val filename = file.name\n        if (filename == null) {\n            Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n            return\n        }\n        val uri = FileProvider.getUriForFile(\n            this,\n            BuildConfig.APPLICATION_ID + \".fileprovider\",\n            File(dir, filename),\n        )\n        val clipboardManager = getSystemService(ClipboardManager::class.java)\n        if (clipboardManager != null) {\n            val clipData = ClipData.newUri(contentResolver, \"ehviewer\", uri)\n            clipboardManager.setPrimaryClip(clipData)\n            Toast.makeText(this, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show()\n        }\n    }\n\n    private fun saveImage(page: Int) {\n        if (null == mGalleryProvider) {\n            return\n        }\n        if (!isAtLeastQ &&\n            ContextCompat.checkSelfPermission(\n                this,\n                Manifest.permission.WRITE_EXTERNAL_STORAGE,\n            ) != PackageManager.PERMISSION_GRANTED\n        ) {\n            mSavingPage = page\n            requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)\n            return\n        }\n        val filename = mGalleryProvider!!.getImageFilenameWithExtension(page)\n        var mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(\n            MimeTypeMap.getFileExtensionFromUrl(filename),\n        )\n        if (TextUtils.isEmpty(mimeType)) {\n            mimeType = \"image/jpeg\"\n        }\n        val realPath: String\n        val resolver = contentResolver\n        val values = ContentValues()\n        values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)\n        values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis())\n        values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)\n        if (isAtLeastQ) {\n            values.put(\n                MediaStore.MediaColumns.RELATIVE_PATH,\n                Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME,\n            )\n            values.put(MediaStore.MediaColumns.IS_PENDING, 1)\n            realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME\n        } else {\n            val path = File(\n                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),\n                AppConfig.APP_DIRNAME,\n            )\n            realPath = path.toString()\n            if (!FileUtils.ensureDirectory(path)) {\n                Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n                return\n            }\n            values.put(MediaStore.MediaColumns.DATA, path.toString() + File.separator + filename)\n        }\n        val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)\n        if (imageUri == null) {\n            Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n            return\n        }\n        if (!mGalleryProvider!!.save(page, UniFile.fromMediaUri(this, imageUri))) {\n            try {\n                resolver.delete(imageUri, null, null)\n            } catch (e: Exception) {\n                e.printStackTrace()\n            }\n            Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show()\n            return\n        } else if (isAtLeastQ) {\n            val contentValues = ContentValues()\n            contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0)\n            resolver.update(imageUri, contentValues, null, null)\n        }\n        Toast.makeText(\n            this,\n            getString(R.string.image_saved, realPath + File.separator + filename),\n            Toast.LENGTH_SHORT,\n        ).show()\n    }\n\n    private fun saveImageTo(page: Int, original: Boolean = false) {\n        lifecycleScope.launchIO {\n            if (null == mGalleryProvider) {\n                return@launchIO\n            }\n            val dir = AppConfig.getExternalTempDir()\n            if (null == dir) {\n                withUIContext {\n                    Toast.makeText(\n                        this@GalleryActivity,\n                        R.string.error_cant_create_temp_file,\n                        Toast.LENGTH_SHORT,\n                    ).show()\n                }\n                return@launchIO\n            }\n            val file = if (original) {\n                withUIContext {\n                    Toast.makeText(\n                        this@GalleryActivity,\n                        R.string.start_download_original,\n                        Toast.LENGTH_SHORT,\n                    ).show()\n                }\n                mGalleryProvider!!.downloadOriginal(\n                    page,\n                    UniFile.fromFile(dir)!!,\n                    mGalleryProvider!!.getImageFilename(page),\n                )\n            } else {\n                mGalleryProvider!!.save(\n                    page,\n                    UniFile.fromFile(dir)!!,\n                    mGalleryProvider!!.getImageFilename(page),\n                )\n            }\n            if (file == null) {\n                withUIContext {\n                    Toast.makeText(\n                        this@GalleryActivity,\n                        R.string.error_cant_save_image,\n                        Toast.LENGTH_SHORT,\n                    ).show()\n                }\n                return@launchIO\n            }\n            val filename = file.name\n            if (filename == null) {\n                withUIContext {\n                    Toast.makeText(\n                        this@GalleryActivity,\n                        R.string.error_cant_save_image,\n                        Toast.LENGTH_SHORT,\n                    ).show()\n                }\n                return@launchIO\n            }\n            mCacheFileName = filename\n            try {\n                saveImageToLauncher.launch(filename)\n            } catch (e: Throwable) {\n                ExceptionUtils.throwIfFatal(e)\n                withUIContext {\n                    Toast.makeText(\n                        this@GalleryActivity,\n                        R.string.error_cant_find_activity,\n                        Toast.LENGTH_SHORT,\n                    ).show()\n                }\n            }\n        }\n    }\n\n    private fun showPageDialog(page: Int) {\n        val resources = this@GalleryActivity.resources\n        val builder = AlertDialog.Builder(this@GalleryActivity)\n        builder.setTitle(resources.getString(R.string.page_menu_title, page + 1))\n        val items = arrayListOf<CharSequence>(\n            getString(R.string.page_menu_refresh),\n            getString(R.string.page_menu_share),\n            getString(android.R.string.copy),\n            getString(R.string.page_menu_save),\n            getString(R.string.page_menu_save_to),\n        )\n        if (ACTION_EH == mAction && !Settings.getDownloadOriginImage(false)) {\n            items.add(getString(R.string.page_menu_download_original))\n        }\n        pageDialogListener(builder, items.toTypedArray(), page)\n        builder.show()\n    }\n\n    private fun pageDialogListener(\n        builder: AlertDialog.Builder,\n        items: Array<CharSequence>,\n        page: Int,\n    ) {\n        builder.setItems(items) { _: DialogInterface?, which: Int ->\n            if (mGalleryProvider == null) {\n                return@setItems\n            }\n            when (which) {\n                0 -> {\n                    mGalleryProvider!!.removeCache(page)\n                    mGalleryProvider!!.forceRequest(page)\n                }\n                1 -> shareImage(page)\n                2 -> copyImage(page)\n                3 -> saveImage(page)\n                4 -> saveImageTo(page)\n                5 -> saveImageTo(page, true)\n            }\n        }\n    }\n\n    override fun onProvideAssistContent(outContent: AssistContent) {\n        super.onProvideAssistContent(outContent)\n        galleryDetailUrl?.let {\n            outContent.webUri = it.toUri()\n        }\n    }\n\n    @SuppressLint(\"InflateParams\", \"UseSwitchCompatOrMaterialCode\")\n    private inner class GalleryMenuHelper(context: Context?) : DialogInterface.OnClickListener {\n        val view: View = LayoutInflater.from(context).inflate(R.layout.dialog_gallery_menu, null)\n        private val mScreenRotation: Spinner = view.findViewById(R.id.screen_rotation)\n        private val mReadingDirection: Spinner = view.findViewById(R.id.reading_direction)\n        private val mScaleMode: Spinner = view.findViewById(R.id.page_scaling)\n        private val mStartPosition: Spinner = view.findViewById(R.id.start_position)\n        private val mReadTheme: Spinner = view.findViewById(R.id.read_theme)\n        private val mKeepScreenOn: Switch = view.findViewById(R.id.keep_screen_on)\n        private val mShowClock: Switch = view.findViewById(R.id.show_clock)\n        private val mShowProgress: Switch = view.findViewById(R.id.show_progress)\n        private val mShowBattery: Switch = view.findViewById(R.id.show_battery)\n        private val mShowPageInterval: Switch = view.findViewById(R.id.show_page_interval)\n        private val mTurnPageInterval: SeekBar = view.findViewById(R.id.turn_page_interval)\n        private val mVolumePage: Switch = view.findViewById(R.id.volume_page)\n        private val mVolumePageInterval: SeekBar = view.findViewById(R.id.volume_page_interval)\n        private val mReverseVolumePage: Switch = view.findViewById(R.id.reverse_volume_page)\n        private val mReadingFullscreen: Switch = view.findViewById(R.id.reading_fullscreen)\n        private val mCustomScreenLightness: Switch = view.findViewById(R.id.custom_screen_lightness)\n        private val mScreenLightness: SeekBar = view.findViewById(R.id.screen_lightness)\n\n        init {\n            mScreenRotation.setSelection(Settings.screenRotation)\n            mReadingDirection.setSelection(Settings.readingDirection)\n            mScaleMode.setSelection(Settings.pageScaling)\n            mStartPosition.setSelection(Settings.startPosition)\n            mReadTheme.setSelection(Settings.readTheme)\n            mKeepScreenOn.isChecked = Settings.keepScreenOn\n            mShowClock.isChecked = Settings.showClock\n            mShowProgress.isChecked = Settings.showProgress\n            mShowBattery.isChecked = Settings.showBattery\n            mShowPageInterval.isChecked = Settings.showPageInterval\n            mTurnPageInterval.progress = Settings.turnPageInterval - 1\n            mVolumePage.isChecked = Settings.volumePage\n            mVolumePage.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->\n                (mVolumePageInterval.parent as? ViewGroup)?.visibility = if (isChecked) View.VISIBLE else View.GONE\n                mReverseVolumePage.visibility = if (isChecked) View.VISIBLE else View.GONE\n            }\n            (mVolumePageInterval.parent as? ViewGroup)?.visibility = if (Settings.volumePage) View.VISIBLE else View.GONE\n            mVolumePageInterval.progress = Settings.volumePageInterval\n            mReverseVolumePage.visibility = if (Settings.volumePage) View.VISIBLE else View.GONE\n            mReverseVolumePage.isChecked = Settings.reverseVolumePage\n            mReadingFullscreen.isChecked = Settings.readingFullscreen\n            mCustomScreenLightness.isChecked = Settings.customScreenLightness\n            mCustomScreenLightness.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->\n                mScreenLightness.visibility = if (isChecked) View.VISIBLE else View.GONE\n            }\n            mScreenLightness.progress = Settings.screenLightness\n            mScreenLightness.visibility = if (Settings.customScreenLightness) View.VISIBLE else View.GONE\n        }\n\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            if (mGalleryView == null) {\n                return\n            }\n            val screenRotation = mScreenRotation.selectedItemPosition\n            val layoutMode = GalleryView.sanitizeLayoutMode(mReadingDirection.selectedItemPosition)\n            val scaleMode = GalleryView.sanitizeScaleMode(mScaleMode.selectedItemPosition)\n            val startPosition =\n                GalleryView.sanitizeStartPosition(mStartPosition.selectedItemPosition)\n            val readTheme = mReadTheme.selectedItemPosition\n            val keepScreenOn = mKeepScreenOn.isChecked\n            val showClock = mShowClock.isChecked\n            val showProgress = mShowProgress.isChecked\n            val showBattery = mShowBattery.isChecked\n            val showPageInterval = mShowPageInterval.isChecked\n            val turnPageInterval = mTurnPageInterval.progress + 1\n            val volumePage = mVolumePage.isChecked\n            val volumePageInterval = mVolumePageInterval.progress\n            val reverseVolumePage = mReverseVolumePage.isChecked\n            val readingFullscreen = mReadingFullscreen.isChecked\n            val customScreenLightness = mCustomScreenLightness.isChecked\n            val screenLightness = mScreenLightness.progress\n            val oldReadingFullscreen = Settings.readingFullscreen\n            val oldReadTheme = Settings.readTheme\n            Settings.putScreenRotation(screenRotation)\n            Settings.putReadingDirection(layoutMode)\n            Settings.putPageScaling(scaleMode)\n            Settings.putStartPosition(startPosition)\n            Settings.putReadTheme(readTheme)\n            Settings.putKeepScreenOn(keepScreenOn)\n            Settings.putShowClock(showClock)\n            Settings.putShowProgress(showProgress)\n            Settings.putShowBattery(showBattery)\n            Settings.putShowPageInterval(showPageInterval)\n            Settings.putTurnPageInterval(turnPageInterval)\n            Settings.putVolumePage(volumePage)\n            Settings.putVolumePageInterval(volumePageInterval)\n            Settings.putReverseVolumePage(reverseVolumePage)\n            Settings.putReadingFullscreen(readingFullscreen)\n            Settings.putCustomScreenLightness(customScreenLightness)\n            Settings.putScreenLightness(screenLightness)\n            requestedOrientation = when (screenRotation) {\n                0 -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED\n                1 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT\n                2 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE\n                3 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR\n                else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED\n            }\n            mGalleryView!!.layoutMode = layoutMode\n            mGalleryView!!.setScaleMode(scaleMode)\n            mGalleryView!!.setStartPosition(startPosition)\n            if (keepScreenOn) {\n                window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n            } else {\n                window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n            }\n            mClock?.visibility = if (showClock) View.VISIBLE else View.GONE\n            mProgress?.visibility = if (showProgress) View.VISIBLE else View.GONE\n            mBattery?.visibility = if (showBattery) View.VISIBLE else View.GONE\n            mGalleryView!!.setPagerInterval(\n                if (showPageInterval) {\n                    resources.getDimensionPixelOffset(\n                        R.dimen.gallery_pager_interval,\n                    )\n                } else {\n                    0\n                },\n            )\n            mGalleryView!!.setScrollInterval(\n                if (showPageInterval) {\n                    resources.getDimensionPixelOffset(\n                        R.dimen.gallery_scroll_interval,\n                    )\n                } else {\n                    0\n                },\n            )\n            mTurnPageIntervalVal = turnPageInterval\n            setScreenLightness(customScreenLightness, screenLightness)\n            // Update slider\n            mLayoutMode = layoutMode\n            updateSlider()\n            if (oldReadingFullscreen != readingFullscreen || oldReadTheme != readTheme) {\n                recreate()\n            }\n        }\n    }\n\n    private inner class NotifyTask : Runnable {\n        private var mKey = 0\n        private var mValue = 0\n\n        fun setData(key: Int, value: Int) {\n            mKey = key\n            mValue = value\n        }\n\n        private fun onTapMenuArea() {\n            val builder = AlertDialog.Builder(this@GalleryActivity)\n            val helper = GalleryMenuHelper(builder.context)\n            builder.setTitle(R.string.gallery_menu_title)\n                .setView(helper.view)\n                .setPositiveButton(android.R.string.ok, helper).show()\n        }\n\n        private fun onTapSliderArea() {\n            if (mSeekBarPanel == null || mSize <= 0 || mCurrentIndex < 0) {\n                return\n            }\n            SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable)\n            if (mSeekBarPanel!!.isVisible) {\n                hideSlider(mSeekBarPanel!!)\n            } else {\n                showSlider(mSeekBarPanel!!)\n                SimpleHandler.getInstance().postDelayed(mHideSliderRunnable, HIDE_SLIDER_DELAY)\n            }\n        }\n\n        private fun onTapErrorText(index: Int) {\n            if (mGalleryProvider != null) {\n                mGalleryProvider!!.forceRequest(index)\n            }\n        }\n\n        private fun onLongPressPage(index: Int) {\n            showPageDialog(index)\n        }\n\n        override fun run() {\n            when (mKey) {\n                NOTIFY_KEY_LAYOUT_MODE -> {\n                    mLayoutMode = mValue\n                    updateSlider()\n                }\n                NOTIFY_KEY_SIZE -> {\n                    mSize = mValue\n                    updateSlider()\n                    updateProgress()\n                }\n                NOTIFY_KEY_CURRENT_INDEX -> {\n                    mCurrentIndex = mValue\n                    updateSlider()\n                    updateProgress()\n                }\n                NOTIFY_KEY_TAP_MENU_AREA -> onTapMenuArea()\n                NOTIFY_KEY_TAP_SLIDER_AREA -> onTapSliderArea()\n                NOTIFY_KEY_TAP_ERROR_TEXT -> onTapErrorText(mValue)\n                NOTIFY_KEY_LONG_PRESS_PAGE -> onLongPressPage(mValue)\n            }\n            mNotifyTaskPool.push(this)\n        }\n    }\n\n    private inner class GalleryAdapter(glRootView: GLRootView, provider: GalleryProvider) : SimpleAdapter(glRootView, provider) {\n        override fun onDataChanged() {\n            super.onDataChanged()\n            if (mGalleryProvider != null) {\n                val size = mGalleryProvider!!.size\n                val task = mNotifyTaskPool.pop() ?: NotifyTask()\n                task.setData(NOTIFY_KEY_SIZE, size)\n                SimpleHandler.getInstance().post(task)\n            }\n        }\n    }\n\n    companion object {\n        const val ACTION_EH = \"eh\"\n        const val KEY_ACTION = \"action\"\n        const val KEY_FILENAME = \"filename\"\n        const val KEY_URI = \"uri\"\n        const val KEY_GALLERY_INFO = \"gallery_info\"\n        const val KEY_PAGE = \"page\"\n        const val KEY_CURRENT_INDEX = \"current_index\"\n        private const val SLIDER_ANIMATION_DURING: Long = 150\n        private const val HIDE_SLIDER_DELAY: Long = 3000\n        private const val NOTIFY_KEY_LAYOUT_MODE = 0\n        private const val NOTIFY_KEY_SIZE = 1\n        private const val NOTIFY_KEY_CURRENT_INDEX = 2\n        private const val NOTIFY_KEY_TAP_SLIDER_AREA = 3\n        private const val NOTIFY_KEY_TAP_MENU_AREA = 4\n        private const val NOTIFY_KEY_TAP_ERROR_TEXT = 5\n        private const val NOTIFY_KEY_LONG_PRESS_PAGE = 6\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/MainActivity.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui\n\nimport android.annotation.SuppressLint\nimport android.app.UiModeManager\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.content.pm.verify.domain.DomainVerificationManager\nimport android.content.pm.verify.domain.DomainVerificationUserState\nimport android.content.res.Configuration\nimport android.net.ConnectivityManager\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.PersistableBundle\nimport android.text.TextUtils\nimport android.view.MenuItem\nimport android.view.View\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.annotation.IdRes\nimport androidx.annotation.RequiresApi\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.coordinatorlayout.widget.CoordinatorLayout\nimport androidx.core.content.getSystemService\nimport androidx.core.net.toUri\nimport androidx.core.view.GravityCompat\nimport androidx.core.view.isVisible\nimport androidx.drawerlayout.widget.DrawerLayout\nimport com.google.android.material.navigation.NavigationView\nimport com.google.android.material.snackbar.Snackbar\nimport com.hippo.app.EditTextDialogBuilder\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUrlOpener\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.ListUrlBuilder\nimport com.hippo.ehviewer.client.parser.GalleryDetailUrlParser\nimport com.hippo.ehviewer.client.parser.GalleryPageUrlParser\nimport com.hippo.ehviewer.ui.scene.BaseScene\nimport com.hippo.ehviewer.ui.scene.CookieSignInScene\nimport com.hippo.ehviewer.ui.scene.DownloadsScene\nimport com.hippo.ehviewer.ui.scene.FavoritesScene\nimport com.hippo.ehviewer.ui.scene.GalleryCommentsScene\nimport com.hippo.ehviewer.ui.scene.GalleryDetailScene\nimport com.hippo.ehviewer.ui.scene.GalleryInfoScene\nimport com.hippo.ehviewer.ui.scene.GalleryListScene\nimport com.hippo.ehviewer.ui.scene.GalleryPreviewsScene\nimport com.hippo.ehviewer.ui.scene.HistoryScene\nimport com.hippo.ehviewer.ui.scene.ProgressScene\nimport com.hippo.ehviewer.ui.scene.SecurityScene\nimport com.hippo.ehviewer.ui.scene.SelectSiteScene\nimport com.hippo.ehviewer.ui.scene.SignInScene\nimport com.hippo.ehviewer.ui.scene.SolidScene\nimport com.hippo.ehviewer.ui.scene.WebViewSignInScene\nimport com.hippo.ehviewer.widget.EhStageLayout\nimport com.hippo.scene.Announcer\nimport com.hippo.scene.SceneFragment\nimport com.hippo.scene.StageActivity\nimport com.hippo.unifile.UniFile\nimport com.hippo.unifile.sha1\nimport com.hippo.util.addTextToClipboard\nimport com.hippo.util.getClipboardManager\nimport com.hippo.util.getParcelableExtraCompat\nimport com.hippo.util.getUrlFromClipboard\nimport com.hippo.util.isAtLeastQ\nimport com.hippo.util.isAtLeastS\nimport com.hippo.widget.DrawerView\nimport com.hippo.widget.LoadImageView\nimport com.hippo.yorozuya.SimpleHandler\nimport com.hippo.yorozuya.ViewUtils\n\nclass MainActivity :\n    StageActivity(),\n    NavigationView.OnNavigationItemSelectedListener {\n    private val settingsLauncher =\n        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {\n            if (it.resultCode == RESULT_OK) refreshTopScene()\n        }\n    private lateinit var connectivityManager: ConnectivityManager\n    private var mSnackBar: CoordinatorLayout? = null\n    private var mDrawerLayout: DrawerLayout? = null\n    private var mStageLayout: EhStageLayout? = null\n    private var mNavView: NavigationView? = null\n    private var mRightDrawer: DrawerView? = null\n    private var mAvatar: LoadImageView? = null\n    private var mDisplayName: TextView? = null\n    private var mNavCheckedItem = 0\n\n    override var containerViewId: Int = R.id.fragment_container\n\n    override var launchAnnouncer: Announcer =\n        if (!TextUtils.isEmpty(Settings.security)) {\n            Announcer(SecurityScene::class.java)\n        } else if (EhUtils.needSignedIn()) {\n            Announcer(SignInScene::class.java)\n        } else if (Settings.selectSite) {\n            Announcer(SelectSiteScene::class.java)\n        } else {\n            val args = Bundle()\n            args.putString(\n                GalleryListScene.KEY_ACTION,\n                Settings.launchPageGalleryListSceneAction,\n            )\n            Announcer(GalleryListScene::class.java).setArgs(args)\n        }\n\n    // Sometimes scene can't show directly\n    private fun processAnnouncer(announcer: Announcer): Announcer {\n        if (0 == sceneCount) {\n            val newArgs = Bundle()\n            newArgs.putString(SolidScene.KEY_TARGET_SCENE, announcer.clazz.name)\n            newArgs.putBundle(SolidScene.KEY_TARGET_ARGS, announcer.args)\n            if (!TextUtils.isEmpty(Settings.security)) {\n                return Announcer(SecurityScene::class.java).setArgs(newArgs)\n            } else if (EhUtils.needSignedIn()) {\n                return Announcer(SignInScene::class.java).setArgs(newArgs)\n            } else if (Settings.selectSite) {\n                return Announcer(SelectSiteScene::class.java).setArgs(newArgs)\n            }\n        }\n        return announcer\n    }\n\n    private fun handleIntent(intent: Intent?): Boolean {\n        if (intent == null) {\n            return false\n        }\n        val action = intent.action\n        if (Intent.ACTION_VIEW == action) {\n            val uri = intent.data ?: return false\n            val announcer = EhUrlOpener.parseUrl(uri.toString())\n            if (announcer != null) {\n                startScene(processAnnouncer(announcer))\n                return true\n            }\n        } else if (Intent.ACTION_SEND == action) {\n            val type = intent.type\n            if (\"text/plain\" == type) {\n                val builder = ListUrlBuilder()\n                builder.keyword = intent.getStringExtra(Intent.EXTRA_TEXT)\n                startScene(processAnnouncer(GalleryListScene.getStartAnnouncer(builder)))\n                return true\n            } else if (type != null && type.startsWith(\"image/\")) {\n                val uri = intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)\n                if (null != uri) {\n                    UniFile.fromUri(this, uri)?.sha1()?.let {\n                        val builder = ListUrlBuilder()\n                        builder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH\n                        builder.hash = it\n                        startScene(processAnnouncer(GalleryListScene.getStartAnnouncer(builder)))\n                        return true\n                    }\n                }\n            }\n        }\n        return false\n    }\n\n    override fun onUnrecognizedIntent(intent: Intent?) {\n        val clazz = topSceneClass\n        if (clazz != null && SolidScene::class.java.isAssignableFrom(clazz)) {\n            // TODO the intent lost\n            return\n        }\n        if (!handleIntent(intent)) {\n            var handleUrl = false\n            if (intent != null && Intent.ACTION_VIEW == intent.action) {\n                handleUrl = true\n                if (intent.data != null) {\n                    val url = intent.data.toString()\n                    EditTextDialogBuilder(this, url, \"\")\n                        .setTitle(R.string.error_cannot_parse_the_url)\n                        .setPositiveButton(android.R.string.copy) { _: DialogInterface?, _: Int ->\n                            this.addTextToClipboard(\n                                url,\n                                false,\n                            )\n                        }\n                        .show()\n                }\n            }\n            if (0 == sceneCount) {\n                if (handleUrl) {\n                    finish()\n                } else {\n                    val args = Bundle()\n                    args.putString(\n                        GalleryListScene.KEY_ACTION,\n                        Settings.launchPageGalleryListSceneAction,\n                    )\n                    startScene(processAnnouncer(Announcer(GalleryListScene::class.java).setArgs(args)))\n                }\n            }\n        }\n    }\n\n    override fun onStartSceneFromIntent(clazz: Class<*>, args: Bundle?): Announcer = processAnnouncer(Announcer(clazz).setArgs(args))\n\n    override fun onCreate2(savedInstanceState: Bundle?) {\n        connectivityManager = getSystemService()!!\n        setContentView(R.layout.activity_main)\n        mSnackBar = ViewUtils.`$$`(this, R.id.snackbar) as CoordinatorLayout\n        mStageLayout = ViewUtils.`$$`(this, R.id.fragment_container) as EhStageLayout\n        mDrawerLayout = ViewUtils.`$$`(this, R.id.draw_view) as DrawerLayout\n        mNavView = ViewUtils.`$$`(this, R.id.nav_view) as NavigationView\n        mRightDrawer = ViewUtils.`$$`(this, R.id.right_drawer) as DrawerView\n        if (mDrawerLayout != null) {\n            mDrawerLayout!!.setStatusBarBackgroundColor(0)\n        }\n        if (mNavView != null) {\n            val headerLayout = mNavView!!.getHeaderView(0)\n            mAvatar = ViewUtils.`$$`(headerLayout, R.id.avatar) as LoadImageView\n            mDisplayName = ViewUtils.`$$`(headerLayout, R.id.display_name) as TextView\n            ViewUtils.`$$`(headerLayout, R.id.night_mode).setOnClickListener {\n                val theme = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES <= 0\n                val target = if (((getSystemService(UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES) == theme) {\n                    AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM\n                } else if (theme) {\n                    AppCompatDelegate.MODE_NIGHT_YES\n                } else {\n                    AppCompatDelegate.MODE_NIGHT_NO\n                }\n                AppCompatDelegate.setDefaultNightMode(target)\n                Settings.putTheme(target)\n                recreate()\n            }\n            updateProfile()\n            mNavView!!.setNavigationItemSelectedListener(this)\n            mAvatar!!.setOnClickListener { updateProfile() }\n        }\n        if (savedInstanceState == null) {\n            checkDownloadLocation()\n            if (Settings.meteredNetworkWarning) {\n                checkMeteredNetwork()\n            }\n            if (isAtLeastS) {\n                if (!Settings.appLinkVerifyTip) {\n                    try {\n                        checkAppLinkVerify()\n                    } catch (_: PackageManager.NameNotFoundException) {\n                    }\n                }\n            }\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    @RequiresApi(Build.VERSION_CODES.S)\n    @Throws(PackageManager.NameNotFoundException::class)\n    private fun checkAppLinkVerify() {\n        val manager = getSystemService(DomainVerificationManager::class.java)\n        val userState = manager.getDomainVerificationUserState(packageName) ?: return\n        var hasUnverified = false\n        val hostToStateMap = userState.hostToStateMap\n        for (key in hostToStateMap.keys) {\n            val stateValue = hostToStateMap[key]\n            if (stateValue == null || stateValue == DomainVerificationUserState.DOMAIN_STATE_VERIFIED || stateValue == DomainVerificationUserState.DOMAIN_STATE_SELECTED) {\n                continue\n            }\n            hasUnverified = true\n            break\n        }\n        if (hasUnverified) {\n            AlertDialog.Builder(this)\n                .setTitle(R.string.app_link_not_verified_title)\n                .setMessage(R.string.app_link_not_verified_message)\n                .setPositiveButton(R.string.open_settings) { _: DialogInterface?, _: Int ->\n                    try {\n                        val intent = Intent(\n                            android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,\n                            \"package:$packageName\".toUri(),\n                        )\n                        startActivity(intent)\n                    } catch (_: Throwable) {\n                        val intent = Intent(\n                            android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,\n                            \"package:$packageName\".toUri(),\n                        )\n                        startActivity(intent)\n                    }\n                }\n                .setNegativeButton(android.R.string.cancel, null)\n                .setNeutralButton(R.string.dont_show_again) { _: DialogInterface?, _: Int ->\n                    Settings.putAppLinkVerifyTip(\n                        true,\n                    )\n                }\n                .show()\n        }\n    }\n\n    private fun checkDownloadLocation() {\n        val uniFile = Settings.downloadLocation\n        // null == uniFile for first start\n        if (null == uniFile || uniFile.ensureDir()) {\n            return\n        }\n        AlertDialog.Builder(this)\n            .setTitle(R.string.waring)\n            .setMessage(R.string.invalid_download_location)\n            .setPositiveButton(R.string.get_it, null)\n            .show()\n    }\n\n    private fun checkMeteredNetwork() {\n        if (connectivityManager.isActiveNetworkMetered) {\n            if (isAtLeastQ && mSnackBar != null) {\n                Snackbar.make(\n                    mSnackBar!!,\n                    R.string.metered_network_warning,\n                    Snackbar.LENGTH_LONG,\n                )\n                    .setAction(R.string.settings) {\n                        val panelIntent =\n                            Intent(android.provider.Settings.Panel.ACTION_INTERNET_CONNECTIVITY)\n                        startActivity(panelIntent)\n                    }\n                    .show()\n            } else {\n                showTip(R.string.metered_network_warning, BaseScene.LENGTH_LONG)\n            }\n        }\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mNavCheckedItem = savedInstanceState.getInt(KEY_NAV_CHECKED_ITEM)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {\n        super.onSaveInstanceState(outState, outPersistentState)\n        outState.putInt(KEY_NAV_CHECKED_ITEM, mNavCheckedItem)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mDrawerLayout = null\n        mNavView = null\n        mRightDrawer = null\n        mAvatar = null\n        mDisplayName = null\n    }\n\n    override fun onResume() {\n        super.onResume()\n        setNavCheckedItem(mNavCheckedItem)\n        checkClipboardUrl()\n    }\n\n    override fun onTransactScene() {\n        super.onTransactScene()\n        checkClipboardUrl()\n    }\n\n    private fun checkClipboardUrl() {\n        SimpleHandler.getInstance().postDelayed({\n            if (!isSolid) {\n                checkClipboardUrlInternal()\n            }\n        }, 300)\n    }\n\n    private val isSolid: Boolean\n        get() {\n            val topClass = topSceneClass\n            return topClass == null || SolidScene::class.java.isAssignableFrom(topClass)\n        }\n\n    private fun createAnnouncerFromClipboardUrl(url: String): Announcer? {\n        val result1 = GalleryDetailUrlParser.parse(url, false)\n        if (result1 != null) {\n            val args = Bundle()\n            args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)\n            args.putLong(GalleryDetailScene.KEY_GID, result1.gid)\n            args.putString(GalleryDetailScene.KEY_TOKEN, result1.token)\n            return Announcer(GalleryDetailScene::class.java).setArgs(args)\n        }\n        val result2 = GalleryPageUrlParser.parse(url, false)\n        if (result2 != null) {\n            val args = Bundle()\n            args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)\n            args.putLong(ProgressScene.KEY_GID, result2.gid)\n            args.putString(ProgressScene.KEY_PTOKEN, result2.pToken)\n            args.putInt(ProgressScene.KEY_PAGE, result2.page)\n            return Announcer(ProgressScene::class.java).setArgs(args)\n        }\n        return null\n    }\n\n    private fun checkClipboardUrlInternal() {\n        val text = this.getClipboardManager().getUrlFromClipboard(this)\n        val hashCode = text?.hashCode() ?: 0\n        if (text != null && hashCode != 0 && Settings.clipboardTextHashCode != hashCode) {\n            val announcer = createAnnouncerFromClipboardUrl(text)\n            if (announcer != null && mSnackBar != null) {\n                val snackbar = Snackbar.make(\n                    mSnackBar!!,\n                    R.string.clipboard_gallery_url_snack_message,\n                    Snackbar.LENGTH_SHORT,\n                )\n                snackbar.setAction(R.string.clipboard_gallery_url_snack_action) {\n                    startScene(\n                        announcer,\n                    )\n                }\n                snackbar.show()\n            }\n        }\n        Settings.putClipboardTextHashCode(hashCode)\n    }\n\n    override fun onSceneViewCreated(scene: SceneFragment, savedInstanceState: Bundle?) {\n        super.onSceneViewCreated(scene, savedInstanceState)\n        createDrawerView(scene)\n    }\n\n    @SuppressLint(\"RtlHardcoded\")\n    fun createDrawerView(scene: SceneFragment?) {\n        if (scene is BaseScene && mRightDrawer != null && mDrawerLayout != null) {\n            mRightDrawer!!.removeAllViews()\n            val drawerView = scene.createDrawerView(\n                scene.layoutInflater,\n                mRightDrawer,\n                null,\n            )\n            if (drawerView != null) {\n                mRightDrawer!!.addView(drawerView)\n                mDrawerLayout!!.setDrawerLockMode(\n                    DrawerLayout.LOCK_MODE_UNLOCKED,\n                    GravityCompat.END,\n                )\n            } else {\n                mDrawerLayout!!.setDrawerLockMode(\n                    DrawerLayout.LOCK_MODE_LOCKED_CLOSED,\n                    GravityCompat.END,\n                )\n            }\n        }\n    }\n\n    override fun onSceneViewDestroyed(scene: SceneFragment) {\n        super.onSceneViewDestroyed(scene)\n        if (scene is BaseScene) {\n            scene.destroyDrawerView()\n        }\n    }\n\n    fun updateProfile() {\n        if (mAvatar == null || mDisplayName == null) {\n            return\n        }\n        val avatarUrl = Settings.avatar\n        if (TextUtils.isEmpty(avatarUrl)) {\n            mAvatar!!.load(R.drawable.default_avatar)\n        } else {\n            mAvatar!!.load(avatarUrl!!, avatarUrl)\n        }\n        val displayName = Settings.displayName\n        if (TextUtils.isEmpty(displayName)) {\n            mDisplayName!!.text = getString(R.string.default_display_name)\n        } else {\n            mDisplayName!!.text = displayName\n        }\n    }\n\n    fun addAboveSnackView(view: View) {\n        mStageLayout?.addAboveSnackView(view)\n    }\n\n    fun removeAboveSnackView(view: View) {\n        mStageLayout?.removeAboveSnackView(view)\n    }\n\n    fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {\n        mDrawerLayout?.setDrawerLockMode(lockMode, edgeGravity)\n    }\n\n    fun openDrawer(drawerGravity: Int) {\n        mDrawerLayout?.openDrawer(drawerGravity)\n    }\n\n    fun closeDrawer(drawerGravity: Int) {\n        mDrawerLayout?.closeDrawer(drawerGravity)\n    }\n\n    fun toggleDrawer(drawerGravity: Int) {\n        mDrawerLayout?.run {\n            if (isDrawerOpen(drawerGravity)) {\n                closeDrawer(drawerGravity)\n            } else {\n                openDrawer(drawerGravity)\n            }\n        }\n    }\n\n    fun setNavCheckedItem(@IdRes resId: Int) {\n        mNavCheckedItem = resId\n        mNavView?.run {\n            if (resId == 0) {\n                setCheckedItem(R.id.nav_stub)\n            } else {\n                setCheckedItem(resId)\n            }\n        }\n    }\n\n    fun showTip(@StringRes id: Int, length: Int) {\n        showTip(getString(id), length)\n    }\n\n    private val isDrawerOpen\n        get() = mNavView?.isVisible == true || mRightDrawer?.isVisible == true\n\n    /**\n     * If activity is running, show snack bar, otherwise show toast\n     */\n    fun showTip(message: CharSequence, length: Int) {\n        if (mSnackBar != null && !isDrawerOpen) {\n            Snackbar.make(\n                mSnackBar!!,\n                message,\n                if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT,\n            ).show()\n        } else {\n            Toast.makeText(\n                this,\n                message,\n                if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,\n            ).show()\n        }\n    }\n\n    @Deprecated(\"Deprecated in Java\")\n    override fun onBackPressed() {\n        if (isDrawerOpen) {\n            mDrawerLayout!!.closeDrawers()\n        } else {\n            @Suppress(\"DEPRECATION\")\n            super.onBackPressed()\n        }\n    }\n\n    override fun onNavigationItemSelected(item: MenuItem): Boolean {\n        // Don't select twice\n        if (item.isChecked) {\n            return false\n        }\n        val id = item.itemId\n        when (id) {\n            R.id.nav_homepage -> {\n                val args = Bundle()\n                args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_HOMEPAGE)\n                startSceneFirstly(\n                    Announcer(GalleryListScene::class.java)\n                        .setArgs(args),\n                )\n            }\n            R.id.nav_subscription -> {\n                val args = Bundle()\n                args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_SUBSCRIPTION)\n                startSceneFirstly(\n                    Announcer(GalleryListScene::class.java)\n                        .setArgs(args),\n                )\n            }\n            R.id.nav_whats_hot -> {\n                val args = Bundle()\n                args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_WHATS_HOT)\n                startSceneFirstly(\n                    Announcer(GalleryListScene::class.java)\n                        .setArgs(args),\n                )\n            }\n            R.id.nav_toplist -> {\n                val args = Bundle()\n                args.putString(GalleryListScene.KEY_ACTION, GalleryListScene.ACTION_TOP_LIST)\n                startSceneFirstly(\n                    Announcer(GalleryListScene::class.java)\n                        .setArgs(args),\n                )\n            }\n            R.id.nav_favourite -> {\n                startScene(Announcer(FavoritesScene::class.java))\n            }\n            R.id.nav_history -> {\n                startScene(Announcer(HistoryScene::class.java))\n            }\n            R.id.nav_downloads -> {\n                startScene(Announcer(DownloadsScene::class.java))\n            }\n            R.id.nav_settings -> {\n                val intent = Intent(this, SettingsActivity::class.java)\n                settingsLauncher.launch(intent)\n            }\n        }\n        if (id != R.id.nav_stub) {\n            mDrawerLayout?.closeDrawers()\n        }\n        return true\n    }\n\n    companion object {\n        private const val KEY_NAV_CHECKED_ITEM = \"nav_checked_item\"\n\n        init {\n            registerLaunchMode(SecurityScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)\n            registerLaunchMode(SignInScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)\n            registerLaunchMode(WebViewSignInScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)\n            registerLaunchMode(CookieSignInScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)\n            registerLaunchMode(SelectSiteScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)\n            registerLaunchMode(GalleryListScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TOP)\n            registerLaunchMode(GalleryDetailScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)\n            registerLaunchMode(GalleryInfoScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)\n            registerLaunchMode(GalleryCommentsScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)\n            registerLaunchMode(GalleryPreviewsScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)\n            registerLaunchMode(DownloadsScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)\n            registerLaunchMode(FavoritesScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TASK)\n            registerLaunchMode(HistoryScene::class.java, SceneFragment.LAUNCH_MODE_SINGLE_TOP)\n            registerLaunchMode(ProgressScene::class.java, SceneFragment.LAUNCH_MODE_STANDARD)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/SettingsActivity.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui\n\nimport android.os.Bundle\nimport android.view.MenuItem\nimport androidx.annotation.StringRes\nimport androidx.fragment.app.FragmentTransaction\nimport com.google.android.material.snackbar.Snackbar\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.ui.fragment.SettingsFragment\nimport com.hippo.ehviewer.ui.scene.BaseScene\n\nclass SettingsActivity : EhActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_preference)\n        setSupportActionBar(findViewById(R.id.toolbar))\n        val bar = supportActionBar\n        bar?.setDisplayHomeAsUpEnabled(true)\n        if (savedInstanceState == null) {\n            supportFragmentManager\n                .beginTransaction()\n                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN)\n                .replace(R.id.fragment, SettingsFragment())\n                .commitAllowingStateLoss()\n        }\n    }\n\n    fun showTip(@StringRes id: Int, length: Int) {\n        showTip(getString(id), length)\n    }\n\n    fun showTip(message: CharSequence?, length: Int) {\n        Snackbar.make(\n            findViewById(R.id.snackbar),\n            message!!,\n            if (length == BaseScene.LENGTH_LONG) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT,\n        ).show()\n    }\n\n    @Suppress(\"DEPRECATION\")\n    override fun onOptionsItemSelected(item: MenuItem): Boolean {\n        if (item.itemId == android.R.id.home) {\n            onBackPressed()\n            return true\n        }\n        return super.onOptionsItemSelected(item)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/WebViewActivity.kt",
    "content": "package com.hippo.ehviewer.ui\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Bundle\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.util.setDefaultSettings\n\nclass WebViewActivity : EhActivity() {\n    private var webView: WebView? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val url = intent.extras?.getString(KEY_URL) ?: return\n        webView = WebView(applicationContext).apply {\n            setDefaultSettings()\n            webViewClient = object : WebViewClient() {\n                override fun onPageFinished(view: WebView, url: String) {\n                    val cloudflareBypassed = EhCookieStore.saveFromWebView(url) {\n                        it.name == EhCookieStore.KEY_CLOUDFLARE\n                    }\n                    if (cloudflareBypassed) {\n                        finish()\n                    }\n                }\n            }\n        }\n        setContentView(webView)\n        EhCookieStore.loadForWebView(url) {\n            it.name != EhCookieStore.KEY_CLOUDFLARE\n        }\n        webView!!.loadUrl(url)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        webView?.destroy()\n        webView = null\n    }\n\n    companion object {\n        const val KEY_URL = \"url\"\n\n        fun newIntent(context: Context, url: String): Intent = Intent(context, WebViewActivity::class.java).apply {\n            putExtra(KEY_URL, url)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/dialog/SelectItemWithIconAdapter.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer.ui.dialog\n\nimport android.content.Context\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.BaseAdapter\nimport android.widget.TextView\nimport androidx.appcompat.content.res.AppCompatResources\nimport com.hippo.ehviewer.R\n\nclass SelectItemWithIconAdapter(\n    private val context: Context,\n    private val texts: Array<CharSequence>,\n    private val icons: IntArray,\n) : BaseAdapter() {\n    private val inflater: LayoutInflater\n\n    init {\n        require(texts.size == icons.size) { \"Length conflict\" }\n        inflater = LayoutInflater.from(context)\n    }\n\n    override fun getCount(): Int = texts.size\n\n    override fun getItem(position: Int): Any = texts[position]\n\n    override fun getItemId(position: Int): Long = position.toLong()\n\n    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {\n        val mConvertView = convertView ?: inflater.inflate(R.layout.dialog_item_select_with_icon, parent, false)\n        val view = mConvertView as TextView\n        view.text = texts[position]\n        val icon = AppCompatResources.getDrawable(context, icons[position])\n        icon!!.setBounds(0, 0, icon.intrinsicWidth, icon.intrinsicHeight)\n        view.setCompoundDrawables(icon, null, null, null)\n        return view\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/AboutFragment.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.fragment\n\nimport android.os.Bundle\nimport androidx.annotation.StringRes\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.R\nimport com.hippo.util.loadHtml\n\nclass AboutFragment : BasePreferenceFragment() {\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        addPreferencesFromResource(R.xml.about_settings)\n        val author = findPreference<Preference>(KEY_AUTHOR)\n        author!!.summary = loadHtml(getString(R.string.settings_about_author_summary).replace('$', '@'))\n    }\n\n    @get:StringRes\n    override val fragmentTitle: Int\n        get() = R.string.settings_about\n\n    companion object {\n        private const val KEY_AUTHOR = \"author\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/AdvancedFragment.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.fragment\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport android.provider.Settings\nimport android.util.Log\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.core.net.toUri\nimport androidx.core.os.LocaleListCompat\nimport androidx.lifecycle.coroutineScope\nimport androidx.lifecycle.lifecycleScope\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.AppConfig\nimport com.hippo.ehviewer.BuildConfig\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.GetText\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings as AppSettings\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.ehviewer.client.data.FavListUrlBuilder\nimport com.hippo.ehviewer.client.parser.FavoritesParser\nimport com.hippo.ehviewer.ui.scene.BaseScene\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.LogCat\nimport com.hippo.util.ReadableTime\nimport com.hippo.util.isAtLeastS\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport com.hippo.yorozuya.IOUtils\nimport java.io.BufferedInputStream\nimport java.io.BufferedOutputStream\nimport java.io.File\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.util.zip.ZipEntry\nimport java.util.zip.ZipOutputStream\nimport kotlin.math.ceil\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\n\n@Suppress(\"BlockingMethodInNonBlockingContext\")\nclass AdvancedFragment : BasePreferenceFragment() {\n    private var exportLauncher = registerForActivityResult<String, Uri>(\n        ActivityResultContracts.CreateDocument(\"application/octet-stream\"),\n    ) { uri: Uri? ->\n        if (uri != null) {\n            try {\n                // grantUriPermission might throw RemoteException on MIUI\n                requireActivity().grantUriPermission(\n                    BuildConfig.APPLICATION_ID,\n                    uri,\n                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION,\n                )\n            } catch (e: Exception) {\n                ExceptionUtils.throwIfFatal(e)\n                e.printStackTrace()\n            }\n            try {\n                val alertDialog = AlertDialog.Builder(requireActivity())\n                    .setCancelable(false)\n                    .setView(R.layout.preference_dialog_task)\n                    .show()\n                lifecycleScope.launchIO {\n                    val success = EhDB.exportDB(requireActivity(), uri)\n                    withUIContext {\n                        if (alertDialog.isShowing) {\n                            alertDialog.dismiss()\n                        }\n                        showTip(\n                            if (success) {\n                                GetText.getString(\n                                    R.string.settings_advanced_export_data_to,\n                                    uri.toString(),\n                                )\n                            } else {\n                                GetText.getString(R.string.settings_advanced_export_data_failed)\n                            },\n                            BaseScene.LENGTH_SHORT,\n                        )\n                    }\n                }\n            } catch (_: Exception) {\n                showTip(R.string.settings_advanced_export_data_failed, BaseScene.LENGTH_SHORT)\n            }\n        }\n    }\n    private var dumpLogcatLauncher = registerForActivityResult<String, Uri>(\n        ActivityResultContracts.CreateDocument(\"application/zip\"),\n    ) { uri: Uri? ->\n        if (uri != null) {\n            try {\n                // grantUriPermission might throw RemoteException on MIUI\n                requireActivity().grantUriPermission(\n                    BuildConfig.APPLICATION_ID,\n                    uri,\n                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION,\n                )\n            } catch (e: Exception) {\n                ExceptionUtils.throwIfFatal(e)\n                e.printStackTrace()\n            }\n            try {\n                val zipFile = File(AppConfig.getExternalTempDir(), \"logs.zip\")\n                if (zipFile.exists()) {\n                    zipFile.delete()\n                }\n                val files = ArrayList<File>()\n                AppConfig.getExternalParseErrorDir()?.listFiles()?.let { files.addAll(it) }\n                AppConfig.getExternalCrashDir()?.listFiles()?.let { files.addAll(it) }\n                var finished = false\n                var origin: BufferedInputStream? = null\n                var out: ZipOutputStream? = null\n                try {\n                    val dest = FileOutputStream(zipFile)\n                    out = ZipOutputStream(BufferedOutputStream(dest))\n                    val bytes = ByteArray(1024 * 64)\n                    for (file in files) {\n                        if (!file.isFile) {\n                            continue\n                        }\n                        try {\n                            val fi = FileInputStream(file)\n                            origin = BufferedInputStream(fi, bytes.size)\n                            val entry = ZipEntry(file.name)\n                            out.putNextEntry(entry)\n                            var count: Int\n                            while (origin.read(bytes, 0, bytes.size).also { count = it } != -1) {\n                                out.write(bytes, 0, count)\n                            }\n                            origin.close()\n                            origin = null\n                        } catch (e: Exception) {\n                            e.printStackTrace()\n                        }\n                    }\n                    val entry =\n                        ZipEntry(\"logcat-\" + ReadableTime.getFilenamableTime() + \".txt\")\n                    out.putNextEntry(entry)\n                    LogCat.save(out)\n                    out.closeEntry()\n                    out.close()\n                    IOUtils.copy(\n                        FileInputStream(zipFile),\n                        requireActivity().contentResolver.openOutputStream(uri),\n                    )\n                    finished = true\n                } catch (e: Exception) {\n                    e.printStackTrace()\n                } finally {\n                    origin?.close()\n                    out?.close()\n                }\n                if (!finished) {\n                    finished = LogCat.save(requireActivity().contentResolver.openOutputStream(uri))\n                }\n                showTip(\n                    if (finished) {\n                        getString(\n                            R.string.settings_advanced_dump_logcat_to,\n                            uri.toString(),\n                        )\n                    } else {\n                        getString(R.string.settings_advanced_dump_logcat_failed)\n                    },\n                    BaseScene.LENGTH_SHORT,\n                )\n            } catch (_: Exception) {\n                showTip(\n                    getString(R.string.settings_advanced_dump_logcat_failed),\n                    BaseScene.LENGTH_SHORT,\n                )\n            }\n        }\n    }\n    private var importDataLauncher = registerForActivityResult<Array<String>, Uri>(\n        ActivityResultContracts.OpenDocument(),\n    ) { uri: Uri? ->\n        if (uri != null) {\n            try {\n                // grantUriPermission might throw RemoteException on MIUI\n                requireActivity().grantUriPermission(\n                    BuildConfig.APPLICATION_ID,\n                    uri,\n                    Intent.FLAG_GRANT_READ_URI_PERMISSION,\n                )\n            } catch (e: Exception) {\n                ExceptionUtils.throwIfFatal(e)\n                e.printStackTrace()\n            }\n            try {\n                val alertDialog = AlertDialog.Builder(requireActivity())\n                    .setCancelable(false)\n                    .setView(R.layout.preference_dialog_task)\n                    .show()\n                lifecycleScope.launchIO {\n                    val error = EhDB.importDB(requireActivity(), uri)\n                    withUIContext {\n                        if (alertDialog.isShowing) {\n                            alertDialog.dismiss()\n                        }\n                        if (null == error) {\n                            showTip(\n                                getString(R.string.settings_advanced_import_data_successfully),\n                                BaseScene.LENGTH_SHORT,\n                            )\n                        } else {\n                            showTip(error, BaseScene.LENGTH_SHORT)\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                showTip(e.localizedMessage, BaseScene.LENGTH_SHORT)\n            }\n        }\n    }\n    private var favTotal = 0\n    private var favIndex = 0\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        addPreferencesFromResource(R.xml.advanced_settings)\n        val dumpLogcat = findPreference<Preference>(KEY_DUMP_LOGCAT)\n        val appLanguage = findPreference<Preference>(AppSettings.KEY_APP_LANGUAGE)\n        val importData = findPreference<Preference>(KEY_IMPORT_DATA)\n        val exportData = findPreference<Preference>(KEY_EXPORT_DATA)\n        val backupFavorite = findPreference<Preference>(KEY_BACKUP_FAVORITE)\n        val openByDefault = findPreference<Preference>(KEY_OPEN_BY_DEFAULT)\n        if (isAtLeastS) {\n            openByDefault!!.onPreferenceClickListener = this\n        } else {\n            openByDefault!!.isVisible = false\n        }\n        dumpLogcat!!.onPreferenceClickListener = this\n        importData!!.onPreferenceClickListener = this\n        exportData!!.onPreferenceClickListener = this\n        backupFavorite!!.onPreferenceClickListener = this\n        appLanguage!!.onPreferenceChangeListener = this\n    }\n\n    override fun onPreferenceClick(preference: Preference): Boolean {\n        val key = preference.key\n        if (KEY_DUMP_LOGCAT == key) {\n            try {\n                dumpLogcatLauncher.launch(\"log-\" + ReadableTime.getFilenamableTime() + \".zip\")\n            } catch (e: Throwable) {\n                ExceptionUtils.throwIfFatal(e)\n                showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT)\n            }\n            return true\n        } else if (KEY_IMPORT_DATA == key) {\n            try {\n                importDataLauncher.launch(arrayOf(\"*/*\"))\n            } catch (e: Throwable) {\n                ExceptionUtils.throwIfFatal(e)\n                showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT)\n            }\n            return true\n        } else if (KEY_EXPORT_DATA == key) {\n            try {\n                exportLauncher.launch(ReadableTime.getFilenamableTime() + \".db\")\n            } catch (e: Throwable) {\n                ExceptionUtils.throwIfFatal(e)\n                showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT)\n            }\n            return true\n        } else if (KEY_BACKUP_FAVORITE == key) {\n            try {\n                backupFavorite()\n            } catch (e: Exception) {\n                ExceptionUtils.throwIfFatal(e)\n                showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT)\n            }\n            return true\n        } else if (KEY_OPEN_BY_DEFAULT == key) {\n            try {\n                @SuppressLint(\"InlinedApi\")\n                val intent = Intent(\n                    Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,\n                    \"package:${requireContext().packageName}\".toUri(),\n                )\n                startActivity(intent)\n            } catch (_: Throwable) {\n                val intent = Intent(\n                    Settings.ACTION_APPLICATION_DETAILS_SETTINGS,\n                    \"package:${requireContext().packageName}\".toUri(),\n                )\n                startActivity(intent)\n            }\n            return true\n        }\n        return false\n    }\n\n    private fun backupFavorite() {\n        val mClient = EhClient\n        val favListUrlBuilder = FavListUrlBuilder()\n        favTotal = 0\n        favIndex = 1\n        val request = EhRequest()\n        request.setMethod(EhClient.METHOD_GET_FAVORITES)\n        request.setCallback(object : EhClient.Callback<FavoritesParser.Result> {\n            override fun onSuccess(result: FavoritesParser.Result) {\n                try {\n                    if (result.galleryInfoList.isEmpty()) {\n                        showTip(\n                            R.string.settings_advanced_backup_favorite_nothing,\n                            BaseScene.LENGTH_SHORT,\n                        )\n                    } else {\n                        if (favTotal == 0) {\n                            var totalFav = 0\n                            for (i in 0..9) {\n                                totalFav += result.countArray[i]\n                            }\n                            favTotal =\n                                ceil(totalFav.toDouble() / result.galleryInfoList.size).toInt()\n                        }\n                        val status = \"($favIndex/$favTotal)\"\n                        showTip(\n                            GetText.getString(\n                                R.string.settings_advanced_backup_favorite_start,\n                                status,\n                            ),\n                            BaseScene.LENGTH_SHORT,\n                        )\n                        Log.d(\"LocalFavorites\", \"now backup page $status\")\n                        EhDB.putLocalFavorites(result.galleryInfoList)\n                        if (result.next != null) {\n                            try {\n                                runBlocking {\n                                    delay(AppSettings.downloadDelay.toLong())\n                                }\n                            } catch (e: InterruptedException) {\n                                e.printStackTrace()\n                            }\n                            favIndex++\n                            favListUrlBuilder.setIndex(result.next, true)\n                            request.setArgs(favListUrlBuilder.build())\n                            viewLifecycleOwner.lifecycleScope.launch {\n                                delay(100)\n                                request.enqueue(this@AdvancedFragment)\n                            }\n                        } else {\n                            showTip(\n                                R.string.settings_advanced_backup_favorite_success,\n                                BaseScene.LENGTH_SHORT,\n                            )\n                        }\n                    }\n                } catch (_: Exception) {\n                    showTip(\n                        R.string.settings_advanced_backup_favorite_failed,\n                        BaseScene.LENGTH_SHORT,\n                    )\n                }\n            }\n\n            override fun onFailure(e: Exception) {\n                showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT)\n            }\n\n            override fun onCancel() {\n                showTip(R.string.settings_advanced_backup_favorite_failed, BaseScene.LENGTH_SHORT)\n            }\n        })\n        request.setArgs(favListUrlBuilder.build())\n        mClient.enqueue(request, viewLifecycleOwner.lifecycle.coroutineScope)\n    }\n\n    override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {\n        val key = preference.key\n        if (AppSettings.KEY_APP_LANGUAGE == key) {\n            if (\"system\" == newValue) {\n                AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())\n            } else {\n                AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue as String))\n            }\n            return true\n        }\n        return false\n    }\n\n    override val fragmentTitle: Int\n        get() = R.string.settings_advanced\n\n    companion object {\n        private const val KEY_DUMP_LOGCAT = \"dump_logcat\"\n        private const val KEY_IMPORT_DATA = \"import_data\"\n        private const val KEY_EXPORT_DATA = \"export_data\"\n        private const val KEY_OPEN_BY_DEFAULT = \"open_by_default\"\n        private const val KEY_BACKUP_FAVORITE = \"backup_favorite\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/BaseFragment.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport androidx.annotation.StringRes\nimport androidx.fragment.app.Fragment\nimport com.hippo.ehviewer.ui.SettingsActivity\n\nabstract class BaseFragment : Fragment() {\n    override fun onStart() {\n        super.onStart()\n        setTitle(getFragmentTitle())\n    }\n\n    abstract fun getFragmentTitle(): Int\n\n    private fun setTitle(@StringRes string: Int) {\n        requireActivity().setTitle(string)\n    }\n\n    fun showTip(@StringRes id: Int, length: Int) {\n        (requireActivity() as SettingsActivity).showTip(getString(id), length)\n    }\n\n    fun showTip(message: CharSequence?, length: Int) {\n        (requireActivity() as SettingsActivity).showTip(message, length)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/BasePreferenceFragment.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport android.os.Bundle\nimport androidx.annotation.StringRes\nimport androidx.fragment.app.FragmentTransaction\nimport androidx.preference.Preference\nimport androidx.preference.PreferenceFragmentCompat\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.ui.SettingsActivity\n\nopen class BasePreferenceFragment :\n    PreferenceFragmentCompat(),\n    Preference.OnPreferenceClickListener,\n    Preference.OnPreferenceChangeListener {\n    override fun onStart() {\n        super.onStart()\n        setTitle(fragmentTitle)\n    }\n\n    @get:StringRes\n    open val fragmentTitle: Int\n        get() = -1\n\n    override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean = false\n\n    override fun onPreferenceClick(preference: Preference): Boolean = false\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {}\n\n    override fun onPreferenceTreeClick(preference: Preference): Boolean {\n        val fragment = when (preference.key) {\n            \"eh\" -> EhFragment()\n            \"read\" -> ReadFragment()\n            \"download\" -> DownloadFragment()\n            \"privacy\" -> PrivacyFragment()\n            \"advanced\" -> AdvancedFragment()\n            \"about\" -> AboutFragment()\n            \"uconfig\" -> UConfigFragment()\n            \"mytags\" -> MyTagsFragment()\n            \"filter\" -> FilterFragment()\n            \"security\" -> SetSecurityFragment()\n            else -> null\n        }\n        fragment?.let {\n            requireActivity().supportFragmentManager\n                .beginTransaction()\n                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)\n                .replace(R.id.fragment, it)\n                .addToBackStack(null)\n                .commitAllowingStateLoss()\n        }\n        return true\n    }\n\n    private fun setTitle(@StringRes string: Int) {\n        requireActivity().setTitle(string)\n    }\n\n    fun showTip(@StringRes id: Int, length: Int) {\n        (requireActivity() as SettingsActivity).showTip(getString(id), length)\n    }\n\n    fun showTip(message: CharSequence?, length: Int) {\n        (requireActivity() as SettingsActivity).showTip(message, length)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/DownloadFragment.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.fragment\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.lifecycleScope\nimport androidx.preference.ListPreference\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.AppConfig\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.ui.keepNoMediaFileStatus\nimport com.hippo.ehviewer.ui.scene.BaseScene\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.launchNonCancellable\n\nclass DownloadFragment : BasePreferenceFragment() {\n    private var mDownloadLocation: Preference? = null\n    private var pickImageDirLauncher = registerForActivityResult<Uri?, Uri>(\n        ActivityResultContracts.OpenDocumentTree(),\n    ) { treeUri: Uri? ->\n        if (treeUri != null) {\n            requireActivity().contentResolver.takePersistableUriPermission(\n                treeUri,\n                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION,\n            )\n            val uniFile = UniFile.fromTreeUri(requireActivity(), treeUri)\n            if (uniFile != null) {\n                Settings.putDownloadLocation(uniFile)\n                lifecycleScope.launchNonCancellable {\n                    keepNoMediaFileStatus()\n                }\n                onUpdateDownloadLocation()\n            } else {\n                showTip(\n                    R.string.settings_download_cant_get_download_location,\n                    BaseScene.LENGTH_SHORT,\n                )\n            }\n        }\n    }\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        addPreferencesFromResource(R.xml.download_settings)\n        mDownloadLocation = findPreference(Settings.KEY_DOWNLOAD_LOCATION)\n        val mediaScan = findPreference<Preference>(Settings.KEY_MEDIA_SCAN)\n        val multiThreadDownload = findPreference<Preference>(Settings.KEY_MULTI_THREAD_DOWNLOAD)\n        val downloadDelay = findPreference<Preference>(Settings.KEY_DOWNLOAD_DELAY)\n        val preloadImage = findPreference<Preference>(Settings.KEY_PRELOAD_IMAGE)\n        val downloadOriginImage = findPreference<Preference>(Settings.KEY_DOWNLOAD_ORIGIN_IMAGE)\n        mDownloadLocation?.onPreferenceClickListener = this\n        mediaScan!!.onPreferenceChangeListener = this\n        multiThreadDownload!!.setSummaryProvider {\n            getString(R.string.settings_download_concurrency_summary, (it as ListPreference).entry)\n        }\n        downloadDelay!!.setSummaryProvider {\n            getString(R.string.settings_download_download_delay_summary, (it as ListPreference).entry)\n        }\n        preloadImage!!.setSummaryProvider {\n            getString(R.string.settings_download_preload_image_summary, (it as ListPreference).entry)\n        }\n        downloadOriginImage!!.setSummaryProvider {\n            getString(R.string.settings_download_download_origin_image_summary, (it as ListPreference).entry)\n        }\n        onUpdateDownloadLocation()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mDownloadLocation = null\n    }\n\n    private fun onUpdateDownloadLocation() {\n        val file = Settings.downloadLocation\n        if (mDownloadLocation != null) {\n            if (file != null) {\n                mDownloadLocation!!.summary = file.uri.toString()\n            } else {\n                mDownloadLocation!!.setSummary(R.string.settings_download_invalid_download_location)\n            }\n        }\n    }\n\n    override fun onPreferenceClick(preference: Preference): Boolean {\n        val key = preference.key\n        if (Settings.KEY_DOWNLOAD_LOCATION == key) {\n            val file = Settings.downloadLocation\n            if (file != null &&\n                !UniFile.isFileUri(Settings.downloadLocation!!.uri)\n            ) {\n                AlertDialog.Builder(requireContext())\n                    .setTitle(R.string.settings_download_download_location)\n                    .setMessage(file.uri.toString())\n                    .setPositiveButton(R.string.settings_download_pick_new_location) { _, _ -> openDirPickerL() }\n                    .setNeutralButton(R.string.settings_download_reset_location) { _, _ ->\n                        val uniFile = UniFile.fromFile(AppConfig.getDefaultDownloadDir())\n                        if (uniFile != null) {\n                            Settings.putDownloadLocation(uniFile)\n                            lifecycleScope.launchNonCancellable {\n                                keepNoMediaFileStatus()\n                            }\n                            onUpdateDownloadLocation()\n                        } else {\n                            showTip(\n                                R.string.settings_download_cant_get_download_location,\n                                BaseScene.LENGTH_SHORT,\n                            )\n                        }\n                    }\n                    .show()\n            } else {\n                openDirPickerL()\n            }\n            return true\n        }\n        return false\n    }\n\n    private fun openDirPickerL() {\n        try {\n            pickImageDirLauncher.launch(null)\n        } catch (e: Throwable) {\n            ExceptionUtils.throwIfFatal(e)\n            showTip(R.string.error_cant_find_activity, BaseScene.LENGTH_SHORT)\n        }\n    }\n\n    override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {\n        val key = preference.key\n        if (Settings.KEY_MEDIA_SCAN == key) {\n            if (newValue is Boolean) {\n                lifecycleScope.launchNonCancellable {\n                    keepNoMediaFileStatus()\n                }\n            }\n            return true\n        }\n        return false\n    }\n\n    override val fragmentTitle: Int\n        get() = R.string.settings_download\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/EhFragment.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.fragment\n\nimport android.app.Activity\nimport android.content.res.Configuration\nimport android.os.Bundle\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AppCompatDelegate\nimport androidx.lifecycle.lifecycleScope\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhEngine\nimport com.hippo.ehviewer.client.EhTagDatabase\nimport com.hippo.util.launchNonCancellable\n\nclass EhFragment : BasePreferenceFragment() {\n    private lateinit var detailSize: Preference\n    private lateinit var listThumbSize: Preference\n    private lateinit var thumbSize: Preference\n    private lateinit var thumbShowTitle: Preference\n\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        addPreferencesFromResource(R.xml.eh_settings)\n        val account = findPreference<Preference>(Settings.KEY_ACCOUNT)\n        val gallerySite = findPreference<Preference>(Settings.KEY_GALLERY_SITE)\n        val theme = findPreference<Preference>(Settings.KEY_THEME)\n        val blackDarkTheme = findPreference<Preference>(Settings.KEY_BLACK_DARK_THEME)\n        val listMode = findPreference<Preference>(Settings.KEY_LIST_MODE)\n        val showTagTranslations = findPreference<Preference>(Settings.KEY_SHOW_TAG_TRANSLATIONS)\n        val tagTranslationsSource = findPreference<Preference>(Settings.KEY_TAG_TRANSLATIONS_SOURCE)\n        detailSize = findPreference(Settings.KEY_DETAIL_SIZE)!!\n        listThumbSize = findPreference(Settings.KEY_LIST_THUMB_SIZE)!!\n        thumbSize = findPreference(Settings.KEY_THUMB_SIZE)!!\n        thumbShowTitle = findPreference(Settings.KEY_THUMB_SHOW_TITLE)!!\n\n        gallerySite!!.onPreferenceChangeListener = this\n        theme!!.onPreferenceChangeListener = this\n        blackDarkTheme!!.onPreferenceChangeListener = this\n        listMode!!.onPreferenceChangeListener = this\n        showTagTranslations!!.onPreferenceChangeListener = this\n        detailSize.onPreferenceChangeListener = this\n        listThumbSize.onPreferenceChangeListener = this\n        thumbSize.onPreferenceChangeListener = this\n        thumbShowTitle.onPreferenceChangeListener = this\n        Settings.displayName?.let { account?.summary = it }\n        if (!EhTagDatabase.isTranslatable(requireActivity())) {\n            if (!Settings.showTagTranslations) {\n                preferenceScreen.removePreference(showTagTranslations)\n            }\n            preferenceScreen.removePreference(tagTranslationsSource!!)\n        }\n        if (!EhCookieStore.hasSignedIn()) {\n            Settings.SIGN_IN_REQUIRED.forEach {\n                val preference = findPreference<Preference>(it)\n                preferenceScreen.removePreference(preference!!)\n            }\n        }\n        updateListPreference(Settings.listMode)\n    }\n\n    override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {\n        val key = preference.key\n        if (Settings.KEY_THEME == key) {\n            AppCompatDelegate.setDefaultNightMode((newValue as String).toInt())\n            requireActivity().recreate()\n        } else if (Settings.KEY_BLACK_DARK_THEME == key) {\n            if (requireActivity().resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES > 0) {\n                EhApplication.application.recreateAllActivity()\n            }\n        } else if (Settings.KEY_GALLERY_SITE == key) {\n            requireActivity().setResult(Activity.RESULT_OK)\n            lifecycleScope.launchNonCancellable {\n                runCatching {\n                    EhEngine.getUConfig()\n                }.onFailure {\n                    it.printStackTrace()\n                }\n            }\n        } else if (Settings.KEY_LIST_MODE == key) {\n            updateListPreference((newValue as String).toInt())\n            requireActivity().setResult(Activity.RESULT_OK)\n        } else if (Settings.KEY_DETAIL_SIZE == key) {\n            requireActivity().setResult(Activity.RESULT_OK)\n        } else if (Settings.KEY_LIST_THUMB_SIZE == key) {\n            requireActivity().setResult(Activity.RESULT_OK)\n        } else if (Settings.KEY_THUMB_SIZE == key) {\n            requireActivity().setResult(Activity.RESULT_OK)\n        } else if (Settings.KEY_THUMB_SHOW_TITLE == key) {\n            requireActivity().setResult(Activity.RESULT_OK)\n        } else if (Settings.KEY_SHOW_TAG_TRANSLATIONS == key) {\n            if (java.lang.Boolean.TRUE == newValue) {\n                lifecycleScope.launchNonCancellable { EhTagDatabase.update(true) }\n            }\n        }\n        return true\n    }\n\n    @get:StringRes\n    override val fragmentTitle: Int\n        get() = R.string.settings_eh\n\n    private fun updateListPreference(newValue: Int) {\n        val isDetailMode = newValue == 0\n        detailSize.isVisible = isDetailMode\n        listThumbSize.isVisible = isDetailMode\n        thumbSize.isVisible = !isDetailMode\n        thumbShowTitle.isVisible = !isDetailMode\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/FilterFragment.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport android.annotation.SuppressLint\nimport android.content.DialogInterface\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.EditText\nimport android.widget.ImageView\nimport android.widget.Spinner\nimport android.widget.TextView\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.MenuProvider\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.checkbox.MaterialCheckBox\nimport com.google.android.material.floatingactionbutton.FloatingActionButton\nimport com.google.android.material.textfield.TextInputLayout\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.LinearDividerItemDecoration\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhFilter\nimport com.hippo.ehviewer.dao.Filter\nimport com.hippo.view.ViewTransition\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.ViewUtils\nimport rikka.core.res.resolveColor\n\nclass FilterFragment : BaseFragment() {\n    private var mViewTransition: ViewTransition? = null\n    private var mAdapter: FilterAdapter = FilterAdapter()\n    private var mFilterList: FilterList = FilterList()\n    private val mMenuProvider: MenuProvider = FilterMenuProvider()\n\n    inner class FilterMenuProvider : MenuProvider {\n        override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {\n            menuInflater.inflate(R.menu.activity_filter, menu)\n        }\n\n        override fun onMenuItemSelected(menuItem: MenuItem): Boolean {\n            val itemId = menuItem.itemId\n            if (itemId == R.id.action_tip) {\n                showTipDialog()\n                return true\n            }\n            return false\n        }\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? {\n        val view = inflater.inflate(R.layout.activity_filter, container, false)\n        val recyclerView: RecyclerView =\n            ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView\n        val tip = ViewUtils.`$$`(view, R.id.tip) as TextView\n        mViewTransition = ViewTransition(recyclerView, tip)\n        val fab = view.findViewById<FloatingActionButton>(R.id.fab)\n        val drawable = ContextCompat.getDrawable(requireActivity(), R.drawable.big_filter)\n        drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n        tip.setCompoundDrawables(null, drawable, null, null)\n        mAdapter.setHasStableIds(true)\n        recyclerView.adapter = mAdapter\n        recyclerView.clipToPadding = false\n        recyclerView.clipChildren = false\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            requireActivity().theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(requireActivity(), 1f),\n        )\n        decoration.setShowLastDivider(true)\n        recyclerView.addItemDecoration(decoration)\n        recyclerView.layoutManager = LinearLayoutManager(requireContext())\n        recyclerView.setHasFixedSize(true)\n        val defaultItemAnimator = recyclerView.itemAnimator as DefaultItemAnimator?\n        defaultItemAnimator?.supportsChangeAnimations = false\n        fab.setOnClickListener { showAddFilterDialog() }\n        return view\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        updateView(false)\n        requireActivity().addMenuProvider(mMenuProvider)\n    }\n\n    private fun updateView(animation: Boolean) {\n        if (null == mViewTransition) {\n            return\n        }\n        if (0 == mFilterList.size()) {\n            mViewTransition!!.showView(1, animation)\n        } else {\n            mViewTransition!!.showView(0, animation)\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mViewTransition = null\n        requireActivity().removeMenuProvider(mMenuProvider)\n    }\n\n    private fun showTipDialog() {\n        AlertDialog.Builder(requireActivity())\n            .setTitle(R.string.filter)\n            .setMessage(R.string.filter_tip)\n            .setPositiveButton(android.R.string.ok, null)\n            .show()\n    }\n\n    private fun showAddFilterDialog() {\n        val dialog = AlertDialog.Builder(requireActivity())\n            .setTitle(R.string.add_filter)\n            .setView(R.layout.dialog_add_filter)\n            .setPositiveButton(R.string.add, null)\n            .setNegativeButton(android.R.string.cancel, null)\n            .show()\n        AddFilterDialogHelper(dialog)\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    private fun showDeleteFilterDialog(filter: Filter) {\n        val message = getString(R.string.delete_filter, filter.text)\n        AlertDialog.Builder(requireActivity())\n            .setMessage(message)\n            .setPositiveButton(R.string.delete) { _: DialogInterface?, which: Int ->\n                if (DialogInterface.BUTTON_POSITIVE != which) {\n                    return@setPositiveButton\n                }\n                mFilterList.delete(filter)\n                mAdapter.notifyDataSetChanged()\n                updateView(true)\n            }\n            .setNegativeButton(android.R.string.cancel, null)\n            .show()\n    }\n\n    override fun getFragmentTitle(): Int = R.string.filter\n\n    private class FilterHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        val checkbox: MaterialCheckBox? = itemView.findViewById(R.id.checkbox)\n        val text: TextView? = itemView.findViewById(R.id.text)\n        val delete: ImageView? = itemView.findViewById(R.id.delete)\n    }\n\n    private inner class AddFilterDialogHelper(dialog: AlertDialog) : View.OnClickListener {\n        private var mDialog: AlertDialog = dialog\n        private var mSpinner: Spinner = ViewUtils.`$$`(dialog, R.id.spinner) as Spinner\n        private var mInputLayout: TextInputLayout = ViewUtils.`$$`(dialog, R.id.text_input_layout) as TextInputLayout\n        private var mEditText: EditText = mInputLayout.editText!!\n\n        init {\n            val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)\n            button?.setOnClickListener(this)\n        }\n\n        @SuppressLint(\"NotifyDataSetChanged\")\n        override fun onClick(v: View) {\n            val text = mEditText.text.toString().trim { it <= ' ' }\n            if (TextUtils.isEmpty(text)) {\n                mInputLayout.error = getString(R.string.text_is_empty)\n                return\n            } else {\n                mInputLayout.error = null\n            }\n            val mode = mSpinner.selectedItemPosition\n            val filter = Filter()\n            filter.mode = mode\n            filter.text = text\n            if (!mFilterList.add(filter)) {\n                mInputLayout.error = getString(R.string.label_text_exist)\n                return\n            } else {\n                mInputLayout.error = null\n            }\n            mAdapter.notifyDataSetChanged()\n            updateView(true)\n            mDialog.dismiss()\n        }\n    }\n\n    private inner class FilterAdapter : RecyclerView.Adapter<FilterHolder>() {\n        override fun getItemViewType(position: Int): Int = if (mFilterList[position].mode == MODE_HEADER) {\n            TYPE_HEADER\n        } else {\n            TYPE_ITEM\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FilterHolder {\n            val layoutId: Int = when (viewType) {\n                TYPE_ITEM -> R.layout.item_filter\n                TYPE_HEADER -> R.layout.item_filter_header\n                else -> R.layout.item_filter\n            }\n            return FilterHolder(layoutInflater.inflate(layoutId, parent, false))\n        }\n\n        override fun onBindViewHolder(holder: FilterHolder, position: Int) {\n            val filter = mFilterList[position]\n            if (MODE_HEADER == filter.mode) {\n                holder.text?.text = filter.text\n            } else {\n                holder.checkbox?.text = if (Settings.showTagTranslations) EhFilter.applyTranslation(filter) else filter.text\n                holder.checkbox?.isChecked = filter.enable!!\n                holder.itemView.setOnClickListener {\n                    mFilterList.trigger(filter)\n\n                    // for updating delete line on filter text\n                    mAdapter.notifyItemChanged(position)\n                }\n                holder.delete?.setOnClickListener { showDeleteFilterDialog(filter) }\n            }\n        }\n\n        override fun getItemCount(): Int = mFilterList.size()\n\n        override fun getItemId(position: Int): Long = run {\n            val filter = mFilterList[position]\n            if (filter.id != null) {\n                (filter.text.hashCode() shr filter.mode) + filter.id!!\n            } else {\n                (filter.text.hashCode() shr filter.mode).toLong()\n            }\n        }\n    }\n\n    private inner class FilterList {\n        private val mEhFilter: EhFilter = EhFilter\n        private val mTitleFilterList: List<Filter> = mEhFilter.titleFilterList\n        private val mUploaderFilterList: List<Filter> = mEhFilter.uploaderFilterList\n        private val mTagFilterList: List<Filter> = mEhFilter.tagFilterList\n        private val mTagNamespaceFilterList: List<Filter> = mEhFilter.tagNamespaceFilterList\n        private val mCommenterFilterList: List<Filter> = mEhFilter.commenterFilterList\n        private val mCommentFilterList: List<Filter> = mEhFilter.commentFilterList\n        private var mTitleHeader: Filter? = null\n        private var mUploaderHeader: Filter? = null\n        private var mTagHeader: Filter? = null\n        private var mTagNamespaceHeader: Filter? = null\n        private var mCommenterHeader: Filter? = null\n        private var mCommentHeader: Filter? = null\n        fun size(): Int {\n            var count = 0\n            var size = mTitleFilterList.size\n            count += if (0 == size) 0 else size + 1\n            size = mUploaderFilterList.size\n            count += if (0 == size) 0 else size + 1\n            size = mTagFilterList.size\n            count += if (0 == size) 0 else size + 1\n            size = mTagNamespaceFilterList.size\n            count += if (0 == size) 0 else size + 1\n            size = mCommenterFilterList.size\n            count += if (0 == size) 0 else size + 1\n            size = mCommentFilterList.size\n            count += if (0 == size) 0 else size + 1\n            return count\n        }\n\n        private val titleHeader: Filter\n            get() {\n                if (null == mTitleHeader) {\n                    mTitleHeader = Filter()\n                    mTitleHeader!!.mode = MODE_HEADER\n                    mTitleHeader!!.text = getString(R.string.filter_title)\n                }\n                return mTitleHeader!!\n            }\n        private val uploaderHeader: Filter\n            get() {\n                if (null == mUploaderHeader) {\n                    mUploaderHeader = Filter()\n                    mUploaderHeader!!.mode = MODE_HEADER\n                    mUploaderHeader!!.text = getString(R.string.filter_uploader)\n                }\n                return mUploaderHeader!!\n            }\n        private val tagHeader: Filter\n            get() {\n                if (null == mTagHeader) {\n                    mTagHeader = Filter()\n                    mTagHeader!!.mode = MODE_HEADER\n                    mTagHeader!!.text = getString(R.string.filter_tag)\n                }\n                return mTagHeader!!\n            }\n        private val tagNamespaceHeader: Filter\n            get() {\n                if (null == mTagNamespaceHeader) {\n                    mTagNamespaceHeader = Filter()\n                    mTagNamespaceHeader!!.mode = MODE_HEADER\n                    mTagNamespaceHeader!!.text = getString(R.string.filter_tag_namespace)\n                }\n                return mTagNamespaceHeader!!\n            }\n        private val commenterHeader: Filter\n            get() {\n                if (null == mCommenterHeader) {\n                    mCommenterHeader = Filter()\n                    mCommenterHeader!!.mode = MODE_HEADER\n                    mCommenterHeader!!.text = getString(R.string.filter_commenter)\n                }\n                return mCommenterHeader!!\n            }\n        private val commentHeader: Filter\n            get() {\n                if (null == mCommentHeader) {\n                    mCommentHeader = Filter()\n                    mCommentHeader!!.mode = MODE_HEADER\n                    mCommentHeader!!.text = getString(R.string.filter_comment)\n                }\n                return mCommentHeader!!\n            }\n\n        operator fun get(index: Int): Filter {\n            var index1 = index\n            var size = mTitleFilterList.size\n            if (0 != size) {\n                index1 -= if (index1 == 0) {\n                    return titleHeader\n                } else if (index1 <= size) {\n                    return mTitleFilterList[index1 - 1]\n                } else {\n                    size + 1\n                }\n            }\n            size = mUploaderFilterList.size\n            if (0 != size) {\n                index1 -= if (index1 == 0) {\n                    return uploaderHeader\n                } else if (index1 <= size) {\n                    return mUploaderFilterList[index1 - 1]\n                } else {\n                    size + 1\n                }\n            }\n            size = mTagFilterList.size\n            if (0 != size) {\n                index1 -= if (index1 == 0) {\n                    return tagHeader\n                } else if (index1 <= size) {\n                    return mTagFilterList[index1 - 1]\n                } else {\n                    size + 1\n                }\n            }\n            size = mTagNamespaceFilterList.size\n            if (0 != size) {\n                index1 -= if (index1 == 0) {\n                    return tagNamespaceHeader\n                } else if (index1 <= size) {\n                    return mTagNamespaceFilterList[index1 - 1]\n                } else {\n                    size + 1\n                }\n            }\n            size = mCommenterFilterList.size\n            if (0 != size) {\n                index1 -= if (index1 == 0) {\n                    return commenterHeader\n                } else if (index1 <= size) {\n                    return mCommenterFilterList[index1 - 1]\n                } else {\n                    size + 1\n                }\n            }\n            size = mCommentFilterList.size\n            if (0 != size) {\n                if (index1 == 0) {\n                    return commentHeader\n                } else if (index1 <= size) {\n                    return mCommentFilterList[index1 - 1]\n                }\n            }\n            throw IndexOutOfBoundsException()\n        }\n\n        fun add(filter: Filter): Boolean = mEhFilter.addFilter(filter)\n\n        fun delete(filter: Filter) {\n            mEhFilter.deleteFilter(filter)\n        }\n\n        fun trigger(filter: Filter) {\n            mEhFilter.triggerFilter(filter)\n        }\n    }\n\n    companion object {\n        private const val MODE_HEADER = -1\n        private const val TYPE_ITEM = 0\n        private const val TYPE_HEADER = 1\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/MyTagsFragment.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport android.graphics.Bitmap\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.webkit.CookieManager\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport com.google.android.material.progressindicator.CircularProgressIndicator\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.util.setDefaultSettings\nimport com.hippo.ehviewer.widget.DialogWebChromeClient\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport rikka.core.res.resolveColor\n\nclass MyTagsFragment : BaseFragment() {\n    private val url = EhUrl.myTagsUrl\n    private var webView: WebView? = null\n    private var progress: CircularProgressIndicator? = null\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.activity_webview, container, false)\n        webView = view.findViewById(R.id.webview)\n        webView!!.run {\n            setBackgroundColor(requireActivity().theme.resolveColor(android.R.attr.colorBackground))\n            setDefaultSettings()\n            webViewClient = MyTagsWebViewClient()\n            webChromeClient = DialogWebChromeClient(requireContext())\n        }\n        progress = view.findViewById(R.id.progress)\n        return view\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        progress!!.visibility = View.VISIBLE\n        webView!!.loadUrl(url)\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        // http://stackoverflow.com/questions/32284642/how-to-handle-an-uncatched-exception\n        val cookieManager = CookieManager.getInstance()\n        cookieManager.flush()\n        cookieManager.removeAllCookies(null)\n        cookieManager.removeSessionCookies(null)\n\n        // Copy cookies from okhttp cookie store to CookieManager\n        for (cookie in EhCookieStore.getCookies(url.toHttpUrl())) {\n            cookieManager.setCookie(url, cookie.toString())\n        }\n    }\n\n    override fun getFragmentTitle(): Int = R.string.my_tags\n\n    private inner class MyTagsWebViewClient : WebViewClient() {\n        override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {\n            // Never load other urls\n            return !request.url.toString().startsWith(this@MyTagsFragment.url)\n        }\n\n        override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {\n            progress!!.visibility = View.VISIBLE\n        }\n\n        override fun onPageFinished(view: WebView, url: String) {\n            progress!!.visibility = View.GONE\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/PrivacyFragment.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport android.os.Bundle\nimport android.text.TextUtils\nimport androidx.annotation.StringRes\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\n\nclass PrivacyFragment : BasePreferenceFragment() {\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        addPreferencesFromResource(R.xml.privacy_settings)\n    }\n\n    override fun onStart() {\n        super.onStart()\n        val patternProtection = findPreference<Preference>(KEY_PATTERN_PROTECTION)\n        patternProtection!!.summary = if (TextUtils.isEmpty(Settings.security)) {\n            getString(R.string.settings_privacy_pattern_protection_not_set)\n        } else {\n            getString(R.string.settings_privacy_pattern_protection_set)\n        }\n    }\n\n    @get:StringRes\n    override val fragmentTitle: Int\n        get() = R.string.settings_privacy\n\n    companion object {\n        private const val KEY_PATTERN_PROTECTION = \"security\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/ReadFragment.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.fragment\n\nimport android.os.Bundle\nimport androidx.annotation.StringRes\nimport com.hippo.ehviewer.R\n\nclass ReadFragment : BasePreferenceFragment() {\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        addPreferencesFromResource(R.xml.read_settings)\n    }\n\n    @get:StringRes\n    override val fragmentTitle: Int\n        get() = R.string.settings_read\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/SetSecurityFragment.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.CheckBox\nimport androidx.biometric.BiometricManager\nimport androidx.core.view.isVisible\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.widget.lockpattern.LockPatternUtils\nimport com.hippo.widget.lockpattern.LockPatternView\nimport com.hippo.yorozuya.ViewUtils\n\nclass SetSecurityFragment :\n    BaseFragment(),\n    View.OnClickListener {\n    private var mPatternView: LockPatternView? = null\n    private var mCancel: View? = null\n    private var mSet: View? = null\n    private var mFingerprint: CheckBox? = null\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? {\n        val view = inflater.inflate(R.layout.activity_set_security, container, false)\n        mPatternView = ViewUtils.`$$`(view, R.id.pattern_view) as LockPatternView\n        mCancel = ViewUtils.`$$`(view, R.id.cancel)\n        mSet = ViewUtils.`$$`(view, R.id.set)\n        mFingerprint = ViewUtils.`$$`(view, R.id.fingerprint_checkbox) as CheckBox\n        val pattern = Settings.security\n        if (!TextUtils.isEmpty(pattern)) {\n            mPatternView!!.setPattern(\n                LockPatternView.DisplayMode.Correct,\n                LockPatternUtils.stringToPattern(pattern),\n            )\n        }\n        if (BiometricManager.from(requireContext()).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS) {\n            mFingerprint!!.visibility = View.VISIBLE\n            mFingerprint!!.isChecked = Settings.enableFingerprint\n        }\n        mCancel!!.setOnClickListener(this)\n        mSet!!.setOnClickListener(this)\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mPatternView = null\n    }\n\n    @Suppress(\"DEPRECATION\")\n    override fun onClick(v: View) {\n        if (v == mCancel) {\n            requireActivity().onBackPressed()\n        } else if (v == mSet) {\n            if (null != mPatternView && null != mFingerprint) {\n                val security = if (mPatternView!!.cellSize <= 1) {\n                    \"\"\n                } else {\n                    mPatternView!!.patternString\n                }\n                Settings.putSecurity(security)\n                Settings.putEnableFingerprint(\n                    mFingerprint!!.isVisible &&\n                        mFingerprint!!.isChecked &&\n                        security.isNotEmpty(),\n                )\n            }\n            requireActivity().onBackPressed()\n        }\n    }\n\n    override fun getFragmentTitle(): Int = R.string.set_pattern_protection\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/SettingsFragment.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport android.os.Bundle\nimport androidx.annotation.StringRes\nimport com.hippo.ehviewer.R\n\nclass SettingsFragment : BasePreferenceFragment() {\n    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {\n        addPreferencesFromResource(R.xml.settings_headers)\n    }\n\n    @get:StringRes\n    override val fragmentTitle: Int\n        get() = R.string.settings\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/fragment/UConfigFragment.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.ui.fragment\n\nimport android.graphics.Bitmap\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.webkit.CookieManager\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.core.view.MenuProvider\nimport androidx.lifecycle.Lifecycle\nimport com.google.android.material.progressindicator.CircularProgressIndicator\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.ui.scene.BaseScene\nimport com.hippo.ehviewer.util.setDefaultSettings\nimport com.hippo.ehviewer.widget.DialogWebChromeClient\nimport com.hippo.util.launchIO\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport okhttp3.Cookie\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport rikka.core.res.resolveColor\n\nclass UConfigFragment : BaseFragment() {\n    private val url = EhUrl.uConfigUrl\n    private var webView: WebView? = null\n    private var progress: CircularProgressIndicator? = null\n    private var loaded = false\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.activity_webview, container, false)\n        webView = view.findViewById(R.id.webview)\n        webView!!.run {\n            setBackgroundColor(requireActivity().theme.resolveColor(android.R.attr.colorBackground))\n            setDefaultSettings()\n            webViewClient = UConfigWebViewClient()\n            webChromeClient = DialogWebChromeClient(requireContext())\n        }\n        progress = view.findViewById(R.id.progress)\n        return view\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        progress!!.visibility = View.VISIBLE\n        webView!!.loadUrl(url)\n        showTip(R.string.apply_tip, BaseScene.LENGTH_LONG)\n        requireActivity().addMenuProvider(\n            object : MenuProvider {\n                override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {\n                    menuInflater.inflate(R.menu.activity_u_config, menu)\n                }\n\n                override fun onMenuItemSelected(menuItem: MenuItem): Boolean {\n                    if (menuItem.itemId == R.id.action_apply) {\n                        if (loaded) apply()\n                        return true\n                    }\n                    return false\n                }\n            },\n            viewLifecycleOwner,\n            Lifecycle.State.RESUMED,\n        )\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        // http://stackoverflow.com/questions/32284642/how-to-handle-an-uncatched-exception\n        val cookieManager = CookieManager.getInstance()\n        cookieManager.flush()\n        cookieManager.removeAllCookies(null)\n        cookieManager.removeSessionCookies(null)\n\n        // Copy cookies from okhttp cookie store to CookieManager\n        for (cookie in EhCookieStore.getCookies(url.toHttpUrl())) {\n            cookieManager.setCookie(url, cookie.toString())\n        }\n    }\n\n    private fun apply() {\n        webView?.loadUrl(\"javascript: document.getElementById('apply').children[0].click();\")\n    }\n\n    private fun longLive(cookie: Cookie): Cookie = Cookie.Builder()\n        .name(cookie.name)\n        .value(cookie.value)\n        .domain(cookie.domain)\n        .path(cookie.path)\n        .expiresAt(Long.MAX_VALUE)\n        .build()\n\n    @OptIn(DelicateCoroutinesApi::class)\n    override fun onDestroyView() {\n        super.onDestroyView()\n        webView?.destroy()\n        webView = null\n\n        val cookiesString = CookieManager.getInstance().getCookie(url)\n        if (!cookiesString.isNullOrEmpty()) {\n            val hostUrl = EhUrl.host.toHttpUrl()\n            launchIO {\n                EhCookieStore.deleteCookie(hostUrl, EhCookieStore.KEY_SETTINGS_PROFILE)\n                for (header in cookiesString.split(\";\".toRegex()).dropLastWhile { it.isEmpty() }) {\n                    Cookie.parse(hostUrl, header)?.let {\n                        if (it.name == EhCookieStore.KEY_CLOUDFLARE || it.name == EhCookieStore.KEY_SETTINGS_PROFILE) {\n                            EhCookieStore.addCookie(longLive(it))\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    override fun getFragmentTitle(): Int = R.string.u_config\n\n    private inner class UConfigWebViewClient : WebViewClient() {\n        override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {\n            // Never load other urls\n            return true\n        }\n\n        override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {\n            progress!!.visibility = View.VISIBLE\n            loaded = false\n        }\n\n        override fun onPageFinished(view: WebView, url: String) {\n            progress!!.visibility = View.GONE\n            loaded = true\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/BaseScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.annotation.SuppressLint\nimport android.content.res.Configuration\nimport android.content.res.Resources\nimport android.content.res.Resources.Theme\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.util.SparseArray\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.ViewTreeObserver.OnPreDrawListener\nimport androidx.annotation.IdRes\nimport androidx.annotation.StringRes\nimport androidx.core.view.GravityCompat\nimport androidx.core.view.SoftwareKeyboardControllerCompat\nimport androidx.core.view.WindowCompat\nimport androidx.drawerlayout.widget.DrawerLayout\nimport com.hippo.ehviewer.ui.MainActivity\nimport com.hippo.scene.SceneFragment\nimport com.hippo.util.getSparseParcelableArrayCompat\nimport com.hippo.util.isAtLeastR\n\nabstract class BaseScene : SceneFragment() {\n    private var drawerView: View? = null\n    private var drawerViewState: SparseArray<Parcelable?>? = null\n    open var needWhiteStatusBar = true\n\n    fun updateAvatar() {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.updateProfile()\n        }\n    }\n\n    fun addAboveSnackView(view: View) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.addAboveSnackView(view)\n        }\n    }\n\n    fun removeAboveSnackView(view: View) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.removeAboveSnackView(view)\n        }\n    }\n\n    fun setDrawerLockMode(lockMode: Int, edgeGravity: Int) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.setDrawerLockMode(lockMode, edgeGravity)\n        }\n    }\n\n    fun openDrawer(drawerGravity: Int) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.openDrawer(drawerGravity)\n        }\n    }\n\n    fun closeDrawer(drawerGravity: Int) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.closeDrawer(drawerGravity)\n        }\n    }\n\n    fun toggleDrawer(drawerGravity: Int) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.toggleDrawer(drawerGravity)\n        }\n    }\n\n    fun showTip(message: CharSequence?, length: Int) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.showTip(message!!, length)\n        }\n    }\n\n    fun showTip(@StringRes id: Int, length: Int) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.showTip(id, length)\n        }\n    }\n\n    open fun needShowLeftDrawer(): Boolean = true\n\n    open fun getNavCheckedItem(): Int = 0\n\n    /**\n     * @param resId 0 for clear\n     */\n    fun setNavCheckedItem(@IdRes resId: Int) {\n        val activity = activity\n        if (activity is MainActivity) {\n            activity.setNavCheckedItem(resId)\n        }\n    }\n\n    fun recreateDrawerView() {\n        val activity = mainActivity\n        activity?.createDrawerView(this)\n    }\n\n    fun createDrawerView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? {\n        drawerView = onCreateDrawerView(inflater, container, savedInstanceState)\n        if (drawerView != null) {\n            var saved = drawerViewState\n            if (saved == null && savedInstanceState != null) {\n                saved = savedInstanceState.getSparseParcelableArrayCompat(KEY_DRAWER_VIEW_STATE)\n            }\n            if (saved != null) {\n                drawerView!!.restoreHierarchyState(saved)\n            }\n        }\n        return drawerView\n    }\n\n    open fun onCreateDrawerView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? = null\n\n    fun destroyDrawerView() {\n        if (drawerView != null) {\n            drawerViewState = SparseArray()\n            drawerView!!.saveHierarchyState(drawerViewState)\n        }\n        onDestroyDrawerView()\n        drawerView = null\n    }\n\n    @Suppress(\"DEPRECATION\")\n    fun setLightStatusBar(set: Boolean) {\n        val activity = requireActivity()\n        val decorView = activity.window.decorView\n        val isLight = set && (activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_YES) <= 0\n        // https://github.com/EhViewer-NekoInverter/EhViewer/issues/55\n        if (isAtLeastR) {\n            WindowCompat.getInsetsController(activity.window, decorView).isAppearanceLightStatusBars = isLight\n        } else {\n            val flags = decorView.systemUiVisibility\n            decorView.systemUiVisibility = if (isLight) {\n                flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR\n            } else {\n                flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()\n            }\n        }\n        needWhiteStatusBar = set\n    }\n\n    open fun onDestroyDrawerView() {}\n\n    @SuppressLint(\"RtlHardcoded\")\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        postponeEnterTransition()\n        view.viewTreeObserver.addOnPreDrawListener(\n            object : OnPreDrawListener {\n                override fun onPreDraw(): Boolean {\n                    view.viewTreeObserver.removeOnPreDrawListener(this)\n                    startPostponedEnterTransition()\n                    return true\n                }\n            },\n        )\n\n        // Update left drawer locked state\n        if (needShowLeftDrawer()) {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)\n        } else {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)\n        }\n\n        // Update nav checked item\n        setNavCheckedItem(getNavCheckedItem())\n\n        // Hide soft ime\n        hideSoftInput()\n        setLightStatusBar(needWhiteStatusBar)\n    }\n\n    val resourcesOrNull: Resources?\n        get() {\n            val context = context\n            return context?.resources\n        }\n\n    val mainActivity: MainActivity?\n        get() {\n            val activity = activity\n            return activity as? MainActivity\n        }\n\n    fun hideSoftInput() = activity?.window?.decorView?.run { SoftwareKeyboardControllerCompat(this) }?.hide()\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        if (drawerView != null) {\n            drawerViewState = SparseArray()\n            drawerView!!.saveHierarchyState(drawerViewState)\n            outState.putSparseParcelableArray(KEY_DRAWER_VIEW_STATE, drawerViewState)\n        }\n    }\n\n    val theme: Theme\n        get() = requireActivity().theme\n\n    companion object {\n        const val LENGTH_SHORT = 0\n        const val LENGTH_LONG = 1\n        const val KEY_DRAWER_VIEW_STATE = \"com.hippo.ehviewer.ui.scene.BaseScene:DRAWER_VIEW_STATE\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/CookieSignInScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.graphics.Paint\nimport android.os.Bundle\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.EditText\nimport android.widget.TextView\nimport android.widget.TextView.OnEditorActionListener\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.lifecycleScope\nimport com.google.android.material.textfield.TextInputLayout\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhEngine\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.exception.CloudflareBypassException\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.getClipboardManager\nimport com.hippo.util.getTextFromClipboard\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport com.hippo.yorozuya.ViewUtils\nimport java.util.Locale\nimport kotlinx.coroutines.Job\nimport okhttp3.Cookie\n\nclass CookieSignInScene :\n    SolidScene(),\n    OnEditorActionListener,\n    View.OnClickListener {\n    private var mProgress: View? = null\n    private var mIpbMemberIdLayout: TextInputLayout? = null\n    private var mIpbPassHashLayout: TextInputLayout? = null\n    private var mIpbMemberId: EditText? = null\n    private var mIpbPassHash: EditText? = null\n    private var mOk: View? = null\n    private var mFromClipboard: TextView? = null\n    private var mSignInJob: Job? = null\n\n    override fun needShowLeftDrawer(): Boolean = false\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_cookie_sign_in, container, false)\n        val loginForm = ViewUtils.`$$`(view, R.id.cookie_signin_form)\n        mProgress = ViewUtils.`$$`(view, R.id.progress)\n        mIpbMemberIdLayout = ViewUtils.`$$`(loginForm, R.id.ipb_member_id_layout) as TextInputLayout\n        mIpbMemberId = mIpbMemberIdLayout!!.editText!!\n        mIpbPassHashLayout = ViewUtils.`$$`(loginForm, R.id.ipb_pass_hash_layout) as TextInputLayout\n        mIpbPassHash = mIpbPassHashLayout!!.editText!!\n        mOk = ViewUtils.`$$`(loginForm, R.id.ok)\n        mFromClipboard = ViewUtils.`$$`(loginForm, R.id.from_clipboard) as TextView\n        mFromClipboard!!.run {\n            paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG or Paint.ANTI_ALIAS_FLAG\n        }\n        mIpbPassHash!!.setOnEditorActionListener(this)\n        mOk!!.setOnClickListener(this)\n        mFromClipboard!!.setOnClickListener(this)\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mProgress = null\n        mIpbMemberIdLayout = null\n        mIpbPassHashLayout = null\n        mIpbMemberId = null\n        mIpbPassHash = null\n        mSignInJob = null\n    }\n\n    private fun showProgress() {\n        if (mProgress?.visibility == View.VISIBLE) return\n        mProgress?.apply {\n            alpha = 0f\n            visibility = View.VISIBLE\n            animate().alpha(1f).setDuration(500).start()\n        }\n    }\n\n    private fun hideProgress() {\n        mProgress?.visibility = View.GONE\n    }\n\n    override fun onClick(v: View) {\n        when (v) {\n            mOk -> enter()\n            mFromClipboard -> fillCookiesFromClipboard()\n        }\n    }\n\n    override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {\n        if (mIpbPassHash === v) {\n            enter()\n        }\n        return true\n    }\n\n    fun enter() {\n        if (mSignInJob?.isActive == true) return\n        val memberIdField = mIpbMemberId ?: return\n        val passHashField = mIpbPassHash ?: return\n        val memberIdLayout = mIpbMemberIdLayout ?: return\n        val passHashLayout = mIpbPassHashLayout ?: return\n        val memberId = memberIdField.text.toString().trim()\n        val passHash = passHashField.text.toString().trim()\n\n        if (memberId.isEmpty()) {\n            memberIdLayout.error = getString(R.string.text_is_empty)\n            return\n        } else {\n            memberIdLayout.error = null\n        }\n        if (passHash.isEmpty()) {\n            passHashLayout.error = getString(R.string.text_is_empty)\n            return\n        } else {\n            passHashLayout.error = null\n        }\n        hideSoftInput()\n        showProgress()\n\n        mSignInJob = viewLifecycleOwner.lifecycleScope.launchIO {\n            EhUtils.signOut()\n            val result = runCatching {\n                storeCookie(memberId, passHash)\n                EhEngine.getProfile().also {\n                    Settings.putDisplayName(it.displayName)\n                    Settings.putAvatar(it.avatar)\n                }\n            }\n            withUIContext {\n                hideProgress()\n                result.onSuccess {\n                    ok()\n                }.onFailure {\n                    showResultErrorDialog(it)\n                }\n            }\n        }\n    }\n\n    private fun ok() {\n        setResult(RESULT_OK, null)\n        finish()\n    }\n\n    private fun showResultErrorDialog(e: Throwable) {\n        val isCloudflareError = e.cause is CloudflareBypassException\n        val message = buildString {\n            append(ExceptionUtils.getReadableString(e))\n            append(\"\\n\\n\")\n            append(getString(R.string.sign_in_failed_tip))\n            if (isCloudflareError) {\n                append(\"\\n\\n\")\n                append(getString(R.string.sign_in_failed_tip_2))\n            }\n        }\n\n        AlertDialog.Builder(requireContext())\n            .setTitle(R.string.sign_in_failed)\n            .setMessage(message)\n            .setPositiveButton(R.string.get_it, null)\n            .apply {\n                if (isCloudflareError) {\n                    setNegativeButton(R.string.ignore) { _, _ -> ok() }\n                }\n            }\n            .show()\n    }\n\n    private suspend fun storeCookie(id: String, hash: String) {\n        fun newCookie(name: String, value: String, domain: String): Cookie = Cookie.Builder().name(name).value(value)\n            .domain(domain).expiresAt(Long.MAX_VALUE).build()\n        EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_E))\n        EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_MEMBER_ID, id, EhUrl.DOMAIN_EX))\n        EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_E))\n        EhCookieStore.addCookie(newCookie(EhCookieStore.KEY_IPB_PASS_HASH, hash, EhUrl.DOMAIN_EX))\n    }\n\n    private fun fillCookiesFromClipboard() {\n        val context = requireContext()\n        fun showClipboardError() = showTip(R.string.from_clipboard_error, LENGTH_SHORT)\n        hideSoftInput()\n\n        val clipboardText = context.getClipboardManager().getTextFromClipboard(context)\n        if (clipboardText.isNullOrBlank()) {\n            showClipboardError()\n            return\n        }\n        val kvs = when {\n            clipboardText.contains(\";\") -> clipboardText.split(\";\")\n            clipboardText.contains(\"\\n\") -> clipboardText.split(\"\\n\")\n            else -> {\n                showClipboardError()\n                return\n            }\n        }.map { it.trim() }.filter { it.isNotEmpty() }\n        val hasRequiredKeys = clipboardText.contains(EhCookieStore.KEY_IPB_MEMBER_ID) &&\n            clipboardText.contains(EhCookieStore.KEY_IPB_PASS_HASH)\n        if (!hasRequiredKeys || kvs.size < 2) {\n            showClipboardError()\n            return\n        }\n\n        try {\n            kvs.forEach { entry ->\n                val kv = when {\n                    entry.contains(\"=\") -> entry.split(\"=\")\n                    entry.contains(\":\") -> entry.split(\":\")\n                    else -> return@forEach\n                }\n                if (kv.size != 2) return@forEach\n\n                val key = kv[0].trim().lowercase(Locale.getDefault())\n                val value = kv[1].trim().replace(Regex(\"[^a-zA-Z0-9\\\\-_.~]\"), \"\")\n                when (key) {\n                    EhCookieStore.KEY_IPB_MEMBER_ID -> mIpbMemberId?.setText(value)\n                    EhCookieStore.KEY_IPB_PASS_HASH -> mIpbPassHash?.setText(value)\n                }\n            }\n            enter()\n        } catch (e: Exception) {\n            ExceptionUtils.throwIfFatal(e)\n            e.printStackTrace()\n            showClipboardError()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/DownloadsScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.Button\nimport android.widget.ImageView\nimport android.widget.ProgressBar\nimport android.widget.TextView\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.content.ContextCompat\nimport androidx.core.util.size\nimport androidx.core.view.GravityCompat\nimport androidx.core.view.ViewCompat\nimport androidx.drawerlayout.widget.DrawerLayout\nimport androidx.lifecycle.lifecycleScope\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.SimpleItemAnimator\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager\nimport com.google.android.material.floatingactionbutton.FloatingActionButton\nimport com.hippo.app.CheckBoxDialogBuilder\nimport com.hippo.app.EditTextDialogBuilder\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.EasyRecyclerView.CustomChoiceListener\nimport com.hippo.easyrecyclerview.FastScroller\nimport com.hippo.easyrecyclerview.FastScroller.OnDragHandlerListener\nimport com.hippo.easyrecyclerview.HandlerDrawable\nimport com.hippo.easyrecyclerview.LinearDividerItemDecoration\nimport com.hippo.easyrecyclerview.MarginItemDecoration\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.getThumbKey\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener\nimport com.hippo.ehviewer.download.DownloadService\nimport com.hippo.ehviewer.download.DownloadService.Companion.clear\nimport com.hippo.ehviewer.spider.DownloadInfoMagics.encodeMagicRequest\nimport com.hippo.ehviewer.spider.SpiderDen\nimport com.hippo.ehviewer.ui.GalleryActivity\nimport com.hippo.ehviewer.widget.SimpleRatingView\nimport com.hippo.scene.Announcer\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.launchIO\nimport com.hippo.util.launchNonCancellable\nimport com.hippo.util.launchUI\nimport com.hippo.view.ViewTransition\nimport com.hippo.widget.FabLayout\nimport com.hippo.widget.FabLayout.OnClickFabListener\nimport com.hippo.widget.LoadImageView\nimport com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.ObjectUtils\nimport com.hippo.yorozuya.ViewUtils\nimport com.hippo.yorozuya.collect.LongList\nimport java.util.LinkedList\nimport rikka.core.res.resolveColor\n\n@SuppressLint(\"RtlHardcoded\")\nclass DownloadsScene :\n    ToolbarScene(),\n    DownloadInfoListener,\n    OnClickFabListener,\n    OnDragHandlerListener {\n    private lateinit var mLabels: MutableList<String>\n    private var mLabel: String? = null\n    private var mList: MutableList<DownloadInfo>? = null\n    private var mTip: TextView? = null\n    private var mFastScroller: FastScroller? = null\n    private var mRecyclerView: EasyRecyclerView? = null\n    private var mViewTransition: ViewTransition? = null\n    private var mFabLayout: FabLayout? = null\n    private var mAdapter: DownloadAdapter? = null\n    private var mItemTouchHelper: ItemTouchHelper? = null\n    private var mLayoutManager: AutoStaggeredGridLayoutManager? = null\n    private var mLabelAdapter: DownloadLabelAdapter? = null\n    private var mLabelItemTouchHelper: ItemTouchHelper? = null\n    private var mKeyword: String? = null\n    private var mSort = Settings.defaultSortingMethod\n    private var mType = -1\n    private var mInitPosition = -1\n\n    override fun getNavCheckedItem(): Int = R.id.nav_downloads\n\n    private fun initLabels() {\n        val listLabel = DownloadManager.labelList\n        mLabels = ArrayList(listLabel.size + LABEL_OFFSET)\n        // Add \"All\" and \"Default\" label names\n        mLabels.add(getString(R.string.download_all))\n        mLabels.add(getString(R.string.default_download_label_name))\n        listLabel.forEach {\n            mLabels.add(it.label!!)\n        }\n    }\n\n    private fun handleArguments(args: Bundle?): Boolean {\n        if (null == args) {\n            return false\n        }\n        if (ACTION_CLEAR_DOWNLOAD_SERVICE == args.getString(KEY_ACTION)) {\n            clear()\n        }\n        val gid = args.getLong(KEY_GID, -1L)\n        if (-1L != gid) {\n            DownloadManager.getDownloadInfo(gid)?.let {\n                mLabel = it.label\n                updateForLabel()\n                updateView()\n\n                // Get position\n                if (null != mList) {\n                    val position = mList!!.indexOf(it)\n                    if (position >= 0 && null != mRecyclerView) {\n                        mRecyclerView!!.scrollToPosition(position)\n                    } else {\n                        mInitPosition = position\n                    }\n                }\n                return true\n            }\n        }\n        return false\n    }\n\n    override fun onNewArguments(args: Bundle) {\n        handleArguments(args)\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        DownloadManager.addDownloadInfoListener(this)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mList = null\n        DownloadManager.removeDownloadInfoListener(this)\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    private fun updateForLabel() {\n        var list: MutableList<DownloadInfo>?\n        if (mLabel == null) {\n            list = DownloadManager.allDownloadInfoList\n        } else if (mLabel == getString(R.string.default_download_label_name)) {\n            list = DownloadManager.defaultDownloadInfoList\n        } else {\n            list = DownloadManager.getLabelDownloadInfoList(mLabel)\n            if (list == null) {\n                mLabel = null\n                list = DownloadManager.allDownloadInfoList\n            }\n        }\n\n        if (mType != -1) {\n            mList = ArrayList()\n            list.forEach {\n                if (mKeyword != null && EhUtils.getSuitableTitle(it).contains(mKeyword!!, true) || it.state == mType) {\n                    mList!!.add(it)\n                }\n            }\n        } else {\n            mList = list\n        }\n\n        if (mSort == 10) {\n            mList = ArrayList(mList!!.shuffled())\n        } else {\n            mList!!.sortWith { o1, o2 ->\n                val title1 = EhUtils.getSuitableTitle(o1)\n                val title2 = EhUtils.getSuitableTitle(o2)\n                when (mSort) {\n                    0 -> o2.time.compareTo(o1.time)\n                    1 -> o1.time.compareTo(o2.time)\n                    2 -> title1.compareTo(title2, true)\n                    3 -> title2.compareTo(title1, true)\n                    4 -> getAuthor(title1).compareTo(getAuthor(title2), true)\n                    5 -> getAuthor(title2).compareTo(getAuthor(title1), true)\n                    6 -> getName(title1).compareTo(getName(title2), true)\n                    7 -> getName(title2).compareTo(getName(title1), true)\n                    8 -> o1.category.compareTo(o2.category)\n                    9 -> o2.category.compareTo(o1.category)\n                    else -> 0\n                }\n            }\n        }\n\n        mAdapter?.notifyDataSetChanged()\n\n        updateTitle()\n        Settings.putRecentDownloadLabel(mLabel)\n    }\n\n    private fun updateTitle() {\n        setTitle(\n            getString(\n                R.string.scene_download_title,\n                if (mLabel != null) mLabel else getString(R.string.download_all),\n            ),\n        )\n    }\n\n    private fun onInit() {\n        if (!handleArguments(arguments)) {\n            mLabel = Settings.recentDownloadLabel\n            updateForLabel()\n        }\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mLabel = savedInstanceState.getString(KEY_LABEL)\n        updateForLabel()\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        outState.putString(KEY_LABEL, mLabel)\n    }\n\n    override fun onCreateViewWithToolbar(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_download, container, false)\n        val content = ViewUtils.`$$`(view, R.id.content)\n        mRecyclerView = ViewUtils.`$$`(content, R.id.recycler_view) as EasyRecyclerView\n        mFastScroller = ViewUtils.`$$`(content, R.id.fast_scroller) as FastScroller\n        mFabLayout = ViewUtils.`$$`(view, R.id.fab_layout) as FabLayout\n        mTip = ViewUtils.`$$`(view, R.id.tip) as TextView\n        mViewTransition = ViewTransition(content, mTip)\n        val context = context\n        val resources = context!!.resources\n        val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_download)\n        drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n        mTip!!.setCompoundDrawables(null, drawable, null, null)\n        mAdapter = DownloadAdapter()\n        mAdapter!!.setHasStableIds(true)\n        mRecyclerView!!.adapter = mAdapter\n        mLayoutManager = AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)\n        mLayoutManager!!.setColumnSize(Settings.detailSize)\n        mLayoutManager!!.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE)\n        mRecyclerView!!.layoutManager = mLayoutManager\n        mRecyclerView!!.clipToPadding = false\n        mRecyclerView!!.clipChildren = false\n        mRecyclerView!!.setChoiceMode(EasyRecyclerView.CHOICE_MODE_MULTIPLE_CUSTOM)\n        mRecyclerView!!.setCustomCheckedListener(DownloadChoiceListener())\n        // Cancel change animation\n        val itemAnimator = mRecyclerView!!.itemAnimator\n        if (itemAnimator is SimpleItemAnimator) {\n            itemAnimator.supportsChangeAnimations = false\n        }\n        val interval = resources.getDimensionPixelOffset(R.dimen.gallery_list_interval)\n        val paddingH = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)\n        val paddingV = resources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)\n        val decoration = MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)\n        mRecyclerView!!.addItemDecoration(decoration)\n        if (mInitPosition >= 0) {\n            mRecyclerView!!.scrollToPosition(mInitPosition)\n            mInitPosition = -1\n        }\n        mItemTouchHelper = ItemTouchHelper(DownloadItemTouchHelperCallback())\n        mItemTouchHelper!!.attachToRecyclerView(mRecyclerView)\n        mFastScroller!!.attachToRecyclerView(mRecyclerView)\n        val handlerDrawable = HandlerDrawable()\n        handlerDrawable.setColor(theme.resolveColor(R.attr.widgetColorThemeAccent))\n        mFastScroller!!.setHandlerDrawable(handlerDrawable)\n        mFastScroller!!.setOnDragHandlerListener(this)\n        mFabLayout!!.setExpanded(expanded = false, animation = false)\n        mFabLayout!!.setHidePrimaryFab(true)\n        mFabLayout!!.setAutoCancel(false)\n        mFabLayout!!.setOnClickFabListener(this)\n        addAboveSnackView(mFabLayout!!)\n        updateView()\n        return view\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        updateTitle()\n        setNavigationIcon(R.drawable.ic_baseline_menu_24)\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        if (null != mRecyclerView) {\n            mRecyclerView!!.stopScroll()\n            mRecyclerView = null\n        }\n        if (null != mFabLayout) {\n            removeAboveSnackView(mFabLayout!!)\n            mFabLayout = null\n        }\n        mRecyclerView = null\n        mViewTransition = null\n        mAdapter = null\n        mLayoutManager = null\n    }\n\n    override fun onNavigationClick() {\n        toggleDrawer(GravityCompat.START)\n    }\n\n    override fun getMenuResId(): Int = R.menu.scene_download\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        // Skip when in choice mode\n        val activity: Activity? = mainActivity\n        if (null == activity || null == mRecyclerView || mRecyclerView!!.isInCustomChoice) {\n            return false\n        }\n        when (item.itemId) {\n            R.id.action_filter -> {\n                AlertDialog.Builder(requireActivity())\n                    .setSingleChoiceItems(\n                        R.array.download_state,\n                        mType + 1,\n                    ) { dialog: DialogInterface, which: Int ->\n                        dialog.dismiss()\n                        if (which == 6) {\n                            showFilterTitleDialog()\n                        } else {\n                            mType = which - 1\n                            mKeyword = null\n                            updateForLabel()\n                            updateView()\n                        }\n                    }\n                    .show()\n                return true\n            }\n            R.id.action_start_all -> {\n                val intent = Intent(activity, DownloadService::class.java)\n                intent.action = DownloadService.ACTION_START_ALL\n                ContextCompat.startForegroundService(activity, intent)\n                return true\n            }\n            R.id.action_stop_all -> {\n                // DownloadManager Actions\n                DownloadManager.stopAllDownload()\n                return true\n            }\n            R.id.action_open_download_labels -> {\n                openDrawer(GravityCompat.END)\n                return true\n            }\n            R.id.action_reset_reading_progress -> {\n                AlertDialog.Builder(requireContext())\n                    .setMessage(R.string.reset_reading_progress_message)\n                    .setNegativeButton(android.R.string.cancel, null)\n                    .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->\n                        lifecycleScope.launchNonCancellable {\n                            // DownloadManager Actions\n                            DownloadManager.resetAllReadingProgress()\n                        }\n                    }.show()\n                return true\n            }\n            R.id.action_start_all_reversed -> {\n                val list = mList ?: return true\n                val gidList = LongList()\n                for (i in list.size - 1 downTo 0) {\n                    val info = list[i]\n                    if (info.state != DownloadInfo.STATE_FINISH) {\n                        gidList.add(info.gid)\n                    }\n                }\n                val intent = Intent(activity, DownloadService::class.java)\n                intent.action = DownloadService.ACTION_START_RANGE\n                intent.putExtra(DownloadService.KEY_GID_LIST, gidList)\n                ContextCompat.startForegroundService(activity, intent)\n                return true\n            }\n            R.id.action_sort -> {\n                AlertDialog.Builder(requireActivity())\n                    .setSingleChoiceItems(\n                        R.array.download_sort,\n                        mSort,\n                    ) { dialog: DialogInterface, which: Int ->\n                        mSort = which\n                        Settings.putDefaultSortingMethod(which)\n                        dialog.dismiss()\n                        updateForLabel()\n                        updateView()\n                    }\n                    .show()\n                return true\n            }\n            else -> return false\n        }\n    }\n\n    private fun showFilterTitleDialog() {\n        val builder = EditTextDialogBuilder(\n            requireActivity(),\n            null,\n            getString(R.string.download_filter_title),\n        )\n        builder.setTitle(R.string.search)\n        builder.setPositiveButton(android.R.string.ok, null)\n        val dialog = builder.show()\n        val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)\n        button?.setOnClickListener {\n            val text = builder.text.trim { it <= ' ' }\n            if (TextUtils.isEmpty(text)) {\n                builder.setError(getString(R.string.text_is_empty))\n            } else {\n                builder.setError(null)\n                dialog.dismiss()\n                mType = 5\n                mKeyword = text.lowercase()\n                updateForLabel()\n                updateView()\n            }\n        }\n    }\n\n    fun updateView() {\n        if (mList.isNullOrEmpty()) {\n            mViewTransition?.showView(1)\n        } else {\n            mViewTransition?.showView(0)\n        }\n    }\n\n    override fun onCreateDrawerView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.drawer_list_rv, container, false)\n        val toolbar = view.findViewById<Toolbar>(R.id.toolbar)\n        toolbar.setTitle(R.string.download_labels)\n        toolbar.inflateMenu(R.menu.drawer_download)\n        toolbar.setOnMenuItemClickListener { item: MenuItem ->\n            val id = item.itemId\n            if (id == R.id.action_add) {\n                val builder =\n                    EditTextDialogBuilder(requireContext(), null, getString(R.string.download_labels))\n                builder.setTitle(R.string.new_label_title)\n                builder.setPositiveButton(android.R.string.ok, null)\n                val dialog = builder.show()\n                NewLabelDialogHelper(builder, dialog)\n                return@setOnMenuItemClickListener true\n            } else if (id == R.id.action_default_download_label) {\n                val list = DownloadManager.labelList\n                val items = arrayOfNulls<String>(list.size + 2)\n                items[0] = getString(R.string.let_me_select)\n                items[1] = getString(R.string.default_download_label_name)\n                var i = 0\n                val n = list.size\n                while (i < n) {\n                    items[i + 2] = list[i].label\n                    i++\n                }\n                AlertDialog.Builder(requireContext())\n                    .setTitle(R.string.default_download_label)\n                    .setItems(items) { _: DialogInterface?, which: Int ->\n                        if (which == 0) {\n                            Settings.putHasDefaultDownloadLabel(false)\n                        } else {\n                            Settings.putHasDefaultDownloadLabel(true)\n                            val label: String? = if (which == 1) {\n                                null\n                            } else {\n                                items[which]\n                            }\n                            Settings.putDefaultDownloadLabel(label)\n                        }\n                    }.show()\n                return@setOnMenuItemClickListener true\n            }\n            false\n        }\n        initLabels()\n        mLabelAdapter = DownloadLabelAdapter(inflater)\n        val recyclerView = view.findViewById<EasyRecyclerView>(R.id.recycler_view_drawer)\n        recyclerView.layoutManager = LinearLayoutManager(context)\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(context, 1f),\n        )\n        decoration.setShowLastDivider(true)\n        mLabelAdapter!!.setHasStableIds(true)\n        mLabelItemTouchHelper = ItemTouchHelper(DownloadLabelItemTouchHelperCallback())\n        mLabelItemTouchHelper!!.attachToRecyclerView(recyclerView)\n        recyclerView.adapter = mLabelAdapter\n        return view\n    }\n\n    override fun onBackPressed() {\n        if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n            mRecyclerView!!.outOfCustomChoiceMode()\n        } else {\n            super.onBackPressed()\n        }\n    }\n\n    override fun onStartDragHandler() {\n        // Lock right drawer\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)\n    }\n\n    override fun onEndDragHandler() {\n        // Restore right drawer\n        if (null != mRecyclerView && !mRecyclerView!!.isInCustomChoice) {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)\n        }\n    }\n\n    override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {\n        if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n            mRecyclerView!!.outOfCustomChoiceMode()\n        }\n    }\n\n    override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {\n        val context = context\n        val activity = mainActivity\n        val recyclerView = mRecyclerView\n        if (null == context || null == activity || null == recyclerView) {\n            return\n        }\n        if (0 == position) {\n            recyclerView.checkAll()\n        } else {\n            val list = mList ?: return\n            var gidList: LongList? = null\n            var downloadInfoList: MutableList<DownloadInfo>? = null\n            val collectGid = position == 2 || position == 3 || position == 4 // Start, Stop, Delete\n            val collectDownloadInfo = position == 1 || position == 4 || position == 5 // Pin, Delete, Move\n            if (collectGid) {\n                gidList = LongList()\n            }\n            if (collectDownloadInfo) {\n                downloadInfoList = LinkedList()\n            }\n            recyclerView.checkedItemPositions?.let {\n                for (i in 0 until it.size) {\n                    if (it.valueAt(i)) {\n                        val info = list[it.keyAt(i)]\n                        if (collectDownloadInfo) {\n                            downloadInfoList!!.add(info)\n                        }\n                        if (collectGid) {\n                            gidList!!.add(info.gid)\n                        }\n                    }\n                }\n            }\n            when (position) {\n                // Pin to top\n                1 -> {\n                    val pinList = downloadInfoList!!.reversed()\n                    val nowTimeStamp = System.currentTimeMillis()\n                    for (i in pinList.indices) {\n                        pinList[i].time = nowTimeStamp + i\n                        // DB Actions\n                        EhDB.putDownloadInfo(pinList[i])\n                    }\n                    recyclerView.outOfCustomChoiceMode()\n                    updateForLabel()\n                }\n                // Start\n                2 -> {\n                    val intent = Intent(activity, DownloadService::class.java)\n                    intent.action = DownloadService.ACTION_START_RANGE\n                    intent.putExtra(DownloadService.KEY_GID_LIST, gidList)\n                    ContextCompat.startForegroundService(activity, intent)\n                    // Cancel check mode\n                    recyclerView.outOfCustomChoiceMode()\n                }\n                // Stop\n                3 -> {\n                    // DownloadManager Actions\n                    DownloadManager.stopRangeDownload(gidList!!)\n                    // Cancel check mode\n                    recyclerView.outOfCustomChoiceMode()\n                }\n                // Delete\n                4 -> {\n                    val builder = CheckBoxDialogBuilder(\n                        context,\n                        getString(R.string.download_remove_dialog_message_2, gidList!!.size),\n                        getString(R.string.download_remove_dialog_check_text),\n                        Settings.removeImageFiles,\n                    )\n                    val helper = DeleteRangeDialogHelper(\n                        downloadInfoList!!,\n                        gidList,\n                        builder,\n                    )\n                    builder.setTitle(R.string.download_remove_dialog_title)\n                        .setPositiveButton(android.R.string.ok, helper)\n                        .show()\n                }\n                // Move\n                5 -> {\n                    val labelRawList = DownloadManager.labelList\n                    val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)\n                    labelList.add(getString(R.string.default_download_label_name))\n                    labelRawList.forEach {\n                        labelList.add(it.label!!)\n                    }\n                    val labels = labelList.toTypedArray()\n                    val helper = MoveDialogHelper(labels, downloadInfoList!!)\n                    AlertDialog.Builder(context)\n                        .setTitle(R.string.download_move_dialog_title)\n                        .setItems(labels, helper)\n                        .show()\n                }\n            }\n        }\n    }\n\n    override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n        if (mList !== list) {\n            return\n        }\n        mAdapter?.notifyItemInserted(position)\n        updateView()\n    }\n\n    override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {\n        if (null == mList) {\n            return\n        }\n        val index = mList!!.indexOf(info)\n        if (index >= 0) {\n            mAdapter?.notifyItemChanged(index, PAYLOAD_STATE)\n        }\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    override fun onUpdateAll() {\n        mAdapter?.notifyDataSetChanged()\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    override fun onReload() {\n        mAdapter?.notifyDataSetChanged()\n        updateView()\n    }\n\n    override fun onChange() {\n        lifecycleScope.launchUI {\n            mLabel = null\n            updateForLabel()\n            updateView()\n        }\n    }\n\n    override fun onRenameLabel(from: String, to: String) {\n        if (!ObjectUtils.equal(mLabel, from)) {\n            return\n        }\n        mLabel = to\n        updateForLabel()\n        updateView()\n    }\n\n    override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n        if (mList !== list) {\n            return\n        }\n        mAdapter?.notifyItemRemoved(position)\n        updateView()\n    }\n\n    override fun onUpdateLabels() {\n        // TODO\n    }\n\n    private fun bindForState(holder: DownloadHolder, info: DownloadInfo) {\n        val context = context ?: return\n        when (info.state) {\n            DownloadInfo.STATE_NONE -> bindState(\n                holder,\n                info,\n                context.getString(R.string.download_state_none),\n            )\n            DownloadInfo.STATE_WAIT -> bindState(\n                holder,\n                info,\n                context.getString(R.string.download_state_wait),\n            )\n            DownloadInfo.STATE_DOWNLOAD -> bindProgress(holder, info)\n            DownloadInfo.STATE_FAILED -> {\n                val text: String = if (info.legacy <= 0) {\n                    context.getString(R.string.download_state_failed)\n                } else {\n                    context.getString(R.string.download_state_failed_2, info.legacy)\n                }\n                bindState(holder, info, text)\n            }\n            DownloadInfo.STATE_FINISH -> bindState(\n                holder,\n                info,\n                context.getString(R.string.download_state_finish),\n            )\n        }\n    }\n\n    private fun bindState(holder: DownloadHolder, info: DownloadInfo, state: String) {\n        holder.uploader.visibility = View.VISIBLE\n        holder.rating.visibility = View.VISIBLE\n        holder.category.visibility = View.VISIBLE\n        holder.state.visibility = View.VISIBLE\n        holder.progressBar.visibility = View.GONE\n        holder.percent.visibility = View.GONE\n        holder.speed.visibility = View.GONE\n        if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {\n            holder.start.visibility = View.GONE\n            holder.stop.visibility = View.VISIBLE\n        } else {\n            holder.start.visibility = View.VISIBLE\n            holder.stop.visibility = View.GONE\n        }\n        holder.state.text = state\n        if (mSort == 0 && mType == -1) {\n            holder.move.visibility = View.VISIBLE\n        } else {\n            holder.move.visibility = View.GONE\n        }\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    private fun bindProgress(holder: DownloadHolder, info: DownloadInfo) {\n        holder.uploader.visibility = View.GONE\n        holder.rating.visibility = View.GONE\n        holder.category.visibility = View.GONE\n        holder.state.visibility = View.GONE\n        holder.progressBar.visibility = View.VISIBLE\n        holder.percent.visibility = View.VISIBLE\n        holder.speed.visibility = View.VISIBLE\n        if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {\n            holder.start.visibility = View.GONE\n            holder.stop.visibility = View.VISIBLE\n        } else {\n            holder.start.visibility = View.VISIBLE\n            holder.stop.visibility = View.GONE\n        }\n        if (info.total <= 0 || info.finished < 0) {\n            holder.percent.text = null\n            holder.progressBar.isIndeterminate = true\n        } else {\n            holder.percent.text = info.finished.toString() + \"/\" + info.total\n            holder.progressBar.isIndeterminate = false\n            holder.progressBar.max = info.total\n            holder.progressBar.progress = info.finished\n        }\n        var speed = info.speed\n        if (speed < 0) {\n            speed = 0\n        }\n        holder.speed.text = FileUtils.humanReadableByteCount(speed, false) + \"/S\"\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private inner class DownloadLabelHolder(\n        itemView: View,\n    ) : RecyclerView.ViewHolder(itemView),\n        View.OnTouchListener {\n        val label: TextView = ViewUtils.`$$`(itemView, R.id.tv_key) as TextView\n        val option: ImageView = ViewUtils.`$$`(itemView, R.id.iv_option) as ImageView\n\n        init {\n            option.setOnTouchListener(this)\n        }\n\n        override fun onTouch(v: View, event: MotionEvent): Boolean {\n            if (mLabelItemTouchHelper != null && event.action == MotionEvent.ACTION_DOWN) {\n                mLabelItemTouchHelper!!.startDrag(this)\n            }\n            return false\n        }\n    }\n\n    private inner class DownloadLabelAdapter(private val mInflater: LayoutInflater) : RecyclerView.Adapter<DownloadLabelHolder>() {\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadLabelHolder {\n            val holder = DownloadLabelHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))\n            holder.itemView.setOnClickListener {\n                val index = holder.bindingAdapterPosition\n                val label1: String? = if (index == 0) {\n                    null\n                } else {\n                    mLabels[index]\n                }\n                if (!ObjectUtils.equal(label1, mLabel)) {\n                    mLabel = label1\n                    updateForLabel()\n                    updateView()\n                    closeDrawer(GravityCompat.END)\n                }\n            }\n            holder.itemView.setOnLongClickListener {\n                val index = holder.bindingAdapterPosition\n                if (index >= LABEL_OFFSET) {\n                    val popupMenu = PopupMenu(requireContext(), holder.option)\n                    popupMenu.inflate(R.menu.download_label_option)\n                    popupMenu.show()\n                    popupMenu.setOnMenuItemClickListener(\n                        object : PopupMenu.OnMenuItemClickListener {\n                            override fun onMenuItemClick(item: MenuItem): Boolean {\n                                val label = mLabels[index]\n                                when (item.itemId) {\n                                    R.id.menu_label_rename -> {\n                                        val builder = EditTextDialogBuilder(\n                                            requireContext(),\n                                            label,\n                                            getString(R.string.download_labels),\n                                        )\n                                        builder.setTitle(R.string.rename_label_title)\n                                        builder.setPositiveButton(android.R.string.ok, null)\n                                        val dialog = builder.show()\n                                        RenameLabelDialogHelper(builder, dialog, label)\n                                        return true\n                                    }\n                                    R.id.menu_label_remove -> {\n                                        AlertDialog.Builder(requireContext())\n                                            .setTitle(R.string.delete_label_title)\n                                            .setMessage(getString(R.string.delete_label_message, label))\n                                            .setPositiveButton(R.string.delete) { _: DialogInterface?, _: Int ->\n                                                // DownloadManager Actions\n                                                DownloadManager.deleteLabel(label)\n                                                mLabels.removeAt(index)\n                                                notifyItemRemoved(index)\n                                            }\n                                            .setNegativeButton(android.R.string.cancel, null)\n                                            .show()\n                                        return true\n                                    }\n                                }\n                                return false\n                            }\n                        },\n                    )\n                }\n                return@setOnLongClickListener true\n            }\n            return holder\n        }\n\n        @SuppressLint(\"SetTextI18n\")\n        override fun onBindViewHolder(holder: DownloadLabelHolder, position: Int) {\n            val index = holder.bindingAdapterPosition\n            val label = mLabels[index]\n            val list = when (position) {\n                0 -> {\n                    DownloadManager.allDownloadInfoList\n                }\n                1 -> {\n                    DownloadManager.defaultDownloadInfoList\n                }\n                else -> {\n                    DownloadManager.getLabelDownloadInfoList(label)\n                }\n            }\n            if (list != null) {\n                holder.label.text = label + \" [\" + list.size + \"]\"\n            } else {\n                holder.label.text = label\n            }\n            if (position < LABEL_OFFSET) {\n                holder.option.visibility = View.GONE\n            } else {\n                holder.option.visibility = View.VISIBLE\n            }\n        }\n\n        override fun getItemId(position: Int): Long = (if (position < LABEL_OFFSET) position else mLabels[position].hashCode()).toLong()\n\n        override fun getItemCount(): Int = mLabels.size\n    }\n\n    private inner class DeleteRangeDialogHelper(\n        private val mDownloadInfoList: List<DownloadInfo>,\n        private val mGidList: LongList,\n        private val mBuilder: CheckBoxDialogBuilder,\n    ) : DialogInterface.OnClickListener {\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            if (which != DialogInterface.BUTTON_POSITIVE) {\n                return\n            }\n\n            // Cancel check mode\n            if (mRecyclerView != null) {\n                mRecyclerView!!.outOfCustomChoiceMode()\n            }\n\n            // Delete\n            // DownloadManager Actions\n            DownloadManager.deleteRangeDownload(mGidList)\n\n            // Delete image files\n            val checked = mBuilder.isChecked\n            Settings.putRemoveImageFiles(checked)\n            if (checked) {\n                val files = arrayOfNulls<UniFile>(mDownloadInfoList.size)\n                for ((i, info) in mDownloadInfoList.withIndex()) {\n                    // Put file\n                    files[i] = SpiderDen.getGalleryDownloadDir(info.gid)\n                    // DB Actions\n                    DownloadManager.removeDownloadDirname(info.gid)\n                }\n                // Other Actions\n                lifecycleScope.launchIO {\n                    runCatching {\n                        files.forEach { it?.delete() }\n                    }\n                }\n            }\n        }\n    }\n\n    private inner class MoveDialogHelper(\n        private val mLabels: Array<String>,\n        private val mDownloadInfoList: List<DownloadInfo>,\n    ) : DialogInterface.OnClickListener {\n        @SuppressLint(\"NotifyDataSetChanged\")\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            // Cancel check mode\n            context ?: return\n            if (null != mRecyclerView) {\n                mRecyclerView!!.outOfCustomChoiceMode()\n            }\n            val label: String? = if (which == 0) {\n                null\n            } else {\n                mLabels[which]\n            }\n            // DownloadManager Actions\n            DownloadManager.changeLabel(mDownloadInfoList, label)\n            mLabelAdapter?.notifyDataSetChanged()\n        }\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private inner class DownloadHolder(itemView: View) :\n        RecyclerView.ViewHolder(itemView),\n        View.OnClickListener,\n        View.OnTouchListener {\n        val thumb: LoadImageView = itemView.findViewById(R.id.thumb)\n        val title: TextView = itemView.findViewById(R.id.title)\n        val uploader: TextView = itemView.findViewById(R.id.uploader)\n        val rating: SimpleRatingView = itemView.findViewById(R.id.rating)\n        val category: TextView = itemView.findViewById(R.id.category)\n        val start: View = itemView.findViewById(R.id.start)\n        val stop: View = itemView.findViewById(R.id.stop)\n        val move: View = itemView.findViewById(R.id.move)\n        val state: TextView = itemView.findViewById(R.id.state)\n        val progressBar: ProgressBar = itemView.findViewById(R.id.progress_bar)\n        val percent: TextView = itemView.findViewById(R.id.percent)\n        val speed: TextView = itemView.findViewById(R.id.speed)\n\n        init {\n            // TODO cancel on click listener when select items\n            thumb.setOnClickListener(this)\n            start.setOnClickListener(this)\n            stop.setOnClickListener(this)\n            move.setOnTouchListener(this)\n        }\n\n        override fun onClick(v: View) {\n            val context = context\n            val activity: Activity? = mainActivity\n            val recyclerView = mRecyclerView\n            if (null == context || null == activity || null == recyclerView || recyclerView.isInCustomChoice) {\n                return\n            }\n            val list = mList ?: return\n            val size = list.size\n            val index = recyclerView.getChildAdapterPosition(itemView)\n            if (index < 0 || index >= size) {\n                return\n            }\n            if (thumb === v) {\n                val args = Bundle()\n                args.putString(\n                    GalleryDetailScene.KEY_ACTION,\n                    GalleryDetailScene.ACTION_GALLERY_INFO,\n                )\n                args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, list[index])\n                val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)\n                announcer.setTranHelper(EnterGalleryDetailTransaction(thumb))\n                startScene(announcer)\n            } else if (start === v) {\n                val intent = Intent(activity, DownloadService::class.java)\n                intent.action = DownloadService.ACTION_START\n                intent.putExtra(DownloadService.KEY_GALLERY_INFO, list[index])\n                ContextCompat.startForegroundService(activity, intent)\n            } else if (stop === v) {\n                // DownloadManager Actions\n                DownloadManager.stopDownload(list[index].gid)\n            }\n        }\n\n        override fun onTouch(v: View, event: MotionEvent): Boolean {\n            if (mItemTouchHelper != null && event.action == MotionEvent.ACTION_DOWN) {\n                mItemTouchHelper!!.startDrag(this)\n            }\n            return false\n        }\n    }\n\n    private inner class DownloadAdapter : RecyclerView.Adapter<DownloadHolder>() {\n        private val mInflater: LayoutInflater = layoutInflater\n        private val mListThumbWidth: Int\n        private val mListThumbHeight: Int\n\n        init {\n            @SuppressLint(\"InflateParams\")\n            val calculator =\n                mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)\n            ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)\n            mListThumbHeight = calculator.measuredHeight\n            mListThumbWidth = mListThumbHeight * 2 / 3\n        }\n\n        override fun getItemId(position: Int): Long = if (mList == null || position < 0 || position >= mList!!.size) {\n            0\n        } else {\n            mList!![position].gid\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {\n            val holder = DownloadHolder(mInflater.inflate(R.layout.item_download, parent, false))\n            val lp = holder.thumb.layoutParams\n            lp.width = mListThumbWidth\n            lp.height = mListThumbHeight\n            holder.thumb.layoutParams = lp\n            return holder\n        }\n\n        override fun onBindViewHolder(holder: DownloadHolder, position: Int) {\n            if (mList == null) {\n                return\n            }\n            val info = mList!![holder.bindingAdapterPosition]\n            info.thumb?.let {\n                holder.thumb.load(\n                    getThumbKey(info.gid),\n                    encodeMagicRequest(info),\n                    hardware = false,\n                )\n            }\n            holder.title.text = EhUtils.getSuitableTitle(info)\n            holder.uploader.text = info.uploader\n            holder.rating.rating = info.rating\n            val category = holder.category\n            val newCategoryText = EhUtils.getCategory(info.category)\n            if (!newCategoryText.contentEquals(category.text)) {\n                category.text = newCategoryText\n                category.setBackgroundColor(EhUtils.getCategoryColor(info.category))\n            }\n            bindForState(holder, info)\n\n            // Update transition name\n            ViewCompat.setTransitionName(\n                holder.thumb,\n                TransitionNameFactory.getThumbTransitionName(info.gid),\n            )\n            holder.itemView.setOnClickListener {\n                if (mainActivity != null && mRecyclerView != null && mList != null) {\n                    val index = holder.bindingAdapterPosition\n                    if (mRecyclerView!!.isInCustomChoice) {\n                        mRecyclerView!!.toggleItemChecked(index)\n                    } else {\n                        if (index in 0 until mList!!.size) {\n                            val intent = Intent(mainActivity!!, GalleryActivity::class.java)\n                            intent.action = GalleryActivity.ACTION_EH\n                            intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mList!![index])\n                            startActivity(intent)\n                        }\n                    }\n                }\n            }\n            holder.itemView.setOnLongClickListener {\n                if (mRecyclerView != null) {\n                    if (!mRecyclerView!!.isInCustomChoice) {\n                        mRecyclerView!!.intoCustomChoiceMode()\n                    }\n                    mRecyclerView!!.toggleItemChecked(holder.bindingAdapterPosition)\n                    return@setOnLongClickListener true\n                }\n                return@setOnLongClickListener false\n            }\n        }\n\n        override fun onBindViewHolder(\n            holder: DownloadHolder,\n            position: Int,\n            payloads: MutableList<Any>,\n        ) {\n            if (payloads.any { it == PAYLOAD_STATE }) {\n                mList?.let { bindForState(holder, it[position]) }\n            } else {\n                super.onBindViewHolder(holder, position, payloads)\n            }\n        }\n\n        override fun getItemCount(): Int = if (mList == null) 0 else mList!!.size\n    }\n\n    private inner class DownloadChoiceListener : CustomChoiceListener {\n        override fun onIntoCustomChoice(view: EasyRecyclerView) {\n            if (mFabLayout != null) {\n                mFabLayout!!.isExpanded = true\n            }\n            // Lock drawer\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)\n        }\n\n        override fun onOutOfCustomChoice(view: EasyRecyclerView) {\n            if (mFabLayout != null) {\n                mFabLayout!!.isExpanded = false\n            }\n            // Unlock drawer\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)\n        }\n\n        override fun onItemCheckedStateChanged(\n            view: EasyRecyclerView,\n            position: Int,\n            id: Long,\n            checked: Boolean,\n        ) {\n            if (view.checkedItemCount == 0) {\n                view.outOfCustomChoiceMode()\n            }\n        }\n    }\n\n    private inner class RenameLabelDialogHelper(\n        private val mBuilder: EditTextDialogBuilder,\n        private val mDialog: AlertDialog,\n        private val mOriginalLabel: String?,\n    ) : View.OnClickListener {\n        init {\n            val button: Button = mDialog.getButton(DialogInterface.BUTTON_POSITIVE)\n            button.setOnClickListener(this)\n        }\n\n        @SuppressLint(\"NotifyDataSetChanged\")\n        override fun onClick(v: View) {\n            context ?: return\n            val text = mBuilder.text\n            if (TextUtils.isEmpty(text)) {\n                mBuilder.setError(getString(R.string.label_text_is_empty))\n            } else if (getString(R.string.download_all) == text || getString(R.string.default_download_label_name) == text) {\n                mBuilder.setError(getString(R.string.label_text_is_invalid))\n            } else if (DownloadManager.containLabel(text)) {\n                mBuilder.setError(getString(R.string.label_text_exist))\n            } else {\n                mBuilder.setError(null)\n                mDialog.dismiss()\n                // DownloadManager Actions\n                DownloadManager.renameLabel(mOriginalLabel!!, text)\n                if (mLabelAdapter != null) {\n                    initLabels()\n                    mLabelAdapter!!.notifyDataSetChanged()\n                }\n            }\n        }\n    }\n\n    private inner class NewLabelDialogHelper(\n        private val mBuilder: EditTextDialogBuilder,\n        private val mDialog: AlertDialog,\n    ) : View.OnClickListener {\n        init {\n            val button: Button = mDialog.getButton(DialogInterface.BUTTON_POSITIVE)\n            button.setOnClickListener(this)\n        }\n\n        override fun onClick(v: View) {\n            context ?: return\n            val text = mBuilder.text\n            if (TextUtils.isEmpty(text)) {\n                mBuilder.setError(getString(R.string.label_text_is_empty))\n            } else if (getString(R.string.download_all) == text || getString(R.string.default_download_label_name) == text) {\n                mBuilder.setError(getString(R.string.label_text_is_invalid))\n            } else if (DownloadManager.containLabel(text)) {\n                mBuilder.setError(getString(R.string.label_text_exist))\n            } else {\n                mBuilder.setError(null)\n                mDialog.dismiss()\n                // DownloadManager Actions\n                DownloadManager.addLabel(text)\n                initLabels()\n                mLabelAdapter?.notifyItemInserted(mLabels.size - 1)\n            }\n        }\n    }\n\n    private inner class DownloadLabelItemTouchHelperCallback : ItemTouchHelper.Callback() {\n        override fun getMovementFlags(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n        ): Int {\n            val position = viewHolder.bindingAdapterPosition\n            return if (position < LABEL_OFFSET) {\n                makeMovementFlags(0, 0)\n            } else {\n                makeMovementFlags(\n                    ItemTouchHelper.UP or ItemTouchHelper.DOWN,\n                    0,\n                )\n            }\n        }\n\n        override fun isLongPressDragEnabled(): Boolean = false\n\n        override fun onMove(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n            target: RecyclerView.ViewHolder,\n        ): Boolean {\n            val fromPosition = viewHolder.bindingAdapterPosition\n            val toPosition = target.bindingAdapterPosition\n            if (fromPosition == toPosition || toPosition < LABEL_OFFSET) {\n                return false\n            }\n            // DownloadManager Actions\n            DownloadManager.moveLabel(fromPosition - LABEL_OFFSET, toPosition - LABEL_OFFSET)\n            val item = mLabels.removeAt(fromPosition)\n            mLabels.add(toPosition, item)\n            mLabelAdapter?.notifyItemMoved(fromPosition, toPosition)\n            return true\n        }\n\n        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}\n    }\n\n    private inner class DownloadItemTouchHelperCallback : ItemTouchHelper.Callback() {\n        override fun getMovementFlags(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n        ): Int = makeMovementFlags(\n            ItemTouchHelper.UP or ItemTouchHelper.DOWN,\n            0,\n        )\n\n        override fun isLongPressDragEnabled(): Boolean = false\n\n        override fun onMove(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n            target: RecyclerView.ViewHolder,\n        ): Boolean {\n            val fromPosition = viewHolder.bindingAdapterPosition\n            val toPosition = target.bindingAdapterPosition\n            if (fromPosition == toPosition) {\n                return false\n            }\n            // DownloadManager Actions\n            when (mLabel) {\n                null -> {\n                    DownloadManager.moveDownload(fromPosition, toPosition)\n                }\n                getString(R.string.default_download_label_name) -> {\n                    DownloadManager.moveDownload(null, fromPosition, toPosition)\n                }\n                else -> {\n                    DownloadManager.moveDownload(mLabel, fromPosition, toPosition)\n                }\n            }\n            mAdapter?.notifyItemMoved(fromPosition, toPosition)\n            return true\n        }\n\n        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}\n    }\n\n    companion object {\n        const val KEY_GID = \"gid\"\n        const val KEY_ACTION = \"action\"\n        const val ACTION_CLEAR_DOWNLOAD_SERVICE = \"clear_download_service\"\n        private val PATTERN_AUTHOR = Regex(\"^(?:\\\\([^\\\\[\\\\]()]+\\\\))?\\\\s*\\\\[([^\\\\[\\\\]]+)]\")\n        private val PATTERN_NAME = Regex(\"^(?:\\\\([^\\\\[\\\\]()]+\\\\))?\\\\s*(?:\\\\[[^\\\\[\\\\]]+])?\\\\s*(.+)\")\n        private const val KEY_LABEL = \"label\"\n        private const val LABEL_OFFSET = 2\n        private const val PAYLOAD_STATE = 0\n\n        private fun getAuthor(title: String): String {\n            val matcher = PATTERN_AUTHOR.find(title) ?: return \"\"\n            return matcher.groupValues[1].trim { it <= ' ' }\n        }\n\n        private fun getName(title: String): String {\n            val matcher = PATTERN_NAME.find(title) ?: return title\n            return matcher.groupValues[1].trim { it <= ' ' }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/EhCallback.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.content.Context\nimport android.widget.Toast\nimport androidx.annotation.StringRes\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.ui.MainActivity\nimport com.hippo.scene.SceneFragment\n\nabstract class EhCallback<E : SceneFragment?, T>(\n    context: Context,\n) : EhClient.Callback<T> {\n    val application: EhApplication = context.applicationContext as EhApplication\n\n    val content: Context\n        get() {\n            val context = application.topActivity\n            return context ?: application\n        }\n\n    fun showTip(@StringRes id: Int, length: Int) {\n        val activity = content\n        if (activity is MainActivity) {\n            activity.showTip(id, length)\n        } else {\n            Toast.makeText(\n                application,\n                id,\n                if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,\n            ).show()\n        }\n    }\n\n    fun showTip(tip: String, length: Int) {\n        val activity = content\n        if (activity is MainActivity) {\n            activity.showTip(tip, length)\n        } else {\n            Toast.makeText(\n                application,\n                tip,\n                if (length == BaseScene.LENGTH_LONG) Toast.LENGTH_LONG else Toast.LENGTH_SHORT,\n            ).show()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/EnterGalleryDetailTransaction.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.content.Context\nimport android.view.View\nimport androidx.core.view.ViewCompat\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentTransaction\nimport androidx.transition.TransitionInflater\nimport com.hippo.ehviewer.R\nimport com.hippo.scene.TransitionHelper\n\nclass EnterGalleryDetailTransaction(\n    private val mThumb: View?,\n) : TransitionHelper {\n    override fun onTransition(\n        context: Context,\n        transaction: FragmentTransaction,\n        exit: Fragment,\n        enter: Fragment,\n    ): Boolean {\n        if (mThumb == null || enter !is GalleryDetailScene) {\n            return false\n        }\n        ViewCompat.getTransitionName(mThumb)?.let {\n            exit.sharedElementReturnTransition =\n                TransitionInflater.from(context).inflateTransition(R.transition.trans_move)\n            exit.exitTransition =\n                TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)\n            enter.sharedElementEnterTransition =\n                TransitionInflater.from(context).inflateTransition(R.transition.trans_move)\n            enter.enterTransition =\n                TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)\n            transaction.addSharedElement(mThumb, it)\n        }\n        return true\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/FavoritesScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.res.Resources\nimport android.net.Uri\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.content.res.AppCompatResources\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.util.size\nimport androidx.core.view.GravityCompat\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowInsetsAnimationCompat\nimport androidx.drawerlayout.widget.DrawerLayout\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.datepicker.CalendarConstraints\nimport com.google.android.material.datepicker.CalendarConstraints.DateValidator\nimport com.google.android.material.datepicker.CompositeDateValidator\nimport com.google.android.material.datepicker.DateValidatorPointBackward\nimport com.google.android.material.datepicker.DateValidatorPointForward\nimport com.google.android.material.datepicker.MaterialDatePicker\nimport com.google.android.material.floatingactionbutton.FloatingActionButton\nimport com.hippo.drawable.AddDeleteDrawable\nimport com.hippo.drawable.DrawerArrowDrawable\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.EasyRecyclerView.CustomChoiceListener\nimport com.hippo.easyrecyclerview.FastScroller.OnDragHandlerListener\nimport com.hippo.easyrecyclerview.LinearDividerItemDecoration\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.WindowInsetsAnimationHelper\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.data.FavListUrlBuilder\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.parser.FavoritesParser\nimport com.hippo.ehviewer.ui.CommonOperations\nimport com.hippo.ehviewer.widget.GalleryInfoContentHelper\nimport com.hippo.ehviewer.widget.SearchBar\nimport com.hippo.scene.Announcer\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.util.toEpochMillis\nimport com.hippo.widget.ContentLayout\nimport com.hippo.widget.FabLayout\nimport com.hippo.widget.FabLayout.OnClickFabListener\nimport com.hippo.widget.FabLayout.OnExpandListener\nimport com.hippo.widget.SearchBarMover\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.ObjectUtils\nimport com.hippo.yorozuya.SimpleHandler\nimport com.hippo.yorozuya.ViewUtils\nimport kotlin.time.Clock\nimport kotlinx.datetime.DateTimeUnit\nimport kotlinx.datetime.LocalDate\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.minus\nimport kotlinx.datetime.todayIn\nimport rikka.core.res.resolveColor\n\n@SuppressLint(\"NotifyDataSetChanged\", \"RtlHardcoded\")\nclass FavoritesScene :\n    BaseScene(),\n    OnDragHandlerListener,\n    SearchBarMover.Helper,\n    SearchBar.Helper,\n    OnClickFabListener,\n    OnExpandListener,\n    CustomChoiceListener {\n    // For modify action\n    private val mModifyGiList: MutableList<GalleryInfo> = ArrayList()\n    var current = 0 // -1 for error\n    var limit = 0 // -1 for error\n    private var mRecyclerView: EasyRecyclerView? = null\n    private var mSearchBar: SearchBar? = null\n    private var mFabLayout: FabLayout? = null\n    private var mAdapter: FavoritesAdapter? = null\n    private var mHelper: FavoritesHelper? = null\n    private var mSearchBarMover: SearchBarMover? = null\n    private var mLeftDrawable: DrawerArrowDrawable? = null\n    private var mActionFabDrawable: AddDeleteDrawable? = null\n    private var mDrawerLayout: DrawerLayout? = null\n    private var mDrawerAdapter: FavDrawerAdapter? = null\n    private var mClient: EhClient? = null\n    private var mFavCatArray: Array<String>? = Settings.favCat\n    private var mFavCountArray: IntArray? = Settings.favCount\n    private var mUrlBuilder: FavListUrlBuilder? = null\n    private val showNormalFabsRunnable = Runnable {\n        if (mFabLayout != null) {\n            updateJumpFab() // Index: 1, 2\n            mFabLayout!!.setSecondaryFabVisibilityAt(0, true)\n            mFabLayout!!.setSecondaryFabVisibilityAt(3, true)\n            mFabLayout!!.setSecondaryFabVisibilityAt(4, false)\n            mFabLayout!!.setSecondaryFabVisibilityAt(5, false)\n            mFabLayout!!.setSecondaryFabVisibilityAt(6, false)\n            mFabLayout!!.setSecondaryFabVisibilityAt(7, false)\n        }\n    }\n    private var mFavLocalCount = 0\n    private var mFavCountSum = 0\n    private var mHasFirstRefresh = false\n    private var mSearchMode = false\n\n    // Avoid unnecessary search bar update\n    private var mOldFavCat: String? = null\n    private var mOldKeyword: String? = null\n\n    // For modify action\n    private var mEnableModify = false\n    private var mModifyAdd = false\n    private var mModifyFavCat = 0\n    private var mFavSlot = -2\n\n    override fun getNavCheckedItem(): Int = R.id.nav_favourite\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        mClient = EhClient\n        mFavLocalCount = Settings.favLocalCount\n        mFavCountSum = Settings.favCloudCount\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        mAdapter?.type = Settings.listMode\n    }\n\n    private fun onInit() {\n        mUrlBuilder = FavListUrlBuilder()\n        mUrlBuilder!!.favCat = Settings.recentFavCat\n        mSearchMode = false\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mUrlBuilder = savedInstanceState.getParcelableCompat(KEY_URL_BUILDER)\n        if (mUrlBuilder == null) {\n            mUrlBuilder = FavListUrlBuilder()\n        }\n        mSearchMode = savedInstanceState.getBoolean(KEY_SEARCH_MODE)\n        mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)\n        mFavCountArray = savedInstanceState.getIntArray(KEY_FAV_COUNT_ARRAY)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        val hasFirstRefresh: Boolean = if (mHelper != null && 1 == mHelper!!.shownViewIndex) {\n            false\n        } else {\n            mHasFirstRefresh\n        }\n        outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh)\n        outState.putParcelable(KEY_URL_BUILDER, mUrlBuilder)\n        outState.putBoolean(KEY_SEARCH_MODE, mSearchMode)\n        outState.putIntArray(KEY_FAV_COUNT_ARRAY, mFavCountArray)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mClient = null\n        mFavCatArray = null\n        mFavCountArray = null\n        mFavCountSum = 0\n        mUrlBuilder = null\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_favorites, container, false)\n        val context = requireContext()\n        val mContentLayout = view.findViewById<ContentLayout>(R.id.content_layout)\n        val activity = mainActivity!!\n        mDrawerLayout = ViewUtils.`$$`(activity, R.id.draw_view) as DrawerLayout\n        mRecyclerView = mContentLayout.recyclerView\n        val fastScroller = mContentLayout.fastScroller\n        mSearchBar = ViewUtils.`$$`(view, R.id.search_bar) as SearchBar\n        mFabLayout = ViewUtils.`$$`(view, R.id.fab_layout) as FabLayout\n        ViewCompat.setWindowInsetsAnimationCallback(\n            view,\n            WindowInsetsAnimationHelper(\n                WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,\n                mFabLayout,\n            ),\n        )\n        val paddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)\n        mHelper = FavoritesHelper()\n        mHelper!!.setEmptyString(resources.getString(R.string.gallery_list_empty_hit))\n        mContentLayout.setHelper(mHelper!!)\n        mContentLayout.fastScroller.setOnDragHandlerListener(this)\n        mContentLayout.setFitPaddingTop(paddingTopSB)\n        mAdapter = FavoritesAdapter(inflater, resources, mRecyclerView!!, Settings.listMode)\n        mRecyclerView!!.clipToPadding = false\n        mRecyclerView!!.clipChildren = false\n        mRecyclerView!!.setChoiceMode(EasyRecyclerView.CHOICE_MODE_MULTIPLE_CUSTOM)\n        mRecyclerView!!.setCustomCheckedListener(this)\n        fastScroller.setPadding(\n            fastScroller.paddingLeft,\n            fastScroller.paddingTop + paddingTopSB,\n            fastScroller.paddingRight,\n            fastScroller.paddingBottom,\n        )\n        mLeftDrawable = DrawerArrowDrawable(context, theme.resolveColor(android.R.attr.colorControlNormal))\n        mSearchBar!!.setLeftDrawable(mLeftDrawable!!)\n        mSearchBar!!.setRightDrawable(AppCompatResources.getDrawable(context, R.drawable.v_magnify_x24)!!)\n        mSearchBar!!.setHelper(this)\n        mSearchBar!!.setAllowEmptySearch(false)\n        updateSearchBar()\n        updateJumpFab()\n        mSearchBarMover = SearchBarMover(this, mSearchBar, mRecyclerView)\n        mActionFabDrawable = AddDeleteDrawable(context, context.getColor(R.color.primary_drawable_dark))\n        mFabLayout!!.primaryFab!!.setImageDrawable(mActionFabDrawable)\n        mFabLayout!!.setExpanded(expanded = false, animation = false)\n        mFabLayout!!.setAutoCancel(true)\n        mFabLayout!!.setHidePrimaryFab(false)\n        mFabLayout!!.setOnClickFabListener(this)\n        mFabLayout!!.setOnExpandListener(this)\n        addAboveSnackView(mFabLayout!!)\n\n        // Restore search mode\n        if (mSearchMode) {\n            mSearchMode = false\n            enterSearchMode(false)\n        }\n\n        // Only refresh for the first time\n        if (!mHasFirstRefresh) {\n            mHasFirstRefresh = true\n            mHelper!!.firstRefresh()\n        }\n        return view\n    }\n\n    // keyword of mUrlBuilder, fav cat of mUrlBuilder, mFavCatArray.\n    // They changed, call it\n    private fun updateSearchBar() {\n        val context = context\n        if (null == context || null == mUrlBuilder || null == mSearchBar || null == mFavCatArray) {\n            return\n        }\n\n        // Update title\n        val favCatName: String = when (val favCat = mUrlBuilder!!.favCat) {\n            in 0..9 -> {\n                mFavCatArray!![favCat]\n            }\n            FavListUrlBuilder.FAV_CAT_LOCAL -> {\n                getString(R.string.local_favorites)\n            }\n            else -> {\n                getString(R.string.cloud_favorites)\n            }\n        }\n        val keyword = mUrlBuilder!!.keyword\n        if (TextUtils.isEmpty(keyword)) {\n            if (!ObjectUtils.equal(favCatName, mOldFavCat)) {\n                mSearchBar!!.setTitle(getString(R.string.favorites_title, favCatName))\n            }\n        } else {\n            if (!ObjectUtils.equal(favCatName, mOldFavCat) ||\n                !ObjectUtils.equal(\n                    keyword,\n                    mOldKeyword,\n                )\n            ) {\n                mSearchBar!!.setTitle(getString(R.string.favorites_title_2, favCatName, keyword))\n            }\n        }\n\n        // Update hint\n        if (!ObjectUtils.equal(favCatName, mOldFavCat)) {\n            mSearchBar!!.setEditTextHint(getString(R.string.favorites_search_bar_hint, favCatName))\n        }\n        mOldFavCat = favCatName\n        mOldKeyword = keyword\n\n        // Save recent fav cat\n        Settings.putRecentFavCat(mUrlBuilder!!.favCat)\n    }\n\n    // Hide jump fab on local fav cat\n    private fun updateJumpFab() {\n        if (mFabLayout != null && mUrlBuilder != null) {\n            mFabLayout!!.setSecondaryFabVisibilityAt(\n                1,\n                mUrlBuilder!!.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,\n            )\n            mFabLayout!!.setSecondaryFabVisibilityAt(\n                2,\n                mUrlBuilder!!.favCat != FavListUrlBuilder.FAV_CAT_LOCAL,\n            )\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        if (null != mHelper) {\n            mHelper!!.destroy()\n            if (1 == mHelper!!.shownViewIndex) {\n                mHasFirstRefresh = false\n            }\n        }\n        if (null != mRecyclerView) {\n            mRecyclerView!!.stopScroll()\n            mRecyclerView = null\n        }\n        if (null != mFabLayout) {\n            removeAboveSnackView(mFabLayout!!)\n            mFabLayout = null\n        }\n        mAdapter = null\n        mSearchBar = null\n        mSearchBarMover = null\n        mLeftDrawable = null\n        mOldFavCat = null\n        mOldKeyword = null\n    }\n\n    override fun onCreateDrawerView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? {\n        val view = inflater.inflate(R.layout.drawer_list_rv, container, false)\n        val context = requireContext()\n        val toolbar = ViewUtils.`$$`(view, R.id.toolbar) as Toolbar\n        toolbar.setTitle(R.string.collections)\n        toolbar.inflateMenu(R.menu.drawer_favorites)\n        toolbar.setOnMenuItemClickListener { item: MenuItem ->\n            val id = item.itemId\n            if (id == R.id.action_default_favorites_slot) {\n                val items = arrayOfNulls<String>(12)\n                items[0] = getString(R.string.let_me_select)\n                items[1] = getString(R.string.local_favorites)\n                val favCat = Settings.favCat\n                System.arraycopy(favCat, 0, items, 2, 10)\n                AlertDialog.Builder(context)\n                    .setTitle(R.string.default_favorites_collection)\n                    .setItems(items) { _: DialogInterface?, which: Int ->\n                        Settings.putDefaultFavSlot(\n                            which - 2,\n                        )\n                        if (which == 0) {\n                            Settings.putNeverAddFavNotes(false)\n                        }\n                    }\n                    .show()\n                return@setOnMenuItemClickListener true\n            }\n            false\n        }\n        val recyclerView = view.findViewById<EasyRecyclerView>(R.id.recycler_view_drawer)\n        recyclerView.layoutManager = LinearLayoutManager(context)\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(context, 1f),\n        )\n        decoration.setShowLastDivider(true)\n        recyclerView.addItemDecoration(decoration)\n        mDrawerAdapter = FavDrawerAdapter(inflater)\n        recyclerView.adapter = mDrawerAdapter\n        return view\n    }\n\n    override fun onDestroyDrawerView() {\n        super.onDestroyDrawerView()\n        mDrawerAdapter = null\n    }\n\n    override fun onBackPressed() {\n        if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n            mRecyclerView!!.outOfCustomChoiceMode()\n        } else if (mFabLayout != null && mFabLayout!!.isExpanded) {\n            mFabLayout!!.toggle()\n        } else if (mSearchMode) {\n            exitSearchMode()\n        } else {\n            finish()\n        }\n    }\n\n    override fun onStartDragHandler() {\n        // Lock right drawer\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)\n    }\n\n    override fun onEndDragHandler() {\n        // Restore right drawer\n        if (null != mRecyclerView && !mRecyclerView!!.isInCustomChoice) {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)\n        }\n        mSearchBarMover?.returnSearchBarPosition()\n    }\n\n    fun onItemClick(view: View, position: Int): Boolean {\n        if (mDrawerLayout != null && mDrawerLayout!!.isDrawerOpen(GravityCompat.END)) {\n            // Skip if in search mode\n            if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n                return false\n            }\n            if (mUrlBuilder == null || mHelper == null) {\n                return false\n            }\n\n            // Local favorite position is 0, All favorite position is 1, so position - 2 is OK\n            val newFavCat = position - 2\n\n            // Check is the same\n            if (mUrlBuilder!!.favCat == newFavCat) {\n                return true\n            }\n            exitSearchMode()\n            mUrlBuilder!!.keyword = null\n            mUrlBuilder!!.favCat = newFavCat\n            updateSearchBar()\n            updateJumpFab()\n            mHelper!!.refresh()\n            closeDrawer(GravityCompat.END)\n        } else {\n            if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n                mRecyclerView!!.toggleItemChecked(position)\n            } else if (mHelper != null) {\n                val gi = mHelper!!.getDataAtEx(position) ?: return false\n                val args = Bundle()\n                args.putString(\n                    GalleryDetailScene.KEY_ACTION,\n                    GalleryDetailScene.ACTION_GALLERY_INFO,\n                )\n                args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi)\n                val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)\n                view.findViewById<View>(R.id.thumb)?.let {\n                    announcer.setTranHelper(EnterGalleryDetailTransaction(it))\n                }\n                startScene(announcer)\n            }\n        }\n        return true\n    }\n\n    fun onItemLongClick(position: Int): Boolean {\n        // Can not into\n        if (mRecyclerView != null && !mSearchMode) {\n            if (!mRecyclerView!!.isInCustomChoice) {\n                mRecyclerView!!.intoCustomChoiceMode()\n            }\n            mRecyclerView!!.toggleItemChecked(position)\n        }\n        return true\n    }\n\n    // SearchBarMover.Helper\n    override fun isValidView(recyclerView: RecyclerView): Boolean = recyclerView == mRecyclerView\n\n    // SearchBarMover.Helper\n    override fun getValidRecyclerView(): RecyclerView? = mRecyclerView\n\n    // SearchBarMover.Helper\n    override fun forceShowSearchBar(): Boolean = false\n\n    // SearchBar.Helper\n    override fun onClickTitle() {\n        // Skip if in search mode\n        if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n            return\n        }\n        if (!mSearchMode) {\n            enterSearchMode(true)\n        }\n    }\n\n    // SearchBar.Helper\n    override fun onClickLeftIcon() {\n        // Skip if in search mode\n        if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n            return\n        }\n        if (mSearchMode) {\n            exitSearchMode()\n        } else {\n            toggleDrawer(GravityCompat.START)\n        }\n    }\n\n    // SearchBar.Helper\n    override fun onClickRightIcon() {\n        // Skip if in search mode\n        if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n            return\n        }\n        if (!mSearchMode) {\n            enterSearchMode(true)\n        } else if (mSearchBar != null) {\n            if (mSearchBar!!.getEditText().length() == 0) {\n                exitSearchMode()\n            } else {\n                mSearchBar!!.applySearch()\n            }\n        }\n    }\n\n    // SearchBar.Helper\n    override fun onSearchEditTextClick() {}\n\n    // SearchBar.Helper\n    override fun onApplySearch(query: String) {\n        // Skip if in search mode\n        if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n            return\n        }\n        if (mUrlBuilder == null || mHelper == null) {\n            return\n        }\n        exitSearchMode()\n        mUrlBuilder!!.keyword = query\n        updateSearchBar()\n        mHelper!!.refresh()\n    }\n\n    // SearchBar.Helper\n    override fun onSearchEditTextBackPressed() {\n        onBackPressed()\n    }\n\n    // SearchBar.Helper\n    override fun onReceiveContent(uri: Uri?) {}\n\n    override fun onExpand(expanded: Boolean) {\n        if (expanded) {\n            mActionFabDrawable!!.setDelete(ANIMATE_TIME)\n        } else {\n            mActionFabDrawable!!.setAdd(ANIMATE_TIME)\n        }\n    }\n\n    override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {\n        if (mRecyclerView != null && mFabLayout != null) {\n            if (mRecyclerView!!.isInCustomChoice) {\n                mRecyclerView!!.outOfCustomChoiceMode()\n            } else {\n                mFabLayout!!.toggle()\n            }\n        }\n    }\n\n    private fun showGoToDialog() {\n        val context = context\n        if (null == context || null == mHelper) {\n            return\n        }\n        val initial = LocalDate(2007, 3, 21)\n        val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)\n        val initialMillis = initial.toEpochMillis()\n        val yesterdayMillis = yesterday.toEpochMillis()\n        val listValidators = ArrayList<DateValidator>()\n        listValidators.add(DateValidatorPointForward.from(initialMillis))\n        listValidators.add(DateValidatorPointBackward.before(yesterdayMillis))\n        val constraintsBuilder = CalendarConstraints.Builder()\n            .setStart(initialMillis)\n            .setEnd(yesterdayMillis)\n            .setValidator(CompositeDateValidator.allOf(listValidators))\n        val datePicker = MaterialDatePicker.Builder.datePicker()\n            .setCalendarConstraints(constraintsBuilder.build())\n            .setTitleText(R.string.go_to)\n            .setSelection(yesterdayMillis)\n            .build()\n        datePicker.show(requireActivity().supportFragmentManager, \"date-picker\")\n        datePicker.addOnPositiveButtonClickListener { v: Long? ->\n            mHelper!!.goTo(\n                v!!,\n                true,\n            )\n        }\n    }\n\n    override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {\n        val context = context\n        if (null == context || null == mRecyclerView || null == mHelper) {\n            return\n        }\n        if (!mRecyclerView!!.isInCustomChoice) {\n            when (position) {\n                // Open right\n                0 -> openDrawer(GravityCompat.END)\n                // Go to\n                1 -> showGoToDialog()\n                // Last page\n                2 -> mHelper!!.goTo(\"1-0\", false)\n                // Refresh\n                3 -> mHelper!!.refresh()\n            }\n            view.isExpanded = false\n            return\n        }\n        mModifyGiList.clear()\n        mRecyclerView!!.checkedItemPositions?.let {\n            for (i in 0 until it.size) {\n                if (it.valueAt(i)) {\n                    mHelper!!.getDataAtEx(it.keyAt(i))?.let { gi ->\n                        mModifyGiList.add(gi)\n                    }\n                }\n            }\n        }\n        when (position) {\n            // Check all\n            4 -> mRecyclerView!!.checkAll()\n            // Download\n            5 -> {\n                val activity: Activity? = mainActivity\n                if (activity != null) {\n                    // CommonOperations Actions\n                    CommonOperations.startDownload(mainActivity!!, mModifyGiList, false)\n                }\n                mModifyGiList.clear()\n                if (mRecyclerView != null && mRecyclerView!!.isInCustomChoice) {\n                    mRecyclerView!!.outOfCustomChoiceMode()\n                }\n            }\n            // Delete\n            6 -> {\n                val helper = DeleteDialogHelper()\n                AlertDialog.Builder(context)\n                    .setTitle(R.string.delete_favorites_dialog_title)\n                    .setMessage(\n                        getString(\n                            R.string.delete_favorites_dialog_message,\n                            mModifyGiList.size,\n                        ),\n                    )\n                    .setPositiveButton(android.R.string.ok, helper)\n                    .setOnCancelListener(helper)\n                    .show()\n            }\n            // Move\n            7 -> {\n                val helper = MoveDialogHelper()\n                // First is local favorite, the other 10 is cloud favorite\n                val array = arrayOfNulls<String>(11)\n                array[0] = getString(R.string.local_favorites)\n                System.arraycopy(Settings.favCat, 0, array, 1, 10)\n                AlertDialog.Builder(context)\n                    .setTitle(R.string.move_favorites_dialog_title)\n                    .setItems(array, helper)\n                    .setOnCancelListener(helper)\n                    .show()\n            }\n        }\n    }\n\n    private fun showNormalFabs() {\n        // Delay showing normal fabs to avoid mutation\n        SimpleHandler.getInstance().removeCallbacks(showNormalFabsRunnable)\n        SimpleHandler.getInstance().postDelayed(showNormalFabsRunnable, 300)\n    }\n\n    private fun showSelectionFabs() {\n        SimpleHandler.getInstance().removeCallbacks(showNormalFabsRunnable)\n        if (mFabLayout != null) {\n            mFabLayout!!.setSecondaryFabVisibilityAt(0, false)\n            mFabLayout!!.setSecondaryFabVisibilityAt(1, false)\n            mFabLayout!!.setSecondaryFabVisibilityAt(2, false)\n            mFabLayout!!.setSecondaryFabVisibilityAt(3, false)\n            mFabLayout!!.setSecondaryFabVisibilityAt(4, true)\n            mFabLayout!!.setSecondaryFabVisibilityAt(5, true)\n            mFabLayout!!.setSecondaryFabVisibilityAt(6, true)\n            mFabLayout!!.setSecondaryFabVisibilityAt(7, true)\n        }\n    }\n\n    override fun onIntoCustomChoice(view: EasyRecyclerView) {\n        if (mFabLayout != null) {\n            showSelectionFabs()\n            mFabLayout!!.setAutoCancel(false)\n            // Delay expanding action to make layout work fine\n            SimpleHandler.getInstance().post { mFabLayout!!.isExpanded = true }\n        }\n        mHelper?.setRefreshLayoutEnable(false)\n        // Lock drawer\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)\n    }\n\n    override fun onOutOfCustomChoice(view: EasyRecyclerView) {\n        if (mFabLayout != null) {\n            showNormalFabs()\n            mFabLayout!!.setAutoCancel(true)\n            mFabLayout!!.isExpanded = false\n        }\n        mHelper?.setRefreshLayoutEnable(true)\n        // Unlock drawer\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)\n    }\n\n    override fun onItemCheckedStateChanged(\n        view: EasyRecyclerView,\n        position: Int,\n        id: Long,\n        checked: Boolean,\n    ) {\n        if (view.checkedItemCount == 0) {\n            view.outOfCustomChoiceMode()\n        }\n    }\n\n    private fun enterSearchMode(animation: Boolean) {\n        if (mSearchMode || mSearchBar == null || mSearchBarMover == null || mLeftDrawable == null) {\n            return\n        }\n        mSearchMode = true\n        mSearchBar!!.setState(SearchBar.STATE_SEARCH_LIST, animation)\n        mSearchBarMover!!.returnSearchBarPosition(animation)\n        mLeftDrawable!!.setArrow(ANIMATE_TIME)\n    }\n\n    private fun exitSearchMode() {\n        if (!mSearchMode || mSearchBar == null || mSearchBarMover == null || mLeftDrawable == null) {\n            return\n        }\n        mSearchMode = false\n        mSearchBar!!.setState(SearchBar.STATE_NORMAL, true)\n        mSearchBarMover!!.returnSearchBarPosition()\n        mLeftDrawable!!.setMenu(ANIMATE_TIME)\n    }\n\n    private fun onGetFavoritesSuccess(result: FavoritesParser.Result, taskId: Int) {\n        if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {\n            if (mFavCatArray != null) {\n                System.arraycopy(result.catArray, 0, mFavCatArray!!, 0, 10)\n            }\n            mFavCountArray = result.countArray\n            mFavCountSum = 0\n            for (i in 0..9) {\n                mFavCountSum += mFavCountArray!![i]\n            }\n            Settings.putFavCloudCount(mFavCountSum)\n            updateSearchBar()\n            mHelper!!.onGetPageData(taskId, 0, 0, result.prev, result.next, result.galleryInfoList)\n            mDrawerAdapter?.notifyDataSetChanged()\n        }\n    }\n\n    private fun onGetFavoritesFailure(e: Exception, taskId: Int) {\n        if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {\n            mHelper!!.onGetException(taskId, e)\n        }\n    }\n\n    private fun onGetFavoritesLocal(keyword: String?, taskId: Int) {\n        if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {\n            val list: List<GalleryInfo> = if (keyword.isNullOrEmpty()) {\n                // DB Actions\n                EhDB.allLocalFavorites\n            } else {\n                // DB Actions\n                EhDB.searchLocalFavorites(keyword)\n            }\n            if (list.isEmpty()) {\n                mHelper!!.onGetPageData(taskId, 0, 0, null, null, list)\n            } else {\n                mHelper!!.onGetPageData(taskId, 1, 0, null, null, list)\n            }\n            if (TextUtils.isEmpty(keyword)) {\n                mFavLocalCount = list.size\n                Settings.putFavLocalCount(mFavLocalCount)\n                mDrawerAdapter?.notifyDataSetChanged()\n            }\n        }\n    }\n\n    // Update fav cat on history\n    private fun updateHistoryFavSlot(gidArray: LongArray?, slot: Int) {\n        if (gidArray != null) {\n            for (gid in gidArray) {\n                // DB Actions\n                EhDB.updateHistoryFavSlot(gid, slot)\n            }\n        }\n    }\n\n    private class FavDrawerHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        val key: TextView = ViewUtils.`$$`(itemView, R.id.key) as TextView\n        val value: TextView = ViewUtils.`$$`(itemView, R.id.value) as TextView\n    }\n\n    private inner class AddFavoritesListener(\n        context: Context,\n        private val mTaskId: Int,\n        private val mKeyword: String?,\n        private val mBackup: List<GalleryInfo>,\n        private val mGidArray: LongArray?,\n        private val mSlot: Int,\n    ) : EhCallback<FavoritesScene?, Void?>(context) {\n        override fun onSuccess(result: Void?) {\n            val scene = this@FavoritesScene\n            scene.updateHistoryFavSlot(mGidArray, mSlot)\n            scene.onGetFavoritesLocal(mKeyword, mTaskId)\n        }\n\n        override fun onFailure(e: Exception) {\n            // TODO It's a failure, add all of backup back to db.\n            // But how to known which one is failed?\n            // DB Actions\n            EhDB.putLocalFavorites(mBackup)\n            val scene = this@FavoritesScene\n            scene.onGetFavoritesLocal(mKeyword, mTaskId)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private inner class GetFavoritesListener(\n        context: Context,\n        private val mTaskId: Int,\n        // Local fav is shown now, but operation need be done for cloud fav\n        private val mLocal: Boolean,\n        private val mKeyword: String?,\n        private val mGidArray: LongArray?,\n        private val mSlot: Int,\n    ) : EhCallback<FavoritesScene?, FavoritesParser.Result>(context) {\n        override fun onSuccess(result: FavoritesParser.Result) {\n            // Put fav cat\n            Settings.favCat = result.catArray\n            Settings.favCount = result.countArray\n            val scene = this@FavoritesScene\n            scene.updateHistoryFavSlot(mGidArray, mSlot)\n            if (mLocal) {\n                scene.onGetFavoritesLocal(mKeyword, mTaskId)\n            } else {\n                scene.onGetFavoritesSuccess(result, mTaskId)\n            }\n        }\n\n        override fun onFailure(e: Exception) {\n            val scene = this@FavoritesScene\n            if (mLocal) {\n                e.printStackTrace()\n                scene.onGetFavoritesLocal(mKeyword, mTaskId)\n            } else {\n                scene.onGetFavoritesFailure(e, mTaskId)\n            }\n        }\n\n        override fun onCancel() {}\n    }\n\n    private inner class FavDrawerAdapter(private val mInflater: LayoutInflater) : RecyclerView.Adapter<FavDrawerHolder>() {\n        override fun getItemViewType(position: Int): Int = position\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavDrawerHolder = FavDrawerHolder(mInflater.inflate(R.layout.item_drawer_favorites, parent, false))\n\n        @SuppressLint(\"SetTextI18n\")\n        override fun onBindViewHolder(holder: FavDrawerHolder, position: Int) {\n            when (position) {\n                0 -> {\n                    holder.key.setText(R.string.local_favorites)\n                    holder.value.text = mFavLocalCount.toString()\n                    holder.itemView.isEnabled = true\n                }\n                1 -> {\n                    holder.key.setText(R.string.cloud_favorites)\n                    holder.value.text = mFavCountSum.toString()\n                    holder.itemView.isEnabled = true\n                }\n                else -> {\n                    if (null == mFavCatArray || null == mFavCountArray || mFavCatArray!!.size < position - 1 || mFavCountArray!!.size < position - 1) {\n                        return\n                    }\n                    holder.key.text = mFavCatArray!![position - 2]\n                    holder.value.text = mFavCountArray!![position - 2].toString()\n                    holder.itemView.isEnabled = true\n                }\n            }\n            holder.itemView.setOnClickListener { onItemClick(holder.itemView, position) }\n        }\n\n        override fun getItemCount(): Int = if (null == mFavCatArray) {\n            2\n        } else {\n            12\n        }\n    }\n\n    private inner class DeleteDialogHelper :\n        DialogInterface.OnClickListener,\n        DialogInterface.OnCancelListener {\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            if (which != DialogInterface.BUTTON_POSITIVE) {\n                return\n            }\n            if (mRecyclerView == null || mHelper == null || mUrlBuilder == null) {\n                return\n            }\n            mRecyclerView!!.outOfCustomChoiceMode()\n            mFavSlot = -2\n            if (mUrlBuilder!!.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Delete local fav\n                val gidArray = LongArray(mModifyGiList.size)\n                var i = 0\n                val n = mModifyGiList.size\n                while (i < n) {\n                    gidArray[i] = mModifyGiList[i].gid\n                    i++\n                }\n                // DB Actions\n                EhDB.removeLocalFavorites(gidArray)\n                updateHistoryFavSlot(gidArray, mFavSlot)\n                mModifyGiList.clear()\n                mHelper!!.refresh()\n            } else { // Delete cloud fav\n                mEnableModify = true\n                mModifyFavCat = -1\n                mModifyAdd = false\n                mHelper!!.refresh()\n            }\n        }\n\n        override fun onCancel(dialog: DialogInterface) {\n            mModifyGiList.clear()\n        }\n    }\n\n    private inner class MoveDialogHelper :\n        DialogInterface.OnClickListener,\n        DialogInterface.OnCancelListener {\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            if (mRecyclerView == null || mHelper == null || mUrlBuilder == null) {\n                return\n            }\n            val srcCat = mUrlBuilder!!.favCat\n            val dstCat: Int = if (which == 0) {\n                FavListUrlBuilder.FAV_CAT_LOCAL\n            } else {\n                which - 1\n            }\n            if (srcCat == dstCat) {\n                return\n            }\n            mRecyclerView!!.outOfCustomChoiceMode()\n            if (srcCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Move from local to cloud\n                val gidArray = LongArray(mModifyGiList.size)\n                var i = 0\n                val n = mModifyGiList.size\n                while (i < n) {\n                    gidArray[i] = mModifyGiList[i].gid\n                    i++\n                }\n                // DB Actions\n                EhDB.removeLocalFavorites(gidArray)\n                mEnableModify = true\n                mModifyFavCat = dstCat\n                mModifyAdd = true\n                mFavSlot = dstCat\n                mHelper!!.refresh()\n            } else if (dstCat == FavListUrlBuilder.FAV_CAT_LOCAL) { // Move from cloud to local\n                // DB Actions\n                EhDB.putLocalFavorites(mModifyGiList)\n                mEnableModify = true\n                mModifyFavCat = -1\n                mModifyAdd = false\n                mFavSlot = -1\n                mHelper!!.refresh()\n            } else {\n                mEnableModify = true\n                mModifyFavCat = dstCat\n                mModifyAdd = false\n                mFavSlot = dstCat\n                mHelper!!.refresh()\n            }\n        }\n\n        override fun onCancel(dialog: DialogInterface) {\n            mModifyGiList.clear()\n        }\n    }\n\n    private inner class FavoritesAdapter(\n        inflater: LayoutInflater,\n        resources: Resources,\n        recyclerView: RecyclerView,\n        type: Int,\n    ) : GalleryAdapter(inflater, resources, recyclerView, type, false) {\n        override fun getItemCount(): Int = if (null != mHelper) mHelper!!.size() else 0\n\n        override fun onItemClick(view: View, position: Int) {\n            this@FavoritesScene.onItemClick(view, position)\n        }\n\n        override fun onItemLongClick(view: View, position: Int): Boolean = this@FavoritesScene.onItemLongClick(position)\n\n        override fun getDataAt(position: Int): GalleryInfo? = if (null != mHelper) mHelper!!.getDataAtEx(position) else null\n    }\n\n    private inner class FavoritesHelper : GalleryInfoContentHelper() {\n        override fun getPageData(\n            taskId: Int,\n            type: Int,\n            page: Int,\n            index: String?,\n            isNext: Boolean,\n        ) {\n            val activity = mainActivity\n            if (null == activity || null == mUrlBuilder || null == mClient) {\n                return\n            }\n            if (mEnableModify) {\n                mEnableModify = false\n                val local = mUrlBuilder!!.favCat == FavListUrlBuilder.FAV_CAT_LOCAL\n                val gidArray = LongArray(mModifyGiList.size)\n                if (mModifyAdd) {\n                    val tokenArray = arrayOfNulls<String>(mModifyGiList.size)\n                    var i = 0\n                    val n = mModifyGiList.size\n                    while (i < n) {\n                        val gi = mModifyGiList[i]\n                        gidArray[i] = gi.gid\n                        tokenArray[i] = gi.token\n                        i++\n                    }\n                    val modifyGiListBackup: List<GalleryInfo> = ArrayList(mModifyGiList)\n                    mModifyGiList.clear()\n                    val request = EhRequest()\n                    request.setMethod(EhClient.METHOD_ADD_FAVORITES_RANGE)\n                    request.setCallback(\n                        AddFavoritesListener(\n                            context,\n                            taskId,\n                            mUrlBuilder!!.keyword,\n                            modifyGiListBackup,\n                            gidArray,\n                            mFavSlot,\n                        ),\n                    )\n                    request.setArgs(gidArray, tokenArray, mModifyFavCat)\n                    request.enqueue(this@FavoritesScene)\n                } else {\n                    var i = 0\n                    val n = mModifyGiList.size\n                    while (i < n) {\n                        gidArray[i] = mModifyGiList[i].gid\n                        i++\n                    }\n                    mModifyGiList.clear()\n                    val url: String = if (local) {\n                        // Local fav is shown now, but operation need be done for cloud fav\n                        EhUrl.favoritesUrl\n                    } else {\n                        mUrlBuilder!!.build()\n                    }\n                    mUrlBuilder!!.setIndex(index, true)\n                    val request = EhRequest()\n                    request.setMethod(EhClient.METHOD_MODIFY_FAVORITES)\n                    request.setCallback(\n                        GetFavoritesListener(\n                            context,\n                            taskId,\n                            local,\n                            mUrlBuilder!!.keyword,\n                            gidArray,\n                            mFavSlot,\n                        ),\n                    )\n                    request.setArgs(url, gidArray, mModifyFavCat)\n                    request.enqueue(this@FavoritesScene)\n                }\n            } else if (mUrlBuilder!!.favCat == FavListUrlBuilder.FAV_CAT_LOCAL) {\n                val keyword = mUrlBuilder!!.keyword\n                SimpleHandler.getInstance().post { onGetFavoritesLocal(keyword, taskId) }\n            } else {\n                mUrlBuilder!!.setIndex(index, isNext)\n                mUrlBuilder!!.jumpTo = jumpTo\n                val url = mUrlBuilder!!.build()\n                val request = EhRequest()\n                request.setMethod(EhClient.METHOD_GET_FAVORITES)\n                request.setCallback(\n                    GetFavoritesListener(\n                        context,\n                        taskId,\n                        false,\n                        mUrlBuilder!!.keyword,\n                        null,\n                        -2,\n                    ),\n                )\n                request.setArgs(url)\n                request.enqueue(this@FavoritesScene)\n            }\n        }\n\n        override val context\n            get() = this@FavoritesScene.requireContext()\n\n        override fun notifyDataSetChanged() {\n            // Ensure outOfCustomChoiceMode to avoid error\n            mRecyclerView?.outOfCustomChoiceMode()\n            mAdapter?.notifyDataSetChanged()\n        }\n\n        override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) {\n            mAdapter?.notifyItemRangeInserted(positionStart, itemCount)\n        }\n\n        override fun onShowView(hiddenView: View, shownView: View) {\n            mSearchBarMover?.showSearchBar()\n        }\n\n        override fun isDuplicate(d1: GalleryInfo?, d2: GalleryInfo?): Boolean = d1?.gid == d2?.gid && d1 != null && d2 != null\n\n        override fun onScrollToPosition(position: Int) {\n            if (0 == position) {\n                mSearchBarMover?.showSearchBar()\n            }\n        }\n    }\n\n    companion object {\n        private const val ANIMATE_TIME = 300L\n        private const val KEY_URL_BUILDER = \"url_builder\"\n        private const val KEY_SEARCH_MODE = \"search_mode\"\n        private const val KEY_HAS_FIRST_REFRESH = \"has_first_refresh\"\n        private const val KEY_FAV_COUNT_ARRAY = \"fav_count_array\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryAdapter.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.annotation.SuppressLint\nimport android.content.res.Resources\nimport android.text.TextUtils\nimport android.util.TypedValue.COMPLEX_UNIT_PX\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.annotation.IntDef\nimport androidx.core.view.ViewCompat\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager\nimport com.hippo.drawable.TriangleDrawable\nimport com.hippo.easyrecyclerview.MarginItemDecoration\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.getThumbKey\nimport com.hippo.ehviewer.client.thumbUrl\nimport com.hippo.ehviewer.download.DownloadManager as downloadManager\nimport com.hippo.ehviewer.widget.TileThumb\nimport com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager\nimport com.hippo.yorozuya.ViewUtils\n\n@SuppressLint(\"InflateParams\")\ninternal abstract class GalleryAdapter(\n    private val mInflater: LayoutInflater,\n    private val mResources: Resources,\n    private val mRecyclerView: RecyclerView,\n    type: Int,\n    showFavourited: Boolean,\n) : RecyclerView.Adapter<GalleryHolder>() {\n    private val mLayoutManager: AutoStaggeredGridLayoutManager =\n        AutoStaggeredGridLayoutManager(0, StaggeredGridLayoutManager.VERTICAL)\n    private val mPaddingTopSB: Int =\n        mResources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)\n    private val mListThumbWidth: Int\n    private val mListThumbHeight: Int\n    private val mShowFavourited: Boolean = showFavourited\n    private var mListDecoration: MarginItemDecoration? = null\n    private var mGirdDecoration: MarginItemDecoration? = null\n    private var mType = TYPE_INVALID\n\n    var type: Int\n        get() = mType\n\n        @SuppressLint(\"NotifyDataSetChanged\")\n        set(type) {\n            if (type == mType) {\n                return\n            }\n            mType = type\n            val recyclerView = mRecyclerView\n            when (type) {\n                TYPE_LIST -> {\n                    mLayoutManager.setColumnSize(Settings.detailSize)\n                    mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE)\n                    if (null != mGirdDecoration) {\n                        recyclerView.removeItemDecoration(mGirdDecoration!!)\n                    }\n                    if (null == mListDecoration) {\n                        val interval =\n                            mResources.getDimensionPixelOffset(R.dimen.gallery_list_interval)\n                        val paddingH =\n                            mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_h)\n                        val paddingV =\n                            mResources.getDimensionPixelOffset(R.dimen.gallery_list_margin_v)\n                        mListDecoration =\n                            MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)\n                    }\n                    recyclerView.addItemDecoration(mListDecoration!!)\n                    notifyDataSetChanged()\n                }\n                TYPE_GRID -> {\n                    mLayoutManager.setColumnSize(Settings.thumbSize)\n                    mLayoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_SUITABLE_SIZE)\n                    if (null != mListDecoration) {\n                        recyclerView.removeItemDecoration(mListDecoration!!)\n                    }\n                    if (null == mGirdDecoration) {\n                        val interval =\n                            mResources.getDimensionPixelOffset(R.dimen.gallery_grid_interval)\n                        val paddingH =\n                            mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_h)\n                        val paddingV =\n                            mResources.getDimensionPixelOffset(R.dimen.gallery_grid_margin_v)\n                        mGirdDecoration =\n                            MarginItemDecoration(interval, paddingH, paddingV, paddingH, paddingV)\n                    }\n                    recyclerView.addItemDecoration(mGirdDecoration!!)\n                    notifyDataSetChanged()\n                }\n            }\n        }\n\n    init {\n        mRecyclerView.adapter = this\n        mRecyclerView.layoutManager = mLayoutManager\n        val calculator =\n            mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)\n        ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)\n        mListThumbHeight = calculator.measuredHeight\n        mListThumbWidth = mListThumbHeight * 2 / 3\n        this.type = type\n        adjustPaddings()\n    }\n\n    private fun adjustPaddings() {\n        val recyclerView = mRecyclerView\n        recyclerView.setPadding(\n            recyclerView.paddingLeft,\n            recyclerView.paddingTop + mPaddingTopSB,\n            recyclerView.paddingRight,\n            recyclerView.paddingBottom,\n        )\n    }\n\n    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryHolder {\n        val layoutId = when (viewType) {\n            TYPE_LIST -> R.layout.item_gallery_list\n            TYPE_GRID -> R.layout.item_gallery_grid\n            else -> throw IllegalStateException(\"Unexpected value: $viewType\")\n        }\n        val holder = GalleryHolder(mInflater.inflate(layoutId, parent, false))\n\n        when (viewType) {\n            TYPE_LIST -> {\n                val lp = holder.thumb.layoutParams\n                lp.width = mListThumbWidth\n                lp.height = mListThumbHeight\n                holder.thumb.layoutParams = lp\n                holder.title.maxLines = if (Settings.listTitleSingleLine) 1 else 2\n            }\n            TYPE_GRID -> {\n                val columnWidth = Settings.thumbSize\n                val textSize = columnWidth / 14\n                val lp = holder.category.layoutParams\n                lp.width = columnWidth / 5\n                lp.height = (lp.width * 0.75).toInt()\n                holder.category.layoutParams = lp\n                holder.simpleLanguage.setTextSize(COMPLEX_UNIT_PX, textSize.toFloat())\n                holder.pages.setTextSize(COMPLEX_UNIT_PX, textSize.toFloat())\n                holder.title.setTextSize(COMPLEX_UNIT_PX, textSize.toFloat())\n                holder.title.maxLines = 2\n            }\n        }\n        holder.card.setOnClickListener {\n            onItemClick(\n                holder.itemView,\n                holder.bindingAdapterPosition,\n            )\n        }\n        holder.card.setOnLongClickListener {\n            onItemLongClick(\n                holder.itemView,\n                holder.bindingAdapterPosition,\n            )\n        }\n        return holder\n    }\n\n    abstract fun onItemClick(view: View, position: Int)\n    abstract fun onItemLongClick(view: View, position: Int): Boolean\n    override fun getItemViewType(position: Int): Int = mType\n\n    abstract fun getDataAt(position: Int): GalleryInfo?\n\n    @SuppressLint(\"SetTextI18n\")\n    override fun onBindViewHolder(holder: GalleryHolder, position: Int) {\n        val gi = getDataAt(position) ?: return\n        when (mType) {\n            TYPE_LIST -> {\n                holder.thumb.load(getThumbKey(gi.gid), gi.thumbUrl!!, hardware = false)\n                holder.title.text = EhUtils.getSuitableTitle(gi)\n                holder.uploader!!.alpha = if (gi.disowned) .5f else 1f\n                if (TextUtils.isEmpty(gi.uploader)) {\n                    holder.uploader.text = null\n                    holder.uploader.visibility = View.GONE\n                } else {\n                    holder.uploader.text = gi.uploader\n                    holder.uploader.visibility = View.VISIBLE\n                }\n                holder.note!!.text = gi.favoriteNote\n                holder.rating.rating = gi.rating\n                val category = holder.category\n                val newCategoryText = EhUtils.getCategory(gi.category)\n                if (newCategoryText != category.text.toString()) {\n                    category.text = newCategoryText\n                    category.setBackgroundColor(EhUtils.getCategoryColor(gi.category))\n                }\n                holder.posted!!.text = gi.posted\n                if (gi.pages == 0 || !Settings.showGalleryPages) {\n                    holder.pages.text = null\n                    holder.pages.visibility = View.GONE\n                } else {\n                    holder.pages.text = gi.pages.toString() + \"P\"\n                    holder.pages.visibility = View.VISIBLE\n                }\n                if (TextUtils.isEmpty(gi.simpleLanguage)) {\n                    holder.simpleLanguage.text = null\n                    holder.simpleLanguage.visibility = View.GONE\n                } else {\n                    holder.simpleLanguage.text = gi.simpleLanguage\n                    holder.simpleLanguage.visibility = View.VISIBLE\n                }\n                holder.favourited!!.visibility =\n                    if (mShowFavourited && gi.favoriteSlot >= -1 && gi.favoriteSlot <= 10) View.VISIBLE else View.GONE\n                holder.downloaded!!.visibility =\n                    if (downloadManager.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE\n            }\n            TYPE_GRID -> {\n                (holder.thumb as TileThumb).setThumbSize(gi.thumbWidth, gi.thumbHeight)\n                holder.thumb.load(getThumbKey(gi.gid), gi.thumbUrl!!, hardware = false)\n                if (Settings.thumbShowTitle) {\n                    holder.title.text = EhUtils.getSuitableTitle(gi)\n                    holder.title.visibility = View.VISIBLE\n                    holder.rating.rating = gi.rating\n                    holder.rating.visibility = View.VISIBLE\n                    if (gi.pages == 0 || !Settings.showGalleryPages) {\n                        holder.pages.text = null\n                        holder.pages.visibility = View.GONE\n                    } else {\n                        holder.pages.text = gi.pages.toString() + \"P\"\n                        holder.pages.visibility = View.VISIBLE\n                    }\n                } else {\n                    holder.title.text = null\n                    holder.title.visibility = View.GONE\n                    holder.rating.visibility = View.GONE\n                    holder.pages.text = null\n                    holder.pages.visibility = View.GONE\n                }\n                val category: View = holder.category\n                var drawable = category.background\n                val color = EhUtils.getCategoryColor(gi.category)\n                if (drawable !is TriangleDrawable) {\n                    drawable = TriangleDrawable(color)\n                    category.background = drawable\n                } else {\n                    drawable.setColor(color)\n                }\n                holder.simpleLanguage.text = gi.simpleLanguage\n            }\n        }\n        // Update transition name\n        ViewCompat.setTransitionName(\n            holder.thumb,\n            TransitionNameFactory.getThumbTransitionName(gi.gid),\n        )\n    }\n\n    @IntDef(TYPE_LIST, TYPE_GRID)\n    @Retention(AnnotationRetention.SOURCE)\n    annotation class Type\n    companion object {\n        const val TYPE_INVALID = -1\n        const val TYPE_LIST = 0\n        const val TYPE_GRID = 1\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryCommentsScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.animation.Animator\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.graphics.Typeface\nimport android.graphics.drawable.Drawable\nimport android.os.Bundle\nimport android.os.Looper\nimport android.text.Spannable\nimport android.text.SpannableStringBuilder\nimport android.text.TextUtils\nimport android.text.style.CharacterStyle\nimport android.text.style.ForegroundColorSpan\nimport android.text.style.RelativeSizeSpan\nimport android.text.style.StrikethroughSpan\nimport android.text.style.StyleSpan\nimport android.text.style.URLSpan\nimport android.text.style.UnderlineSpan\nimport android.util.Log\nimport android.view.ActionMode\nimport android.view.LayoutInflater\nimport android.view.Menu\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewAnimationUtils\nimport android.view.ViewGroup\nimport android.widget.EditText\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.content.ContextCompat\nimport androidx.core.text.getSpans\nimport androidx.core.text.inSpans\nimport androidx.core.text.set\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowInsetsAnimationCompat\nimport androidx.core.view.isVisible\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener\nimport com.google.android.material.floatingactionbutton.FloatingActionButton\nimport com.hippo.app.EditTextDialogBuilder\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.LinearDividerItemDecoration\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.UrlOpener\nimport com.hippo.ehviewer.WindowInsetsAnimationHelper\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhFilter\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.data.GalleryComment\nimport com.hippo.ehviewer.client.data.GalleryCommentList\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.client.data.ListUrlBuilder\nimport com.hippo.ehviewer.client.parser.VoteCommentParser\nimport com.hippo.ehviewer.dao.Filter\nimport com.hippo.ehviewer.ui.MainActivity\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.ReadableTime\nimport com.hippo.util.TextUrl\nimport com.hippo.util.addTextToClipboard\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.util.loadHtml\nimport com.hippo.util.toBBCode\nimport com.hippo.view.ViewTransition\nimport com.hippo.widget.FabLayout\nimport com.hippo.widget.LinkifyTextView\nimport com.hippo.widget.ObservedTextView\nimport com.hippo.yorozuya.AnimationUtils\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.ResourcesUtils\nimport com.hippo.yorozuya.SimpleAnimatorListener\nimport com.hippo.yorozuya.StringUtils\nimport com.hippo.yorozuya.ViewUtils\nimport com.hippo.yorozuya.collect.IntList\nimport kotlin.math.hypot\nimport rikka.core.res.resolveColor\n\nclass GalleryCommentsScene :\n    ToolbarScene(),\n    View.OnClickListener,\n    OnRefreshListener {\n    private var mGalleryDetail: GalleryDetail? = null\n    private var mRecyclerView: EasyRecyclerView? = null\n    private var mFabLayout: FabLayout? = null\n    private var mFab: FloatingActionButton? = null\n    private var mEditPanel: View? = null\n    private var mSendImage: ImageView? = null\n    private var mEditText: EditText? = null\n    private var mAdapter: CommentAdapter? = null\n    private var mViewTransition: ViewTransition? = null\n    private var mRefreshLayout: SwipeRefreshLayout? = null\n    private var mSendDrawable: Drawable? = null\n    private var mPencilDrawable: Drawable? = null\n    private var mCommentId: Long = 0\n    private var mInAnimation = false\n    private var mShowAllComments = false\n    private var mRefreshingComments = false\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    private fun handleArgs(args: Bundle?) {\n        if (args == null) {\n            return\n        }\n        mGalleryDetail = args.getParcelableCompat(KEY_GALLERY_DETAIL)\n        mShowAllComments = mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore\n    }\n\n    private fun onInit() {\n        handleArgs(arguments)\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mGalleryDetail = savedInstanceState.getParcelableCompat(KEY_GALLERY_DETAIL)\n        mShowAllComments = mGalleryDetail != null && mGalleryDetail!!.comments != null && !mGalleryDetail!!.comments!!.hasMore\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail)\n    }\n\n    override fun onCreateViewWithToolbar(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_gallery_comments, container, false) as View\n        mRecyclerView = ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView\n        val tip = ViewUtils.`$$`(view, R.id.tip) as TextView\n        mEditPanel = ViewUtils.`$$`(view, R.id.edit_panel)\n        mSendImage = ViewUtils.`$$`(mEditPanel, R.id.send) as ImageView\n        mEditText = ViewUtils.`$$`(mEditPanel, R.id.edit_text) as EditText\n        mFabLayout = ViewUtils.`$$`(view, R.id.fab_layout) as FabLayout\n        mFab = ViewUtils.`$$`(view, R.id.fab) as FloatingActionButton\n        mRefreshLayout = ViewUtils.`$$`(view, R.id.refresh_layout) as SwipeRefreshLayout\n\n        ViewCompat.setWindowInsetsAnimationCallback(\n            view,\n            WindowInsetsAnimationHelper(\n                WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,\n                mEditPanel,\n                mFabLayout,\n            ),\n        )\n        mRefreshLayout!!.setColorSchemeResources(\n            R.color.loading_indicator_red,\n            R.color.loading_indicator_purple,\n            R.color.loading_indicator_blue,\n            R.color.loading_indicator_cyan,\n            R.color.loading_indicator_green,\n            R.color.loading_indicator_yellow,\n        )\n        mRefreshLayout!!.setOnRefreshListener(this)\n        val context = requireContext()\n        val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)\n        drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n        tip.setCompoundDrawables(null, drawable, null, null)\n        mSendDrawable = ContextCompat.getDrawable(context, R.drawable.v_send_dark_x24)\n        mPencilDrawable = ContextCompat.getDrawable(context, R.drawable.v_pencil_dark_x24)\n        mAdapter = CommentAdapter()\n        mRecyclerView!!.adapter = mAdapter\n        mRecyclerView!!.layoutManager = LinearLayoutManager(\n            context,\n            RecyclerView.VERTICAL,\n            false,\n        )\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(context, 1.0f),\n        )\n        decoration.setShowLastDivider(true)\n        mRecyclerView!!.addItemDecoration(decoration)\n        mRecyclerView!!.setHasFixedSize(true)\n        // Cancel change animator\n        val itemAnimator = mRecyclerView!!.itemAnimator\n        if (itemAnimator is DefaultItemAnimator) {\n            itemAnimator.supportsChangeAnimations = false\n        }\n        mSendImage!!.setOnClickListener(this)\n        mEditText!!.customSelectionActionModeCallback = object : ActionMode.Callback {\n            override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {\n                requireActivity().menuInflater.inflate(R.menu.context_comment, menu)\n                return true\n            }\n\n            override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean = true\n\n            override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {\n                item?.let {\n                    val text = mEditText!!.editableText\n                    val start = mEditText!!.selectionStart\n                    val end = mEditText!!.selectionEnd\n                    when (item.itemId) {\n                        R.id.action_bold -> text[start, end] = StyleSpan(Typeface.BOLD)\n                        R.id.action_italic -> text[start, end] = StyleSpan(Typeface.ITALIC)\n                        R.id.action_underline -> text[start, end] = UnderlineSpan()\n                        R.id.action_strikethrough -> text[start, end] = StrikethroughSpan()\n                        R.id.action_url -> {\n                            val oldSpans = text.getSpans<URLSpan>(start, end)\n                            var oldUrl = \"https://\"\n                            oldSpans.forEach {\n                                if (!TextUtils.isEmpty(it.url)) {\n                                    oldUrl = it.url\n                                }\n                            }\n                            val builder = EditTextDialogBuilder(\n                                context,\n                                oldUrl,\n                                getString(R.string.format_url),\n                            )\n                            builder.setTitle(getString(R.string.format_url))\n                            builder.setPositiveButton(android.R.string.ok, null)\n                            val dialog = builder.show()\n                            val button: View? = dialog.getButton(DialogInterface.BUTTON_POSITIVE)\n                            button?.setOnClickListener(\n                                View.OnClickListener {\n                                    val url = builder.text.trim()\n                                    if (TextUtils.isEmpty(url)) {\n                                        builder.setError(getString(R.string.text_is_empty))\n                                        return@OnClickListener\n                                    } else {\n                                        builder.setError(null)\n                                    }\n                                    text.clearSpan(start, end, true)\n                                    text[start, end] = URLSpan(url)\n                                    dialog.dismiss()\n                                },\n                            )\n                        }\n                        R.id.action_clear -> {\n                            text.clearSpan(start, end, false)\n                        }\n                        else -> return false\n                    }\n                    mode?.finish()\n                }\n                return true\n            }\n\n            override fun onDestroyActionMode(mode: ActionMode?) {\n            }\n        }\n        mFab!!.setOnClickListener(this)\n        addAboveSnackView(mEditPanel!!)\n        addAboveSnackView(mFabLayout!!)\n        mViewTransition = ViewTransition(mRecyclerView, tip)\n        updateView(false)\n        return view\n    }\n\n    fun Spannable.clearSpan(start: Int, end: Int, url: Boolean) {\n        val spans = if (url) getSpans<URLSpan>(start, end) else getSpans<CharacterStyle>(start, end)\n        spans.forEach {\n            val spanStart = getSpanStart(it)\n            val spanEnd = getSpanEnd(it)\n            removeSpan(it)\n            if (spanStart < start) {\n                this[spanStart, start] = it\n            }\n            if (spanEnd > end) {\n                this[end, spanEnd] = it\n            }\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        if (null != mRecyclerView) {\n            mRecyclerView!!.stopScroll()\n            mRecyclerView = null\n        }\n        if (null != mEditPanel) {\n            removeAboveSnackView(mEditPanel!!)\n            mEditPanel = null\n        }\n        if (null != mFabLayout) {\n            removeAboveSnackView(mFabLayout!!)\n            mFabLayout = null\n        }\n        mFab = null\n        mSendImage = null\n        mEditText = null\n        mAdapter = null\n        mViewTransition = null\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        setTitle(R.string.gallery_comments)\n        setNavigationIcon(R.drawable.v_arrow_left_dark_x24)\n    }\n\n    override fun onNavigationClick() {\n        onBackPressed()\n    }\n\n    private fun showFilterCommenterDialog(commenter: String?, position: Int) {\n        val context = context\n        if (context == null || commenter == null) {\n            return\n        }\n        AlertDialog.Builder(context)\n            .setMessage(getString(R.string.filter_the_commenter, commenter))\n            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->\n                val filter = Filter()\n                filter.mode = EhFilter.MODE_COMMENTER\n                filter.text = commenter\n                EhFilter.addFilter(filter)\n                hideComment(position)\n                showTip(R.string.filter_added, LENGTH_SHORT)\n            }\n            .setNegativeButton(android.R.string.cancel, null)\n            .show()\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    private fun hideComment(position: Int) {\n        if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {\n            return\n        }\n        val oldCommentsList = mGalleryDetail!!.comments!!.comments\n        val newCommentsList = arrayOfNulls<GalleryComment>(\n            oldCommentsList!!.size - 1,\n        )\n        var i = 0\n        var j = 0\n        while (i < oldCommentsList.size) {\n            if (i != position) {\n                newCommentsList[j] = oldCommentsList[i]\n                j++\n            }\n            i++\n        }\n        mGalleryDetail!!.comments!!.comments = newCommentsList.requireNoNulls()\n        mAdapter!!.notifyDataSetChanged()\n        updateView(true)\n    }\n\n    private fun voteComment(id: Long, vote: Int) {\n        val context = context\n        val activity = mainActivity\n        if (null == context || null == activity) {\n            return\n        }\n        val request = EhRequest()\n            .setMethod(EhClient.METHOD_VOTE_COMMENT)\n            .setArgs(\n                mGalleryDetail!!.apiUid,\n                mGalleryDetail!!.apiKey,\n                mGalleryDetail!!.gid,\n                mGalleryDetail!!.token,\n                id,\n                vote,\n            )\n            .setCallback(VoteCommentListener(context))\n        request.enqueue(this)\n    }\n\n    @SuppressLint(\"InflateParams\")\n    fun showVoteStatusDialog(context: Context?, voteStatus: String?) {\n        var mContext = context\n        val temp = StringUtils.split(voteStatus, ',')\n        val length = temp.size\n        val userArray = arrayOfNulls<String>(length)\n        val voteArray = arrayOfNulls<String>(length)\n        for (i in 0 until length) {\n            val str = StringUtils.trim(temp[i])\n            val index = str.lastIndexOf(' ')\n            if (index < 0) {\n                Log.d(TAG, \"Something wrong happened about vote state\")\n                userArray[i] = str\n                voteArray[i] = \"\"\n            } else {\n                userArray[i] = StringUtils.trim(str.substring(0, index))\n                voteArray[i] = StringUtils.trim(str.substring(index + 1))\n            }\n        }\n        val builder = AlertDialog.Builder(mContext!!)\n        mContext = builder.context\n        val inflater = LayoutInflater.from(mContext)\n        val rv = inflater.inflate(R.layout.dialog_recycler_view, null) as EasyRecyclerView\n        rv.adapter = object : RecyclerView.Adapter<InfoHolder>() {\n            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfoHolder = InfoHolder(inflater.inflate(R.layout.item_drawer_favorites, parent, false))\n\n            override fun onBindViewHolder(holder: InfoHolder, position: Int) {\n                holder.key.text = userArray[position]\n                holder.value.text = voteArray[position]\n            }\n\n            override fun getItemCount(): Int = length\n        }\n        rv.layoutManager = LinearLayoutManager(mContext)\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(mContext, 1.0f),\n        )\n        decoration.setPadding(ResourcesUtils.getAttrDimensionPixelOffset(mContext, androidx.appcompat.R.attr.dialogPreferredPadding))\n        rv.addItemDecoration(decoration)\n        rv.clipToPadding = false\n        builder.setView(rv).show()\n    }\n\n    private fun showCommentDialog(position: Int, text: CharSequence) {\n        val context = context\n        if (context == null || mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null || position >= mGalleryDetail!!.comments!!.comments!!.size || position < 0) {\n            return\n        }\n        val comment = mGalleryDetail!!.comments!!.comments!![position]\n        val menu: MutableList<String> = ArrayList()\n        val menuId = IntList()\n        val resources = context.resources\n        menu.add(resources.getString(R.string.copy_comment_text))\n        menuId.add(R.id.copy)\n        if (!comment.uploader && !comment.editable) {\n            menu.add(resources.getString(R.string.block_commenter))\n            menuId.add(R.id.block_commenter)\n        }\n        if (comment.editable) {\n            menu.add(resources.getString(R.string.edit_comment))\n            menuId.add(R.id.edit_comment)\n        }\n        if (comment.voteUpAble) {\n            menu.add(resources.getString(if (comment.voteUpEd) R.string.cancel_vote_up else R.string.vote_up))\n            menuId.add(R.id.vote_up)\n        }\n        if (comment.voteDownAble) {\n            menu.add(resources.getString(if (comment.voteDownEd) R.string.cancel_vote_down else R.string.vote_down))\n            menuId.add(R.id.vote_down)\n        }\n        if (!TextUtils.isEmpty(comment.voteState)) {\n            menu.add(resources.getString(R.string.check_vote_status))\n            menuId.add(R.id.check_vote_status)\n        }\n        AlertDialog.Builder(context)\n            .setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->\n                if (which < 0 || which >= menuId.size) {\n                    return@setItems\n                }\n                val id = menuId[which]\n                if (id == R.id.copy) {\n                    requireActivity().addTextToClipboard(text, false)\n                } else if (id == R.id.block_commenter) {\n                    showFilterCommenterDialog(comment.user, position)\n                } else if (id == R.id.vote_up) {\n                    voteComment(comment.id, 1)\n                } else if (id == R.id.vote_down) {\n                    voteComment(comment.id, -1)\n                } else if (id == R.id.check_vote_status) {\n                    showVoteStatusDialog(context, comment.voteState)\n                } else if (id == R.id.edit_comment) {\n                    prepareEditComment(comment.id, text)\n                    if (!mInAnimation && mEditPanel != null && mEditPanel!!.visibility != View.VISIBLE) {\n                        showEditPanel()\n                    }\n                }\n            }.show()\n    }\n\n    fun onItemClick(parent: EasyRecyclerView?, view2: View?, position: Int): Boolean {\n        val activity = mainActivity ?: return false\n        val holder = parent!!.getChildViewHolder(view2!!)\n        if (holder is ActualCommentHolder) {\n            val span = holder.comment.currentSpan\n            holder.comment.clearCurrentSpan()\n            if (span is URLSpan) {\n                UrlOpener.openUrl(activity, span.url, true, mGalleryDetail)\n            } else {\n                showCommentDialog(position, holder.sp)\n            }\n        } else if (holder is MoreCommentHolder && !mRefreshingComments && mAdapter != null) {\n            mRefreshingComments = true\n            mShowAllComments = true\n            mAdapter!!.notifyItemChanged(position)\n            val url = galleryDetailUrl\n            if (url != null) {\n                // Request\n                val request = EhRequest()\n                    .setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)\n                    .setArgs(url)\n                    .setCallback(RefreshCommentListener(activity))\n                request.enqueue(this)\n            }\n        }\n        return true\n    }\n\n    private fun updateView(animation: Boolean) {\n        if (null == mViewTransition) {\n            return\n        }\n        if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null || mGalleryDetail!!.comments!!.comments!!.isEmpty()) {\n            mViewTransition!!.showView(1, animation)\n        } else {\n            mViewTransition!!.showView(0, animation)\n        }\n    }\n\n    private fun prepareNewComment() {\n        mCommentId = 0\n        if (mSendImage != null) {\n            mSendImage!!.setImageDrawable(mSendDrawable)\n        }\n    }\n\n    private fun prepareEditComment(commentId: Long, text: CharSequence) {\n        mCommentId = commentId\n        mEditText?.setText(text)\n        if (mSendImage != null) {\n            mSendImage!!.setImageDrawable(mPencilDrawable)\n        }\n    }\n\n    private fun showEditPanelWithAnimation() {\n        if (null == mFab || null == mEditPanel) {\n            return\n        }\n        mInAnimation = true\n        mFab!!.translationX = 0.0f\n        mFab!!.translationY = 0.0f\n        mFab!!.scaleX = 1.0f\n        mFab!!.scaleY = 1.0f\n        val fabEndX = mEditPanel!!.left + mEditPanel!!.width / 2 - mFab!!.width / 2\n        val fabEndY = mEditPanel!!.top + mEditPanel!!.height / 2 - mFab!!.height / 2\n        mFab!!.animate().x(fabEndX.toFloat()).y(fabEndY.toFloat()).scaleX(0.0f).scaleY(0.0f)\n            .setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)\n            .setDuration(300L).setListener(object : SimpleAnimatorListener() {\n                override fun onAnimationEnd(animation: Animator) {\n                    if (null == mFab || null == mEditPanel) {\n                        return\n                    }\n                    (mFab as View).visibility = View.INVISIBLE\n                    mEditPanel!!.visibility = View.VISIBLE\n                    val halfW = mEditPanel!!.width / 2\n                    val halfH = mEditPanel!!.height / 2\n                    val animator = ViewAnimationUtils.createCircularReveal(\n                        mEditPanel,\n                        halfW,\n                        halfH,\n                        0f,\n                        hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),\n                    ).setDuration(300L)\n                    animator.addListener(object : SimpleAnimatorListener() {\n                        override fun onAnimationEnd(a: Animator) {\n                            mInAnimation = false\n                        }\n                    })\n                    animator.start()\n                }\n            }).start()\n    }\n\n    private fun showEditPanel() {\n        showEditPanelWithAnimation()\n    }\n\n    private fun hideEditPanelWithAnimation() {\n        if (null == mFab || null == mEditPanel) {\n            return\n        }\n        mInAnimation = true\n        val halfW = mEditPanel!!.width / 2\n        val halfH = mEditPanel!!.height / 2\n        val animator = ViewAnimationUtils.createCircularReveal(\n            mEditPanel,\n            halfW,\n            halfH,\n            hypot(halfW.toDouble(), halfH.toDouble()).toFloat(),\n            0.0f,\n        ).setDuration(300L)\n        animator.addListener(object : SimpleAnimatorListener() {\n            override fun onAnimationEnd(a: Animator) {\n                if (null == mFab || null == mEditPanel) {\n                    return\n                }\n                if (Looper.myLooper() != Looper.getMainLooper()) {\n                    // Some devices may run this block in non-UI thread.\n                    // It might be a bug of Android OS.\n                    // Check it here to avoid crash.\n                    return\n                }\n                mEditPanel!!.visibility = View.GONE\n                (mFab as View).visibility = View.VISIBLE\n                val fabStartX = mEditPanel!!.left + mEditPanel!!.width / 2 - mFab!!.width / 2\n                val fabStartY = mEditPanel!!.top + mEditPanel!!.height / 2 - mFab!!.height / 2\n                mFab!!.x = fabStartX.toFloat()\n                mFab!!.y = fabStartY.toFloat()\n                mFab!!.scaleX = 0.0f\n                mFab!!.scaleY = 0.0f\n                mFab!!.rotation = -45.0f\n                mFab!!.animate().translationX(0.0f).translationY(0.0f).scaleX(1.0f).scaleY(1.0f)\n                    .rotation(0.0f)\n                    .setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR)\n                    .setDuration(300L).setListener(object : SimpleAnimatorListener() {\n                        override fun onAnimationEnd(animation: Animator) {\n                            mInAnimation = false\n                        }\n                    }).start()\n            }\n        })\n        animator.start()\n    }\n\n    private fun hideEditPanel() {\n        hideSoftInput()\n        hideEditPanelWithAnimation()\n    }\n\n    private val galleryDetailUrl: String?\n        get() = if (mGalleryDetail != null && mGalleryDetail!!.gid != -1L && mGalleryDetail!!.token != null) {\n            EhUrl.getGalleryDetailUrl(\n                mGalleryDetail!!.gid,\n                mGalleryDetail!!.token,\n                0,\n                mShowAllComments,\n            )\n        } else {\n            null\n        }\n\n    override fun onClick(v: View) {\n        val context = context\n        val activity = mainActivity\n        if (null == context || null == activity || null == mEditText) {\n            return\n        }\n        if (mFab === v) {\n            if (!mInAnimation) {\n                prepareNewComment()\n                showEditPanel()\n            }\n        } else if (mSendImage === v) {\n            if (!mInAnimation) {\n                val comment = mEditText!!.text.toBBCode()\n                if (TextUtils.isEmpty(comment)) {\n                    // Comment is empty\n                    return\n                }\n                val url = galleryDetailUrl ?: return\n                // Request\n                val request = EhRequest()\n                    .setMethod(EhClient.METHOD_GET_COMMENT_GALLERY)\n                    .setArgs(\n                        url,\n                        comment,\n                        if (mCommentId != 0L) mCommentId.toString() else null,\n                    )\n                    .setCallback(CommentGalleryListener(context, mCommentId))\n                request.enqueue(this)\n                hideSoftInput()\n                hideEditPanel()\n            }\n        }\n    }\n\n    override fun onBackPressed() {\n        if (mInAnimation) {\n            return\n        }\n        if (null != mEditPanel && mEditPanel!!.isVisible) {\n            hideEditPanel()\n        } else {\n            finish()\n        }\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    private fun onRefreshGallerySuccess(result: GalleryCommentList?) {\n        if (mGalleryDetail == null || mAdapter == null) {\n            return\n        }\n        mRefreshLayout!!.isRefreshing = false\n        mRefreshingComments = false\n        mGalleryDetail!!.comments = result\n        mAdapter!!.notifyDataSetChanged()\n        updateView(true)\n\n        val re = Bundle()\n        re.putParcelable(KEY_COMMENT_LIST, result)\n        setResult(RESULT_OK, re)\n    }\n\n    private fun onRefreshGalleryFailure() {\n        if (mAdapter == null) {\n            return\n        }\n        mRefreshLayout!!.isRefreshing = false\n        mRefreshingComments = false\n        val position = mAdapter!!.itemCount - 1\n        if (position >= 0) {\n            mAdapter!!.notifyItemChanged(position)\n        }\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    private fun onCommentGallerySuccess(result: GalleryCommentList) {\n        if (mGalleryDetail == null || mAdapter == null) {\n            return\n        }\n        mGalleryDetail!!.comments = result\n        mAdapter!!.notifyDataSetChanged()\n        val re = Bundle()\n        re.putParcelable(KEY_COMMENT_LIST, result)\n        setResult(RESULT_OK, re)\n\n        // Remove text\n        if (mEditText != null) {\n            mEditText!!.setText(\"\")\n        }\n        updateView(true)\n    }\n\n    private fun onVoteCommentSuccess(result: VoteCommentParser.Result) {\n        if (mAdapter == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {\n            return\n        }\n        var position = -1\n        var i = 0\n        val n = mGalleryDetail!!.comments!!.comments!!.size\n        while (i < n) {\n            val comment = mGalleryDetail!!.comments!!.comments!![i]\n            if (comment.id == result.id) {\n                position = i\n                break\n            }\n            i++\n        }\n        if (-1 == position) {\n            Log.d(TAG, \"Can't find comment with id \" + result.id)\n            return\n        }\n\n        // Update comment\n        val comment = mGalleryDetail!!.comments!!.comments!![position]\n        comment.score = result.score\n        if (result.expectVote > 0) {\n            comment.voteUpEd = 0 != result.vote\n            comment.voteDownEd = false\n        } else {\n            comment.voteDownEd = 0 != result.vote\n            comment.voteUpEd = false\n        }\n        mAdapter!!.notifyItemChanged(position)\n\n        val re = Bundle()\n        val comments = mGalleryDetail!!.comments\n        re.putParcelable(KEY_COMMENT_LIST, comments)\n        setResult(RESULT_OK, re)\n    }\n\n    override fun onRefresh() {\n        if (!mRefreshingComments && mAdapter != null) {\n            val activity = requireActivity() as MainActivity\n            mRefreshingComments = true\n            val url = galleryDetailUrl\n            if (url != null) {\n                // Request\n                val request = EhRequest()\n                    .setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)\n                    .setArgs(url)\n                    .setCallback(RefreshCommentListener(activity))\n                request.enqueue(this)\n            }\n        }\n    }\n\n    private inner class RefreshCommentListener(context: Context) : EhCallback<GalleryCommentsScene?, GalleryDetail>(context) {\n        override fun onSuccess(result: GalleryDetail) {\n            val scene = this@GalleryCommentsScene\n            scene.onRefreshGallerySuccess(result.comments)\n        }\n\n        override fun onFailure(e: Exception) {\n            val scene = this@GalleryCommentsScene\n            scene.onRefreshGalleryFailure()\n        }\n\n        override fun onCancel() {}\n    }\n\n    private inner class CommentGalleryListener(context: Context, private val mCommentId: Long) : EhCallback<GalleryCommentsScene?, GalleryCommentList>(context) {\n        override fun onSuccess(result: GalleryCommentList) {\n            showTip(\n                if (mCommentId != 0L) R.string.edit_comment_successfully else R.string.comment_successfully,\n                LENGTH_SHORT,\n            )\n            val scene = this@GalleryCommentsScene\n            scene.onCommentGallerySuccess(result)\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(\n                \"\"\"\n    ${content.getString(if (mCommentId != 0L) R.string.edit_comment_failed else R.string.comment_failed)}\n    ${ExceptionUtils.getReadableString(e)}\n                \"\"\".trimIndent(),\n                LENGTH_LONG,\n            )\n        }\n\n        override fun onCancel() {}\n    }\n\n    private inner class VoteCommentListener(context: Context) : EhCallback<GalleryCommentsScene?, VoteCommentParser.Result>(context) {\n        override fun onSuccess(result: VoteCommentParser.Result) {\n            showTip(\n                if (result.expectVote > 0) {\n                    if (0 != result.vote) R.string.vote_up_successfully else R.string.cancel_vote_up_successfully\n                } else if (0 != result.vote) {\n                    R.string.vote_down_successfully\n                } else {\n                    R.string.cancel_vote_down_successfully\n                },\n                LENGTH_SHORT,\n            )\n            val scene = this@GalleryCommentsScene\n            scene.onVoteCommentSuccess(result)\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(R.string.vote_failed, LENGTH_LONG)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private class InfoHolder(itemView: View?) :\n        RecyclerView.ViewHolder(\n            itemView!!,\n        ) {\n        val key: TextView = ViewUtils.`$$`(itemView, R.id.key) as TextView\n        val value: TextView = ViewUtils.`$$`(itemView, R.id.value) as TextView\n    }\n\n    private abstract class CommentHolder(inflater: LayoutInflater, resId: Int, parent: ViewGroup?) : RecyclerView.ViewHolder(inflater.inflate(resId, parent, false))\n\n    private class MoreCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) : CommentHolder(inflater, R.layout.item_gallery_comment_more, parent)\n\n    private class ProgressCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) : CommentHolder(inflater, R.layout.item_gallery_comment_progress, parent)\n\n    private inner class ActualCommentHolder(inflater: LayoutInflater, parent: ViewGroup?) : CommentHolder(inflater, R.layout.item_gallery_comment, parent) {\n        private val user: TextView = itemView.findViewById(R.id.user)\n        private val time: TextView = itemView.findViewById(R.id.time)\n        val comment: LinkifyTextView = itemView.findViewById(R.id.comment)\n        lateinit var sp: CharSequence\n\n        private fun generateComment(\n            context: Context,\n            textView: ObservedTextView,\n            comment: GalleryComment,\n        ): CharSequence {\n            sp = loadHtml(comment.comment, textView)\n            val ssb = SpannableStringBuilder(sp)\n            if (0L != comment.id && 0 != comment.score) {\n                val score = comment.score\n                val scoreString = if (score > 0) \"+$score\" else score.toString()\n                ssb.append(\"  \").inSpans(\n                    RelativeSizeSpan(0.8f),\n                    StyleSpan(Typeface.BOLD),\n                    ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),\n                ) {\n                    append(scoreString)\n                }\n            }\n            if (comment.lastEdited != 0L) {\n                val str = context.getString(\n                    R.string.last_edited,\n                    ReadableTime.getTimeAgo(comment.lastEdited),\n                )\n                ssb.append(\"\\n\\n\").inSpans(\n                    RelativeSizeSpan(0.8f),\n                    StyleSpan(Typeface.BOLD),\n                    ForegroundColorSpan(theme.resolveColor(android.R.attr.textColorSecondary)),\n                ) {\n                    append(str)\n                }\n            }\n            return TextUrl.handleTextUrl(ssb)\n        }\n\n        fun bind(value: GalleryComment) {\n            user.text = if (value.uploader) {\n                getString(\n                    R.string.comment_user_uploader,\n                    value.user,\n                )\n            } else {\n                value.user\n            }\n            user.setOnClickListener {\n                if (\"Anonymous\" != value.user) {\n                    val lub = ListUrlBuilder()\n                    lub.mode = ListUrlBuilder.MODE_UPLOADER\n                    lub.keyword = value.user\n                    GalleryListScene.startScene(this@GalleryCommentsScene, lub)\n                }\n            }\n            time.text = ReadableTime.getTimeAgo(value.time)\n            comment.text = generateComment(comment.context, comment, value)\n        }\n    }\n\n    private inner class CommentAdapter : RecyclerView.Adapter<CommentHolder>() {\n        private val mInflater: LayoutInflater = layoutInflater\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentHolder = when (viewType) {\n            TYPE_COMMENT -> ActualCommentHolder(mInflater, parent)\n            TYPE_MORE -> MoreCommentHolder(mInflater, parent)\n            TYPE_PROGRESS -> ProgressCommentHolder(mInflater, parent)\n            else -> throw IllegalStateException(\"Invalid view type: $viewType\")\n        }\n\n        override fun onBindViewHolder(holder: CommentHolder, position: Int) {\n            val context = context\n            if (context == null || mGalleryDetail == null || mGalleryDetail!!.comments == null) {\n                return\n            }\n            holder.itemView.setOnClickListener {\n                onItemClick(\n                    mRecyclerView,\n                    holder.itemView,\n                    position,\n                )\n            }\n            holder.itemView.isClickable = true\n            holder.itemView.isFocusable = true\n            if (holder is ActualCommentHolder) {\n                holder.bind(mGalleryDetail!!.comments!!.comments!![position])\n            }\n        }\n\n        override fun getItemCount(): Int = if (mGalleryDetail == null || mGalleryDetail!!.comments == null || mGalleryDetail!!.comments!!.comments == null) {\n            0\n        } else if (mGalleryDetail!!.comments!!.hasMore) {\n            mGalleryDetail!!.comments!!.comments!!.size + 1\n        } else {\n            mGalleryDetail!!.comments!!.comments!!.size\n        }\n\n        override fun getItemViewType(position: Int): Int = if (position >= mGalleryDetail!!.comments!!.comments!!.size) {\n            if (mRefreshingComments) TYPE_PROGRESS else TYPE_MORE\n        } else {\n            TYPE_COMMENT\n        }\n    }\n\n    companion object {\n        val TAG: String = GalleryCommentsScene::class.java.simpleName\n        const val KEY_API_UID = \"api_uid\"\n        const val KEY_API_KEY = \"api_key\"\n        const val KEY_GID = \"gid\"\n        const val KEY_TOKEN = \"token\"\n        const val KEY_COMMENT_LIST = \"comment_list\"\n        const val KEY_GALLERY_DETAIL = \"gallery_detail\"\n        private const val TYPE_COMMENT = 0\n        private const val TYPE_MORE = 1\n        private const val TYPE_PROGRESS = 2\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryDetailScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.Dialog\nimport android.app.DownloadManager\nimport android.app.ForegroundServiceStartNotAllowedException\nimport android.app.assist.AssistContent\nimport android.content.ActivityNotFoundException\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.content.res.ColorStateList\nimport android.graphics.Color\nimport android.graphics.Typeface\nimport android.os.Bundle\nimport android.os.Environment\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.View.OnLongClickListener\nimport android.view.ViewAnimationUtils\nimport android.view.ViewGroup\nimport android.widget.AdapterView\nimport android.widget.ArrayAdapter\nimport android.widget.FrameLayout\nimport android.widget.ImageView\nimport android.widget.LinearLayout\nimport android.widget.ListView\nimport android.widget.RatingBar\nimport android.widget.ScrollView\nimport android.widget.TextView\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.IntDef\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.content.res.AppCompatResources\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.getSystemService\nimport androidx.core.net.toUri\nimport androidx.core.view.ViewCompat\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentTransaction\nimport androidx.lifecycle.lifecycleScope\nimport androidx.transition.TransitionInflater\nimport com.google.android.material.progressindicator.CircularProgressIndicator\nimport com.google.android.material.snackbar.Snackbar\nimport com.hippo.app.CheckBoxDialogBuilder\nimport com.hippo.app.EditTextDialogBuilder\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.EhApplication.Companion.galleryDetailCache\nimport com.hippo.ehviewer.EhApplication.Companion.imageCache\nimport com.hippo.ehviewer.EhApplication.Companion.okHttpClient\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.UrlOpener\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhFilter\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.ehviewer.client.EhRequestBuilder\nimport com.hippo.ehviewer.client.EhTagDatabase\nimport com.hippo.ehviewer.client.EhTagDatabase.isTranslatable\nimport com.hippo.ehviewer.client.EhTagDatabase.namespaceToPrefix\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.GalleryComment\nimport com.hippo.ehviewer.client.data.GalleryCommentList\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryTagGroup\nimport com.hippo.ehviewer.client.data.ListUrlBuilder\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.client.getImageKey\nimport com.hippo.ehviewer.client.getThumbKey\nimport com.hippo.ehviewer.client.parser.ArchiveParser\nimport com.hippo.ehviewer.client.parser.GalleryDetailParser\nimport com.hippo.ehviewer.client.parser.HomeParser\nimport com.hippo.ehviewer.client.parser.RateGalleryParser\nimport com.hippo.ehviewer.client.parser.TorrentParser\nimport com.hippo.ehviewer.client.thumbUrl\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.dao.Filter\nimport com.hippo.ehviewer.download.DownloadManager as EhDownloadManager\nimport com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener\nimport com.hippo.ehviewer.download.DownloadService\nimport com.hippo.ehviewer.spider.SpiderDen\nimport com.hippo.ehviewer.spider.SpiderInfo\nimport com.hippo.ehviewer.spider.SpiderQueen\nimport com.hippo.ehviewer.spider.SpiderQueen.Companion.GET_FULL_HASH\nimport com.hippo.ehviewer.spider.SpiderQueen.Companion.MODE_READ\nimport com.hippo.ehviewer.spider.SpiderQueen.Companion.SPIDER_INFO_FILENAME\nimport com.hippo.ehviewer.spider.saveToUniFile\nimport com.hippo.ehviewer.ui.CommonOperations\nimport com.hippo.ehviewer.ui.GalleryActivity\nimport com.hippo.ehviewer.widget.GalleryRatingBar\nimport com.hippo.ehviewer.widget.GalleryRatingBar.OnUserRateListener\nimport com.hippo.scene.Announcer\nimport com.hippo.scene.TransitionHelper\nimport com.hippo.util.AppHelper\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.ReadableTime\nimport com.hippo.util.addTextToClipboard\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.util.isAtLeastQ\nimport com.hippo.util.isAtLeastS\nimport com.hippo.util.launchIO\nimport com.hippo.util.loadHtml\nimport com.hippo.util.withUIContext\nimport com.hippo.view.ViewTransition\nimport com.hippo.widget.AutoWrapLayout\nimport com.hippo.widget.LoadImageView\nimport com.hippo.widget.ObservedTextView\nimport com.hippo.widget.SimpleGridAutoSpanLayout\nimport com.hippo.yorozuya.FileUtils\nimport com.hippo.yorozuya.IntIdGenerator\nimport com.hippo.yorozuya.SimpleHandler\nimport com.hippo.yorozuya.ViewUtils\nimport com.hippo.yorozuya.collect.IntList\nimport kotlin.math.abs\nimport kotlin.math.hypot\nimport kotlin.math.max\nimport kotlin.math.roundToInt\nimport okhttp3.HttpUrl.Companion.toHttpUrl\nimport okhttp3.coroutines.executeAsync\nimport rikka.core.res.resolveBoolean\nimport rikka.core.res.resolveColor\n\nclass GalleryDetailScene :\n    BaseScene(),\n    View.OnClickListener,\n    DownloadInfoListener,\n    OnLongClickListener {\n    private var mTip: TextView? = null\n    private var mViewTransition: ViewTransition? = null\n\n    // Header\n    private var mHeader: FrameLayout? = null\n    private var mColorBg: View? = null\n    private var mThumb: LoadImageView? = null\n    private var mTitle: TextView? = null\n    private var mUploader: TextView? = null\n    private var mCategory: TextView? = null\n    private var mBackAction: ImageView? = null\n    private var mOtherActions: ImageView? = null\n    private var mActionGroup: ViewGroup? = null\n    private var mDownload: TextView? = null\n    private var mRead: TextView? = null\n\n    // Below header\n    private var mBelowHeader: View? = null\n\n    // Info\n    private var mInfo: View? = null\n    private var mLanguage: TextView? = null\n    private var mPages: TextView? = null\n    private var mSize: TextView? = null\n    private var mPosted: TextView? = null\n    private var mFavoredTimes: TextView? = null\n    private var mNewerVersion: TextView? = null\n\n    // Actions\n    private var mActions: View? = null\n    private var mRatingText: TextView? = null\n    private var mRating: RatingBar? = null\n    private var mHeartGroup: View? = null\n    private var mHeart: TextView? = null\n    private var mHeartOutline: TextView? = null\n    private var mTorrent: TextView? = null\n    private var mArchive: TextView? = null\n    private var mShare: TextView? = null\n    private var mRate: View? = null\n    private var mSimilar: TextView? = null\n\n    // Tags\n    private var mTags: LinearLayout? = null\n    private var mNoTags: TextView? = null\n\n    // Comments\n    private var mComments: LinearLayout? = null\n    private var mCommentsText: TextView? = null\n\n    // Previews\n    private var mPreviews: View? = null\n    private var mGridLayout: SimpleGridAutoSpanLayout? = null\n    private var mPreviewText: TextView? = null\n\n    // Progress\n    private var mProgress: View? = null\n    private var mViewTransition2: ViewTransition? = null\n    private var mPopupMenu: PopupMenu? = null\n    private var mDownloadState = 0\n    private var mAction: String? = null\n    private var mGalleryInfo: GalleryInfo? = null\n    private var mGid: Long = 0\n    private var mToken: String? = null\n    private var mPage = 0\n    private var mGalleryDetail: GalleryDetail? = null\n    private var mRequestId = IntIdGenerator.INVALID_ID\n    private var mTorrentList: List<TorrentParser.Result>? = null\n    private var requestStoragePermissionLauncher = registerForActivityResult(\n        ActivityResultContracts.RequestPermission(),\n    ) { result: Boolean ->\n        if (result && mGalleryDetail != null) {\n            val helper = TorrentListDialogHelper()\n            val dialog = AlertDialog.Builder(requireActivity())\n                .setTitle(R.string.torrents)\n                .setView(R.layout.dialog_torrent_list)\n                .setOnDismissListener(helper)\n                .show()\n            helper.setDialog(dialog, mGalleryDetail!!.torrentUrl)\n        }\n    }\n    private var mArchiveList: List<ArchiveParser.Archive>? = null\n    private var mCurrentFunds: HomeParser.Funds? = null\n\n    @State\n    private var mState = STATE_INIT\n    private var mModifyingFavorites = false\n\n    @StringRes\n    private fun getRatingText(rating: Float): Int = when ((rating * 2).roundToInt()) {\n        0 -> R.string.rating0\n        1 -> R.string.rating1\n        2 -> R.string.rating2\n        3 -> R.string.rating3\n        4 -> R.string.rating4\n        5 -> R.string.rating5\n        6 -> R.string.rating6\n        7 -> R.string.rating7\n        8 -> R.string.rating8\n        9 -> R.string.rating9\n        10 -> R.string.rating10\n        else -> R.string.rating_none\n    }\n\n    private fun handleArgs(args: Bundle?) {\n        val action = args?.getString(KEY_ACTION) ?: return\n        mAction = action\n        if (ACTION_GALLERY_INFO == action) {\n            mGalleryInfo = args.getParcelableCompat(KEY_GALLERY_INFO)\n            // Add history\n            // DB Actions\n            mGalleryInfo?.let { EhDB.putHistoryInfo(it) }\n        } else if (ACTION_GID_TOKEN == action) {\n            mGid = args.getLong(KEY_GID)\n            mToken = args.getString(KEY_TOKEN)\n            mPage = args.getInt(KEY_PAGE)\n        }\n    }\n\n    private val galleryDetailUrl: String?\n        get() {\n            val gid: Long\n            val token: String?\n            if (mGalleryDetail != null) {\n                gid = mGalleryDetail!!.gid\n                token = mGalleryDetail!!.token\n            } else if (mGalleryInfo != null) {\n                gid = mGalleryInfo!!.gid\n                token = mGalleryInfo!!.token\n            } else if (ACTION_GID_TOKEN == mAction) {\n                gid = mGid\n                token = mToken\n            } else {\n                return null\n            }\n            return EhUrl.getGalleryDetailUrl(gid, token, 0, false)\n        }\n\n    // -1 for error\n    private val gid: Long\n        get() = if (mGalleryDetail != null) {\n            mGalleryDetail!!.gid\n        } else if (mGalleryInfo != null) {\n            mGalleryInfo!!.gid\n        } else if (ACTION_GID_TOKEN == mAction) {\n            mGid\n        } else {\n            -1\n        }\n\n    private val uploader: String?\n        get() = if (mGalleryDetail != null) {\n            mGalleryDetail!!.uploader\n        } else if (mGalleryInfo != null) {\n            mGalleryInfo!!.uploader\n        } else {\n            null\n        }\n\n    // Judging by the uploader to exclude the cooldown period\n    private val disowned: Boolean\n        get() = uploader == \"(Disowned)\"\n\n    // -1 for error\n    private val category: Int\n        get() = if (mGalleryDetail != null) {\n            mGalleryDetail!!.category\n        } else if (mGalleryInfo != null) {\n            mGalleryInfo!!.category\n        } else {\n            -1\n        }\n\n    private val galleryInfo: GalleryInfo?\n        get() = if (null != mGalleryDetail) {\n            mGalleryDetail\n        } else if (null != mGalleryInfo) {\n            mGalleryInfo\n        } else {\n            null\n        }\n\n    override var needWhiteStatusBar = false\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        mRead ?: return\n        mGalleryInfo?.let {\n            // Other Actions\n            viewLifecycleOwner.lifecycleScope.launchIO {\n                runCatching {\n                    val queen = SpiderQueen.obtainSpiderQueen(it, MODE_READ)\n                    val startPage = queen.awaitStartPage()\n                    SpiderQueen.releaseSpiderQueen(queen, MODE_READ)\n                    withUIContext {\n                        mRead!!.text = if (startPage == 0) {\n                            getString(R.string.read)\n                        } else {\n                            getString(R.string.read_from, startPage + 1)\n                        }\n                    }\n                }.onFailure { e ->\n                    e.printStackTrace()\n                }\n            }\n        }\n    }\n\n    private fun onInit() {\n        handleArgs(arguments)\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mAction = savedInstanceState.getString(KEY_ACTION)\n        mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)\n        mGid = savedInstanceState.getLong(KEY_GID)\n        mToken = savedInstanceState.getString(KEY_TOKEN)\n        mGalleryDetail = savedInstanceState.getParcelableCompat(KEY_GALLERY_DETAIL)\n        mRequestId = savedInstanceState.getInt(KEY_REQUEST_ID)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        if (mAction != null) {\n            outState.putString(KEY_ACTION, mAction)\n        }\n        if (mGalleryInfo != null) {\n            outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo)\n        }\n        outState.putLong(KEY_GID, mGid)\n        if (mToken != null) {\n            outState.putString(KEY_TOKEN, mAction)\n        }\n        if (mGalleryDetail != null) {\n            outState.putParcelable(KEY_GALLERY_DETAIL, mGalleryDetail)\n        }\n        outState.putInt(KEY_REQUEST_ID, mRequestId)\n    }\n\n    private fun ensurePopMenu() {\n        if (mPopupMenu != null || mOtherActions == null) {\n            return\n        }\n        val popup = PopupMenu(requireContext(), mOtherActions!! as View)\n        mPopupMenu = popup\n        popup.menuInflater.inflate(R.menu.scene_gallery_detail, popup.menu)\n        popup.setOnMenuItemClickListener(\n            object : PopupMenu.OnMenuItemClickListener {\n                override fun onMenuItemClick(item: MenuItem): Boolean {\n                    when (item.itemId) {\n                        R.id.action_refresh -> {\n                            if (mState != STATE_REFRESH && mState != STATE_REFRESH_HEADER) {\n                                adjustViewVisibility(STATE_REFRESH, true)\n                                request()\n                            }\n                            return true\n                        }\n                        R.id.action_add_tag -> {\n                            if (mGalleryDetail == null) {\n                                return false\n                            }\n                            if (mGalleryDetail!!.apiUid < 0) {\n                                showTip(R.string.error_please_login_first, LENGTH_LONG)\n                                return false\n                            }\n                            val builder =\n                                EditTextDialogBuilder(requireContext(), \"\", getString(R.string.action_add_tag_tip))\n                            builder.setPositiveButton(android.R.string.ok, null)\n                            val dialog = builder.setTitle(R.string.action_add_tag)\n                                .show()\n                            dialog.getButton(DialogInterface.BUTTON_POSITIVE)\n                                .setOnClickListener {\n                                    voteTag(builder.text.trim { it <= ' ' }, 1)\n                                    dialog.dismiss()\n                                }\n                            return true\n                        }\n                        R.id.action_clear_image_cache -> {\n                            if (mGalleryDetail == null) {\n                                return false\n                            }\n                            SpiderQueen.reset(mGalleryDetail!!.gid)\n                            (0..<mGalleryDetail!!.pages).forEach {\n                                val key = getImageKey(mGalleryDetail!!.gid, it)\n                                imageCache.remove(key)\n                            }\n                            showTip(R.string.action_image_cache_cleared, LENGTH_LONG)\n                            return true\n                        }\n                        R.id.action_open_in_other_app -> {\n                            val url = galleryDetailUrl\n                            val activity: Activity? = mainActivity\n                            if (null != url && null != activity) {\n                                UrlOpener.openUrl(activity, url, false)\n                            }\n                            return true\n                        }\n                    }\n                    return false\n                }\n            },\n        )\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        // Get download state\n        val gid = gid\n        mDownloadState = if (gid != -1L) {\n            EhDownloadManager.getDownloadState(gid)\n        } else {\n            DownloadInfo.STATE_INVALID\n        }\n        val view = inflater.inflate(R.layout.scene_gallery_detail, container, false)\n        val main = ViewUtils.`$$`(view, R.id.main) as ViewGroup\n        val mainView = ViewUtils.`$$`(main, R.id.scroll_view) as ScrollView\n        mainView.setOnScrollChangeListener { _, _, scrollY, _, _ ->\n            if (mActionGroup != null && mHeader != null) {\n                setLightStatusBar(\n                    (\n                        mActionGroup!!.y - mHeader!!.findViewById<View>(R.id.header_content)\n                            .paddingTop / 2f\n                        ).toInt() < scrollY,\n                )\n            }\n        }\n        val progressView = ViewUtils.`$$`(main, R.id.progress_view)\n        mTip = ViewUtils.`$$`(main, R.id.tip) as TextView\n        mViewTransition = ViewTransition(mainView, progressView, mTip)\n        val drawable = AppCompatResources.getDrawable(requireContext(), R.drawable.big_sad_pandroid)\n        drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n        mTip!!.setCompoundDrawables(null, drawable, null, null)\n        mTip!!.setOnClickListener(this)\n        mHeader = ViewUtils.`$$`(mainView, R.id.header) as FrameLayout\n        mColorBg = ViewUtils.`$$`(mHeader, R.id.color_bg)\n        mThumb = ViewUtils.`$$`(mHeader, R.id.thumb) as LoadImageView\n        mTitle = ViewUtils.`$$`(mHeader, R.id.title) as TextView\n        mUploader = ViewUtils.`$$`(mHeader, R.id.uploader) as TextView\n        mCategory = ViewUtils.`$$`(mHeader, R.id.category) as TextView\n        mBackAction = ViewUtils.`$$`(mHeader, R.id.back_action) as ImageView\n        mOtherActions = ViewUtils.`$$`(mHeader, R.id.other_actions) as ImageView\n        mActionGroup = ViewUtils.`$$`(mHeader, R.id.action_card) as ViewGroup\n        mDownload = ViewUtils.`$$`(mActionGroup, R.id.download) as TextView\n        mRead = ViewUtils.`$$`(mActionGroup, R.id.read) as TextView\n        mUploader!!.setOnClickListener(this)\n        mCategory!!.setOnClickListener(this)\n        mBackAction!!.setOnClickListener(this)\n        mOtherActions!!.setOnClickListener(this)\n        mDownload!!.setOnClickListener(this)\n        mDownload!!.setOnLongClickListener(this)\n        mRead!!.setOnClickListener(this)\n        mUploader!!.setOnLongClickListener(this)\n        mBelowHeader = mainView.findViewById(R.id.below_header)\n        val belowHeader = mBelowHeader\n        mInfo = ViewUtils.`$$`(belowHeader, R.id.info)\n        mLanguage = ViewUtils.`$$`(mInfo, R.id.language) as TextView\n        mPages = ViewUtils.`$$`(mInfo, R.id.pages) as TextView\n        mSize = ViewUtils.`$$`(mInfo, R.id.size) as TextView\n        mPosted = ViewUtils.`$$`(mInfo, R.id.posted) as TextView\n        mFavoredTimes = ViewUtils.`$$`(mInfo, R.id.favoredTimes) as TextView\n        mInfo!!.setOnClickListener(this)\n        mActions = ViewUtils.`$$`(belowHeader, R.id.actions)\n        mNewerVersion = ViewUtils.`$$`(mActions, R.id.newerVersion) as TextView\n        mRatingText = ViewUtils.`$$`(mActions, R.id.rating_text) as TextView\n        mRating = ViewUtils.`$$`(mActions, R.id.rating) as RatingBar\n        mHeartGroup = ViewUtils.`$$`(mActions, R.id.heart_group)\n        mHeart = ViewUtils.`$$`(mHeartGroup, R.id.heart) as TextView\n        mHeartOutline = ViewUtils.`$$`(mHeartGroup, R.id.heart_outline) as TextView\n        mTorrent = ViewUtils.`$$`(mActions, R.id.torrent) as TextView\n        mArchive = ViewUtils.`$$`(mActions, R.id.archive) as TextView\n        mShare = ViewUtils.`$$`(mActions, R.id.share) as TextView\n        mRate = ViewUtils.`$$`(mActions, R.id.rate)\n        mSimilar = ViewUtils.`$$`(mActions, R.id.similar) as TextView\n        mNewerVersion!!.setOnClickListener(this)\n        mHeartGroup!!.setOnClickListener(this)\n        mHeartGroup!!.setOnLongClickListener(this)\n        mTorrent!!.setOnClickListener(this)\n        mArchive!!.setOnClickListener(this)\n        mShare!!.setOnClickListener(this)\n        mRate!!.setOnClickListener(this)\n        mSimilar!!.setOnClickListener(this)\n        ensureActionDrawable()\n        mTags = ViewUtils.`$$`(belowHeader, R.id.tags) as LinearLayout\n        mNoTags = ViewUtils.`$$`(mTags, R.id.no_tags) as TextView\n        mComments = ViewUtils.`$$`(belowHeader, R.id.comments) as LinearLayout\n        if (Settings.showComments) {\n            mCommentsText = ViewUtils.`$$`(mComments, R.id.comments_text) as TextView\n            mComments!!.setOnClickListener(this)\n        } else {\n            mComments!!.visibility = View.GONE\n        }\n        mPreviews = ViewUtils.`$$`(belowHeader, R.id.previews)\n        mGridLayout = ViewUtils.`$$`(mPreviews, R.id.grid_layout) as SimpleGridAutoSpanLayout\n        mPreviewText = ViewUtils.`$$`(mPreviews, R.id.preview_text) as TextView\n        mPreviews!!.setOnClickListener(this)\n        mProgress = ViewUtils.`$$`(mainView, R.id.progress)\n        mViewTransition2 = ViewTransition(mBelowHeader, mProgress)\n        if (prepareData()) {\n            if (mGalleryDetail != null) {\n                bindViewSecond()\n                setTransitionName()\n                adjustViewVisibility(STATE_NORMAL, false)\n            } else if (mGalleryInfo != null) {\n                bindViewFirst()\n                setTransitionName()\n                adjustViewVisibility(STATE_REFRESH_HEADER, false)\n            } else {\n                adjustViewVisibility(STATE_REFRESH, false)\n            }\n        } else {\n            mTip!!.setText(R.string.error_cannot_find_gallery)\n            adjustViewVisibility(STATE_FAILED, false)\n        }\n        EhDownloadManager.addDownloadInfoListener(this)\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        EhDownloadManager.removeDownloadInfoListener(this)\n        mTip = null\n        mViewTransition = null\n        mHeader = null\n        mColorBg = null\n        mThumb = null\n        mTitle = null\n        mUploader = null\n        mCategory = null\n        mBackAction = null\n        mOtherActions = null\n        mActionGroup = null\n        mDownload = null\n        mRead = null\n        mBelowHeader = null\n        mInfo = null\n        mLanguage = null\n        mPages = null\n        mSize = null\n        mPosted = null\n        mFavoredTimes = null\n        mActions = null\n        mNewerVersion = null\n        mRatingText = null\n        mRating = null\n        mHeartGroup = null\n        mHeart = null\n        mHeartOutline = null\n        mTorrent = null\n        mArchive = null\n        mShare = null\n        mRate = null\n        mSimilar = null\n        mTags = null\n        mNoTags = null\n        mComments = null\n        mCommentsText = null\n        mPreviews = null\n        mGridLayout = null\n        mPreviewText = null\n        mProgress = null\n        mViewTransition2 = null\n        mPopupMenu = null\n    }\n\n    private fun prepareData(): Boolean {\n        if (mGalleryDetail != null) {\n            return true\n        }\n        val gid = gid\n        if (gid == -1L) {\n            return false\n        }\n        // Get from cache\n        mGalleryDetail = galleryDetailCache[gid]\n        if (mGalleryDetail != null) {\n            return true\n        }\n        val application = requireContext().applicationContext as EhApplication\n        return if (application.containGlobalStuff(mRequestId)) {\n            // request exist\n            true\n        } else {\n            request()\n        }\n    }\n\n    private fun request(): Boolean {\n        val context = context\n        val activity = mainActivity\n        val url = galleryDetailUrl\n        if (null == context || null == activity || null == url) {\n            return false\n        }\n        val callback: EhClient.Callback<*> = GetGalleryDetailListener(context)\n        mRequestId = (context.applicationContext as EhApplication).putGlobalStuff(callback)\n        val request = EhRequest()\n            .setMethod(EhClient.METHOD_GET_GALLERY_DETAIL)\n            .setArgs(url)\n            .setCallback(callback)\n        request.enqueue(this)\n        return true\n    }\n\n    private fun setActionDrawable(text: TextView?, @DrawableRes resId: Int) {\n        text ?: return\n        val drawable = AppCompatResources.getDrawable(text.context, resId) ?: return\n        drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n        text.setCompoundDrawables(null, drawable, null, null)\n    }\n\n    private fun ensureActionDrawable() {\n        setActionDrawable(mHeart, R.drawable.v_heart_primary_x48)\n        setActionDrawable(mHeartOutline, R.drawable.v_heart_outline_primary_x48)\n        setActionDrawable(mTorrent, R.drawable.v_utorrent_primary_x48)\n        setActionDrawable(mArchive, R.drawable.v_archive_primary_x48)\n        setActionDrawable(mShare, R.drawable.v_share_primary_x48)\n        setActionDrawable(mSimilar, R.drawable.v_similar_primary_x48)\n    }\n\n    private fun createCircularReveal(): Boolean {\n        val context = context\n        if (null == context || null == mColorBg) {\n            return false\n        }\n        val w = mColorBg!!.width\n        val h = mColorBg!!.height\n        return if (mColorBg!!.isAttachedToWindow && w != 0 && h != 0) {\n            val resources = context.resources\n            val keylineMargin = resources.getDimensionPixelSize(R.dimen.keyline_margin)\n            val thumbWidth = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_width)\n            val thumbHeight = resources.getDimensionPixelSize(R.dimen.gallery_detail_thumb_height)\n            val x = thumbWidth / 2 + keylineMargin\n            val y = thumbHeight / 2 + keylineMargin\n            val radiusX = max(abs(x), abs(w - x)).toDouble()\n            val radiusY = max(abs(y), abs(h - y)).toDouble()\n            val radius = hypot(radiusX, radiusY).toFloat()\n            ViewAnimationUtils.createCircularReveal(mColorBg!!, x, y, 0f, radius).setDuration(300).start()\n            true\n        } else {\n            false\n        }\n    }\n\n    @Suppress(\"KotlinConstantConditions\", \"SimplifyBooleanWithConstants\")\n    private fun adjustViewVisibility(state: Int, animation: Boolean) {\n        if (state == mState || mViewTransition == null || mViewTransition2 == null) {\n            return\n        }\n        val oldState = mState\n        mState = state\n        val doAnimation = !TRANSITION_ANIMATION_DISABLED && animation\n        when (state) {\n            STATE_NORMAL -> {\n                setLightStatusBar(false)\n                // Show mMainView\n                mViewTransition!!.showView(0, doAnimation)\n                // Show mBelowHeader\n                mViewTransition2!!.showView(0, doAnimation)\n            }\n            STATE_REFRESH -> {\n                setLightStatusBar(true)\n                // Show mProgressView\n                mViewTransition!!.showView(1, doAnimation)\n            }\n            STATE_REFRESH_HEADER -> {\n                setLightStatusBar(false)\n                // Show mMainView\n                mViewTransition!!.showView(0, doAnimation)\n                // Show mProgress\n                mViewTransition2!!.showView(1, doAnimation)\n            }\n            STATE_INIT, STATE_FAILED -> {\n                setLightStatusBar(true)\n                // Show mFailedView\n                mViewTransition!!.showView(2, doAnimation)\n            }\n        }\n        if ((oldState == STATE_INIT || oldState == STATE_FAILED || oldState == STATE_REFRESH) &&\n            (state == STATE_NORMAL || state == STATE_REFRESH_HEADER) &&\n            theme.resolveBoolean(androidx.appcompat.R.attr.isLightTheme, false)\n        ) {\n            if (!createCircularReveal()) {\n                SimpleHandler.getInstance().post(this::createCircularReveal)\n            }\n        }\n    }\n\n    private fun bindViewFirst() {\n        if (mGalleryDetail != null || mThumb == null || mTitle == null || mUploader == null || mCategory == null) {\n            return\n        }\n        if (ACTION_GALLERY_INFO == mAction && mGalleryInfo != null) {\n            val gi: GalleryInfo = mGalleryInfo!!\n            mThumb!!.load(getThumbKey(gi.gid), gi.thumbUrl!!, hardware = false)\n            mTitle!!.text = EhUtils.getSuitableTitle(gi)\n            mUploader!!.text = gi.uploader\n            mUploader!!.alpha = if (gi.disowned) .5f else 1f\n            mCategory!!.text = EhUtils.getCategory(gi.category)\n            mCategory!!.setTextColor(EhUtils.getCategoryColor(gi.category))\n            updateDownloadText()\n        }\n    }\n\n    private fun updateFavoriteDrawable() {\n        val gd = mGalleryDetail ?: return\n        if (mHeart == null || mHeartOutline == null) {\n            return\n        }\n        // DB Actions\n        if (gd.isFavorited || EhDB.containLocalFavorites(gd.gid)) {\n            mHeart!!.visibility = View.VISIBLE\n            if (gd.favoriteName == null) {\n                mHeart!!.setText(R.string.local_favorites)\n            } else {\n                mHeart!!.text = gd.favoriteName\n            }\n            mHeartOutline!!.visibility = View.GONE\n        } else {\n            mHeart!!.visibility = View.GONE\n            mHeartOutline!!.visibility = View.VISIBLE\n        }\n    }\n\n    private fun bindViewSecond() {\n        context ?: return\n        val gd = mGalleryDetail ?: return\n        if (mPage != 0) {\n            Snackbar.make(\n                requireActivity().findViewById(R.id.snackbar),\n                getString(R.string.read_from, mPage + 1),\n                Snackbar.LENGTH_LONG,\n            )\n                .setAction(R.string.read) {\n                    val intent = Intent(context, GalleryActivity::class.java)\n                    intent.action = GalleryActivity.ACTION_EH\n                    intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mGalleryDetail)\n                    intent.putExtra(GalleryActivity.KEY_PAGE, mPage)\n                    startActivity(intent)\n                }\n                .show()\n        }\n        if (mThumb == null || mTitle == null || mUploader == null || mCategory == null || mLanguage == null || mPages == null || mSize == null || mPosted == null || mFavoredTimes == null || mRatingText == null || mRating == null || mTorrent == null || mNewerVersion == null) {\n            return\n        }\n        val resources = resources\n        mThumb!!.load(getThumbKey(gd.gid), gd.thumbUrl!!, false, hardware = false)\n        mTitle!!.text = EhUtils.getSuitableTitle(gd)\n        mUploader!!.text = gd.uploader\n        mUploader!!.alpha = if (gd.disowned) .5f else 1f\n        mCategory!!.text = EhUtils.getCategory(gd.category)\n        mCategory!!.setTextColor(EhUtils.getCategoryColor(gd.category))\n        updateDownloadText()\n        mLanguage!!.text = gd.language\n        mPages!!.text = resources.getQuantityString(\n            R.plurals.page_count,\n            gd.pages,\n            gd.pages,\n        )\n        mSize!!.text = gd.size\n        mPosted!!.text = gd.posted\n        mFavoredTimes!!.text = resources.getString(R.string.favored_times, gd.favoriteCount)\n        if (gd.newerVersions.isNotEmpty()) {\n            mNewerVersion!!.visibility = View.VISIBLE\n        }\n        mRatingText!!.text = getAllRatingText(gd.rating, gd.ratingCount)\n        mRating!!.rating = gd.rating\n        updateFavoriteDrawable()\n        mTorrent!!.text = resources.getString(R.string.torrent_count, gd.torrentCount)\n        bindTags(gd.tags)\n        bindComments(gd.comments!!.comments)\n        bindPreviews(gd)\n    }\n\n    private fun bindTags(tagGroups: Array<GalleryTagGroup>?) {\n        val context = context\n        if (null == context || null == mTags || null == mNoTags) {\n            return\n        }\n        mTags!!.removeViews(1, mTags!!.childCount - 1)\n        if (tagGroups.isNullOrEmpty()) {\n            mNoTags!!.visibility = View.VISIBLE\n            return\n        } else {\n            mNoTags!!.visibility = View.GONE\n        }\n        val ehTags =\n            if (Settings.showTagTranslations && isTranslatable(context)) EhTagDatabase else null\n        val colorTag = theme.resolveColor(R.attr.tagBackgroundColor)\n        val colorName = theme.resolveColor(R.attr.tagGroupBackgroundColor)\n        for (tgs in tagGroups) {\n            val ll = layoutInflater.inflate(R.layout.gallery_tag_group, mTags, false) as LinearLayout\n            ll.orientation = LinearLayout.HORIZONTAL\n            mTags!!.addView(ll)\n            var readableTagName: String? = null\n            if (ehTags != null && ehTags.isInitialized()) {\n                readableTagName = ehTags.getTranslation(tag = tgs.groupName)\n            }\n            val tgName = layoutInflater.inflate(R.layout.item_gallery_tag, ll, false) as TextView\n            ll.addView(tgName)\n            tgName.text = readableTagName ?: tgs.groupName\n            tgName.backgroundTintList = ColorStateList.valueOf(colorName)\n            val prefix = namespaceToPrefix(tgs.groupName!!)\n            val awl = AutoWrapLayout(context)\n            ll.addView(\n                awl,\n                ViewGroup.LayoutParams.WRAP_CONTENT,\n                ViewGroup.LayoutParams.WRAP_CONTENT,\n            )\n            for (tg in tgs) {\n                val tag = layoutInflater.inflate(R.layout.item_gallery_tag, awl, false) as TextView\n                awl.addView(tag)\n                var tagStr = tg\n                var status: String? = null\n                while (tagStr.startsWith(\"_\")) {\n                    when (tagStr.substring(1, 2)) {\n                        \"W\" -> tag.alpha = 0.5f\n                        \"L\" -> tag.setTypeface(tag.typeface, Typeface.ITALIC)\n                        \"U\" -> status = TAG_STATUS_UP\n                        \"D\" -> status = TAG_STATUS_DN\n                    }\n                    tagStr = tagStr.substring(2)\n                }\n                var readableTag: String? = null\n                if (ehTags != null && ehTags.isInitialized()) {\n                    readableTag = ehTags.getTranslation(prefix, tagStr)\n                }\n                var tagText = readableTag ?: tagStr\n                if (status != null) {\n                    tagText += status\n                } else if (tag.typeface.isItalic) {\n                    tagText += \" \"\n                }\n                tag.text = tagText\n                tag.backgroundTintList = ColorStateList.valueOf(colorTag)\n                tag.setTag(R.id.tag, tgs.groupName + \":\" + tagStr)\n                tag.setOnClickListener(this)\n                tag.setOnLongClickListener(this)\n            }\n        }\n    }\n\n    private fun bindComments(comments: Array<GalleryComment>?) {\n        val context = context\n        val inflater = layoutInflater\n        if (null == context || null == mComments || null == mCommentsText) {\n            return\n        }\n        mComments!!.removeViews(0, mComments!!.childCount - 1)\n        val maxShowCount = 2\n        if (comments.isNullOrEmpty()) {\n            mCommentsText!!.setText(R.string.no_comments)\n            return\n        } else if (comments.size <= maxShowCount) {\n            mCommentsText!!.setText(R.string.no_more_comments)\n        } else {\n            mCommentsText!!.setText(R.string.more_comment)\n        }\n        val length = maxShowCount.coerceAtMost(comments.size)\n        for (i in 0 until length) {\n            val comment = comments[i]\n            val v = inflater.inflate(R.layout.item_gallery_comment, mComments, false)\n            mComments!!.addView(v, i)\n            val user = v.findViewById<TextView>(R.id.user)\n            user.text = comment.user\n            user.setBackgroundColor(Color.TRANSPARENT)\n            val time = v.findViewById<TextView>(R.id.time)\n            time.text = ReadableTime.getTimeAgo(comment.time)\n            val c = v.findViewById<ObservedTextView>(R.id.comment)\n            c.maxLines = 5\n            c.text = loadHtml(comment.comment, c)\n            v.setBackgroundColor(Color.TRANSPARENT)\n        }\n    }\n\n    @SuppressLint(\"SetTextI18n\")\n    private fun bindPreviews(gd: GalleryDetail) {\n        val inflater = layoutInflater\n        val resources = resourcesOrNull\n        if (null == resources || null == mGridLayout || null == mPreviewText) {\n            return\n        }\n        mGridLayout!!.removeAllViews()\n        val previewSet = gd.previewSet\n        val previewNum = Settings.previewNum\n        if (gd.previewPages <= 0 || previewSet == null || previewSet.size() == 0) {\n            mPreviewText!!.setText(R.string.no_previews)\n            return\n        } else if (gd.previewPages == 1 && previewSet.size() <= previewNum) {\n            mPreviewText!!.setText(R.string.no_more_previews)\n        } else {\n            mPreviewText!!.setText(R.string.more_previews)\n        }\n        mGridLayout!!.setColumnSize(Settings.previewSize)\n        mGridLayout!!.setStrategy(SimpleGridAutoSpanLayout.STRATEGY_SUITABLE_SIZE)\n        val size = previewNum.coerceAtMost(previewSet.size())\n        for (i in 0 until size) {\n            val view = inflater.inflate(R.layout.item_gallery_preview, mGridLayout, false)\n            val image = view.findViewById<LoadImageView>(R.id.image)\n            mGridLayout!!.addView(view)\n            image.setTag(R.id.index, i)\n            image.setOnClickListener(this)\n            val text = view.findViewById<TextView>(R.id.text)\n            text.text = (previewSet.getPosition(i) + 1).toString()\n            previewSet.load(image, gd.gid, i)\n        }\n    }\n\n    private fun getAllRatingText(rating: Float, ratingCount: Int): String = getString(\n        R.string.rating_text,\n        getString(getRatingText(rating)),\n        rating,\n        ratingCount,\n    )\n\n    private fun setTransitionName() {\n        val gid = gid\n        if (gid != -1L &&\n            mThumb != null &&\n            mTitle != null &&\n            mUploader != null &&\n            mCategory != null\n        ) {\n            ViewCompat.setTransitionName(mThumb!!, TransitionNameFactory.getThumbTransitionName(gid))\n            ViewCompat.setTransitionName(mTitle!!, TransitionNameFactory.getTitleTransitionName(gid))\n            ViewCompat.setTransitionName(mUploader!!, TransitionNameFactory.getUploaderTransitionName(gid))\n            ViewCompat.setTransitionName(mCategory!!, TransitionNameFactory.getCategoryTransitionName(gid))\n        }\n    }\n\n    private fun showSimilarGalleryList() {\n        val gd = mGalleryDetail ?: return\n        val keyword = EhUtils.extractTitle(gd.title)\n        if (null != keyword) {\n            val lub = ListUrlBuilder()\n            lub.mode = ListUrlBuilder.MODE_NORMAL\n            lub.keyword = \"\\\"\" + keyword + \"\\\"\"\n            GalleryListScene.startScene(this, lub)\n            return\n        }\n        val artist = getArtist(gd.tags)\n        if (null != artist) {\n            val lub = ListUrlBuilder()\n            lub.mode = ListUrlBuilder.MODE_TAG\n            lub.keyword = \"artist:$artist\"\n            GalleryListScene.startScene(this, lub)\n            return\n        }\n        if (null != gd.uploader) {\n            val lub = ListUrlBuilder()\n            lub.mode = ListUrlBuilder.MODE_UPLOADER\n            lub.keyword = gd.uploader\n            GalleryListScene.startScene(this, lub)\n        }\n    }\n\n    override fun onClick(v: View) {\n        val context = context\n        val activity = mainActivity\n        if (null == context || null == activity) {\n            return\n        }\n        if (v == mTip && request()) {\n            adjustViewVisibility(STATE_REFRESH, true)\n        }\n        val galleryDetail = mGalleryDetail ?: return\n        when (v) {\n            mBackAction -> {\n                onBackPressed()\n            }\n            mOtherActions -> {\n                ensurePopMenu()\n                mPopupMenu?.show()\n            }\n            mUploader -> {\n                if (uploader.isNullOrEmpty() || disowned) {\n                    return\n                }\n                val lub = ListUrlBuilder()\n                lub.mode = ListUrlBuilder.MODE_UPLOADER\n                lub.keyword = uploader\n                GalleryListScene.startScene(this, lub)\n            }\n            mCategory -> {\n                val category = category\n                if (category == EhUtils.NONE || category == EhUtils.PRIVATE || category == EhUtils.UNKNOWN) {\n                    return\n                }\n                val lub = ListUrlBuilder()\n                lub.category = category\n                GalleryListScene.startScene(this, lub)\n            }\n            mDownload -> {\n                val downloadState = EhDownloadManager.getDownloadState(galleryDetail.gid)\n                when (downloadState) {\n                    DownloadInfo.STATE_INVALID -> {\n                        // CommonOperations Actions\n                        CommonOperations.startDownload(activity, galleryDetail, false)\n                    }\n                    DownloadInfo.STATE_FINISH if galleryDetail.newerVersions.isNotEmpty() -> {\n                        showGalleryUpgradeDialog(galleryDetail)\n                    }\n                    else -> {\n                        val builder = CheckBoxDialogBuilder(\n                            context,\n                            getString(R.string.download_remove_dialog_message, galleryDetail.title),\n                            getString(R.string.download_remove_dialog_check_text),\n                            Settings.removeImageFiles,\n                        )\n                        val helper = DeleteDialogHelper(galleryDetail, builder)\n                        builder.setTitle(R.string.download_remove_dialog_title)\n                            .setPositiveButton(android.R.string.ok, helper)\n                            .show()\n                    }\n                }\n            }\n            mRead -> {\n                val intent = Intent(activity, GalleryActivity::class.java)\n                intent.action = GalleryActivity.ACTION_EH\n                intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail)\n                startActivity(intent)\n            }\n            mNewerVersion -> {\n                val titles = ArrayList<CharSequence>()\n                for (newerVersion in galleryDetail.newerVersions) {\n                    titles.add(\n                        getString(\n                            R.string.newer_version_title,\n                            newerVersion.title,\n                            newerVersion.posted,\n                        ),\n                    )\n                }\n                AlertDialog.Builder(requireContext())\n                    .setItems(titles.toTypedArray()) { _: DialogInterface?, which: Int ->\n                        val newerVersion = galleryDetail.newerVersions[which]\n                        val args = Bundle()\n                        args.putString(KEY_ACTION, ACTION_GID_TOKEN)\n                        args.putLong(KEY_GID, newerVersion.gid)\n                        args.putString(KEY_TOKEN, newerVersion.token)\n                        startScene(Announcer(GalleryDetailScene::class.java).setArgs(args))\n                    }\n                    .show()\n            }\n            mInfo -> {\n                val args = Bundle()\n                args.putParcelable(GalleryInfoScene.KEY_GALLERY_DETAIL, galleryDetail)\n                startScene(Announcer(GalleryInfoScene::class.java).setArgs(args))\n            }\n            mHeartGroup -> {\n                // DB Actions\n                // CommonOperations Actions\n                if (!mModifyingFavorites) {\n                    mModifyingFavorites = true\n                    val isLocalFavorites = EhDB.containLocalFavorites(galleryDetail.gid)\n                    val isOnlineFavorites = galleryDetail.isFavorited\n                    if (isLocalFavorites || isOnlineFavorites) {\n                        CommonOperations.removeFromFavorites(\n                            activity,\n                            galleryDetail,\n                            ModifyFavoritesListener(context, true),\n                            isLocalFavorites && !isOnlineFavorites,\n                        )\n                    } else {\n                        CommonOperations.addToFavorites(\n                            activity,\n                            galleryDetail,\n                            ModifyFavoritesListener(context, false),\n                            false,\n                        )\n                    }\n                    // Update UI\n                    updateFavoriteDrawable()\n                }\n            }\n            mShare -> {\n                galleryDetailUrl?.let {\n                    AppHelper.share(activity, it)\n                }\n            }\n            mTorrent -> {\n                if (!isAtLeastQ &&\n                    ContextCompat.checkSelfPermission(\n                        activity,\n                        Manifest.permission.WRITE_EXTERNAL_STORAGE,\n                    ) != PackageManager.PERMISSION_GRANTED\n                ) {\n                    requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)\n                } else {\n                    val helper = TorrentListDialogHelper()\n                    val dialog = AlertDialog.Builder(context)\n                        .setTitle(R.string.torrents)\n                        .setView(R.layout.dialog_torrent_list)\n                        .setOnDismissListener(helper)\n                        .show()\n                    helper.setDialog(dialog, galleryDetail.torrentUrl)\n                }\n            }\n            mArchive -> {\n                if (!isAtLeastQ &&\n                    ContextCompat.checkSelfPermission(\n                        activity,\n                        Manifest.permission.WRITE_EXTERNAL_STORAGE,\n                    ) != PackageManager.PERMISSION_GRANTED\n                ) {\n                    requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)\n                } else {\n                    if (galleryDetail.apiUid < 0) {\n                        showTip(R.string.error_please_login_first, LENGTH_LONG)\n                        return\n                    }\n                    val helper = ArchiveListDialogHelper()\n                    val dialog = AlertDialog.Builder(context)\n                        .setTitle(R.string.settings_download)\n                        .setView(R.layout.dialog_archive_list)\n                        .setOnDismissListener(helper)\n                        .show()\n                    helper.setDialog(dialog, galleryDetail.archiveUrl)\n                }\n            }\n            mRate -> {\n                if (galleryDetail.apiUid < 0) {\n                    showTip(R.string.error_please_login_first, LENGTH_LONG)\n                    return\n                }\n                val helper = RateDialogHelper()\n                val dialog = AlertDialog.Builder(context)\n                    .setTitle(R.string.rate)\n                    .setView(R.layout.dialog_rate)\n                    .setNegativeButton(android.R.string.cancel, null)\n                    .setPositiveButton(android.R.string.ok, helper)\n                    .show()\n                helper.setDialog(dialog, galleryDetail.rating)\n            }\n            mSimilar -> {\n                showSimilarGalleryList()\n            }\n            mComments -> {\n                val args = Bundle()\n                args.putLong(GalleryCommentsScene.KEY_API_UID, galleryDetail.apiUid)\n                args.putString(GalleryCommentsScene.KEY_API_KEY, galleryDetail.apiKey)\n                args.putLong(GalleryCommentsScene.KEY_GID, galleryDetail.gid)\n                args.putString(GalleryCommentsScene.KEY_TOKEN, galleryDetail.token)\n                args.putParcelable(GalleryCommentsScene.KEY_COMMENT_LIST, galleryDetail.comments)\n                args.putParcelable(GalleryCommentsScene.KEY_GALLERY_DETAIL, galleryDetail)\n                startScene(\n                    Announcer(GalleryCommentsScene::class.java)\n                        .setArgs(args)\n                        .setRequestCode(this, REQUEST_CODE_COMMENT_GALLERY),\n                )\n            }\n            mPreviews -> {\n                val previewNum = Settings.previewNum\n                var scrollTo = 0\n                if (previewNum < (galleryDetail.previewSet?.size() ?: 0)) {\n                    scrollTo = previewNum\n                } else if (galleryDetail.previewPages > 1) {\n                    scrollTo = -1\n                }\n                val args = Bundle()\n                args.putParcelable(GalleryPreviewsScene.KEY_GALLERY_INFO, galleryDetail)\n                args.putInt(GalleryPreviewsScene.KEY_SCROLL_TO, scrollTo)\n                startScene(Announcer(GalleryPreviewsScene::class.java).setArgs(args))\n            }\n            else -> {\n                var o = v.getTag(R.id.tag)\n                if (o is String) {\n                    val lub = ListUrlBuilder()\n                    lub.mode = ListUrlBuilder.MODE_TAG\n                    lub.keyword = o\n                    GalleryListScene.startScene(this, lub)\n                    return\n                }\n                o = v.getTag(R.id.index)\n                if (o is Int) {\n                    val intent = Intent(context, GalleryActivity::class.java)\n                    intent.action = GalleryActivity.ACTION_EH\n                    intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, galleryDetail)\n                    intent.putExtra(GalleryActivity.KEY_PAGE, o)\n                    startActivity(intent)\n                }\n            }\n        }\n    }\n\n    private fun showGalleryUpgradeDialog(gd: GalleryDetail) {\n        val context = context\n        val activity = mainActivity\n        if (null == context || null == activity) {\n            return\n        }\n        val titles = ArrayList<CharSequence>()\n        gd.newerVersions.forEach {\n            titles.add(getString(R.string.newer_version_title, it.title, it.posted))\n        }\n        AlertDialog.Builder(requireContext())\n            .setItems(titles.toTypedArray()) { _: DialogInterface?, which: Int ->\n                val newerVersion = gd.newerVersions[which]\n                if (EhDownloadManager.containDownloadInfo(newerVersion.gid)) {\n                    showTip(R.string.download_upgrade_existed, LENGTH_SHORT)\n                } else {\n                    val dialog = AlertDialog.Builder(context)\n                        .setTitle(null)\n                        .setView(R.layout.preference_dialog_task)\n                        .setCancelable(false)\n                        .show()\n                    lifecycleScope.launchIO {\n                        var success = false\n                        val url = EhUrl.getGalleryDetailUrl(\n                            newerVersion.gid,\n                            newerVersion.token,\n                            0,\n                            false,\n                            GET_FULL_HASH,\n                        )\n                        val request = EhRequestBuilder(url, EhUrl.referer).build()\n                        runCatching {\n                            okHttpClient.newCall(request).executeAsync().use { response ->\n                                val body = response.body.string()\n                                val result = GalleryDetailParser.parse(body)\n                                val spiderInfo = SpiderInfo(\n                                    result.gid,\n                                    result.token,\n                                    result.pages,\n                                    upgradeFrom = gd.gid,\n                                )\n                                SpiderQueen.readPreviews(body, 0, spiderInfo)\n                                val dirName = FileUtils.sanitizeFilename(\"${result.gid}-${EhUtils.getSuitableTitle(result)}\")\n                                SpiderDen.perSafeDownloadDir(result.gid, dirName)!!.run {\n                                    createFile(SPIDER_INFO_FILENAME)!!.also {\n                                        spiderInfo.saveToUniFile(it)\n                                    }\n                                }\n                                // Start download\n                                val label = EhDownloadManager.getDownloadInfo(gd.gid)?.label\n                                val intent = Intent(activity, DownloadService::class.java)\n                                intent.action = DownloadService.ACTION_START\n                                intent.putExtra(DownloadService.KEY_LABEL, label)\n                                intent.putExtra(DownloadService.KEY_GALLERY_INFO, result)\n                                runCatching {\n                                    ContextCompat.startForegroundService(activity, intent)\n                                    success = true\n                                }.onFailure {\n                                    if (isAtLeastS && it is ForegroundServiceStartNotAllowedException) {\n                                        // App not in a valid state to start foreground service\n                                        withUIContext {\n                                            dialog.dismiss()\n                                            AlertDialog.Builder(context)\n                                                .setMessage(R.string.download_upgrade_service_failed)\n                                                .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->\n                                                    ContextCompat.startForegroundService(activity, intent)\n                                                    success = true\n                                                }\n                                                .show()\n                                        }\n                                    } else {\n                                        it.printStackTrace()\n                                    }\n                                }\n                            }\n                        }.onFailure {\n                            it.printStackTrace()\n                        }\n                        withUIContext {\n                            dialog.dismiss()\n                            if (success) {\n                                showTip(R.string.added_to_download_list, LENGTH_SHORT)\n                            } else {\n                                showTip(R.string.download_state_failed, LENGTH_SHORT)\n                                launchIO {\n                                    SpiderDen.getGalleryDownloadDir(newerVersion.gid)?.takeIf { it.exists() }?.delete()\n                                    EhDownloadManager.removeDownloadDirname(newerVersion.gid)\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            .show()\n    }\n\n    private fun showFilterUploaderDialog() {\n        val context = context\n        val uploader = uploader\n        if (context == null || uploader == null) {\n            return\n        }\n        AlertDialog.Builder(context)\n            .setMessage(getString(R.string.filter_the_uploader, uploader))\n            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->\n                val filter = Filter()\n                filter.mode = EhFilter.MODE_UPLOADER\n                filter.text = uploader\n                EhFilter.addFilter(filter)\n                showTip(R.string.filter_added, LENGTH_SHORT)\n            }\n            .setNegativeButton(android.R.string.cancel, null)\n            .show()\n    }\n\n    private fun showFilterTagDialog(tag: String) {\n        val context = context ?: return\n        AlertDialog.Builder(context)\n            .setMessage(getString(R.string.filter_the_tag, tag))\n            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->\n                val filter = Filter()\n                filter.mode = EhFilter.MODE_TAG\n                filter.text = tag\n                EhFilter.addFilter(filter)\n                showTip(R.string.filter_added, LENGTH_SHORT)\n            }\n            .setNegativeButton(android.R.string.cancel, null)\n            .show()\n    }\n\n    private fun showTagDialog(tv: TextView, tag: String) {\n        val context = context ?: return\n        val temp: String\n        val index = tag.indexOf(':')\n        temp = if (index >= 0) {\n            tag.substring(index + 1)\n        } else {\n            tag\n        }\n        val menu: MutableList<String> = ArrayList()\n        val menuId = IntList()\n        val resources = context.resources\n        menu.add(resources.getString(android.R.string.copy))\n        menuId.add(R.id.copy)\n        if (temp != tv.text.toString()) {\n            menu.add(resources.getString(R.string.copy_trans))\n            menuId.add(R.id.copy_trans)\n        }\n        menu.add(resources.getString(R.string.show_definition))\n        menuId.add(R.id.show_definition)\n        menu.add(resources.getString(R.string.add_filter))\n        menuId.add(R.id.add_filter)\n        if (mGalleryDetail != null && mGalleryDetail!!.apiUid >= 0) {\n            val isUp = tv.text.endsWith(TAG_STATUS_UP)\n            val isDn = tv.text.endsWith(TAG_STATUS_DN)\n            if (!isUp) {\n                menu.add(resources.getString(if (isDn) R.string.tag_vote_down_cancel else R.string.tag_vote_up))\n                menuId.add(R.id.vote_up)\n            }\n            if (!isDn) {\n                menu.add(resources.getString(if (isUp) R.string.tag_vote_up_cancel else R.string.tag_vote_down))\n                menuId.add(R.id.vote_down)\n            }\n        }\n        AlertDialog.Builder(context)\n            .setTitle(tag)\n            .setItems(menu.toTypedArray()) { _: DialogInterface?, which: Int ->\n                if (which < 0 || which >= menuId.size) {\n                    return@setItems\n                }\n                when (menuId[which]) {\n                    R.id.vote_up -> {\n                        voteTag(tag, 1)\n                    }\n                    R.id.vote_down -> {\n                        voteTag(tag, -1)\n                    }\n                    R.id.show_definition -> {\n                        UrlOpener.openUrl(context, EhUrl.getTagDefinitionUrl(temp), false)\n                    }\n                    R.id.add_filter -> {\n                        showFilterTagDialog(tag)\n                    }\n                    R.id.copy -> {\n                        requireActivity().addTextToClipboard(tag, false)\n                    }\n                    R.id.copy_trans -> {\n                        var transText = tv.text.toString().trim()\n                        if (transText.endsWith(TAG_STATUS_UP) || transText.endsWith(TAG_STATUS_DN)) {\n                            transText = transText.substring(0, transText.length - 1)\n                        }\n                        requireActivity().addTextToClipboard(transText, false)\n                    }\n                }\n            }.show()\n    }\n\n    private fun voteTag(tag: String, vote: Int) {\n        val context = context\n        val activity = mainActivity\n        if (null == context || null == activity) {\n            return\n        }\n        val request = EhRequest()\n            .setMethod(EhClient.METHOD_VOTE_TAG)\n            .setArgs(\n                mGalleryDetail!!.apiUid,\n                mGalleryDetail!!.apiKey!!,\n                mGalleryDetail!!.gid,\n                mGalleryDetail!!.token!!,\n                tag,\n                vote,\n            )\n            .setCallback(VoteTagListener(context))\n        request.enqueue(this)\n    }\n\n    override fun onLongClick(v: View): Boolean {\n        val activity = mainActivity ?: return false\n        if (mUploader === v) {\n            if (uploader.isNullOrEmpty() || disowned) {\n                return false\n            }\n            showFilterUploaderDialog()\n        } else if (mDownload === v) {\n            val galleryInfo = galleryInfo\n            if (galleryInfo != null) {\n                // CommonOperations Actions\n                CommonOperations.startDownload(activity, galleryInfo, true)\n            }\n            return true\n        } else if (mHeartGroup == v) {\n            // DB Actions\n            // CommonOperations Actions\n            if (mGalleryDetail != null && !mModifyingFavorites) {\n                mModifyingFavorites = true\n                if (EhDB.containLocalFavorites(mGalleryDetail!!.gid)) {\n                    CommonOperations.removeFromFavorites(\n                        activity,\n                        mGalleryDetail!!,\n                        ModifyFavoritesListener(activity, true),\n                        true,\n                    )\n                } else {\n                    CommonOperations.addToFavorites(\n                        activity,\n                        mGalleryDetail!!,\n                        ModifyFavoritesListener(activity, false),\n                        true,\n                    )\n                }\n                // Update UI\n                updateFavoriteDrawable()\n            }\n        } else {\n            val tag = v.getTag(R.id.tag) as? String\n            if (null != tag) {\n                showTagDialog(v as TextView, tag)\n                return true\n            }\n        }\n        return false\n    }\n\n    override fun onBackPressed() {\n        if (mViewTransition != null &&\n            mThumb != null &&\n            mViewTransition!!.shownViewIndex == 0 &&\n            mThumb!!.isShown\n        ) {\n            val location = IntArray(2)\n            mThumb!!.getLocationInWindow(location)\n            // Only show transaction when thumb can be seen\n            if (location[1] + mThumb!!.height > 0) {\n                setTransitionName()\n                finish(ExitTransaction(mThumb!!))\n                return\n            }\n        }\n        finish()\n    }\n\n    override fun onSceneResult(requestCode: Int, resultCode: Int, data: Bundle?) {\n        if (requestCode == REQUEST_CODE_COMMENT_GALLERY) {\n            if (resultCode != RESULT_OK || data == null) {\n                return\n            }\n            val comments = data.getParcelableCompat<GalleryCommentList>(GalleryCommentsScene.KEY_COMMENT_LIST)\n            if (mGalleryDetail == null && comments == null) {\n                return\n            }\n            mGalleryDetail!!.comments = comments\n            bindComments(comments!!.comments)\n        } else {\n            super.onSceneResult(requestCode, resultCode, data)\n        }\n    }\n\n    private fun updateDownloadText() {\n        mDownload?.run {\n            when (mDownloadState) {\n                DownloadInfo.STATE_INVALID -> setText(R.string.download)\n                DownloadInfo.STATE_NONE -> setText(R.string.download_state_none)\n                DownloadInfo.STATE_WAIT -> setText(R.string.download_state_wait)\n                DownloadInfo.STATE_DOWNLOAD -> setText(R.string.download_state_downloading)\n                DownloadInfo.STATE_FINISH -> setText(\n                    if (mGalleryDetail != null && mGalleryDetail!!.newerVersions.isNotEmpty()) {\n                        R.string.download_upgradeable\n                    } else {\n                        R.string.download_state_downloaded\n                    },\n                )\n                DownloadInfo.STATE_FAILED -> setText(R.string.download_state_failed)\n            }\n        }\n    }\n\n    private fun updateDownloadState() {\n        val context = context\n        val gid = gid\n        if (null == context || -1L == gid) {\n            return\n        }\n        val downloadState = EhDownloadManager.getDownloadState(gid)\n        if (downloadState == mDownloadState) {\n            return\n        }\n        mDownloadState = downloadState\n        updateDownloadText()\n    }\n\n    override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n        updateDownloadState()\n    }\n\n    override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {\n        updateDownloadState()\n    }\n\n    override fun onUpdateAll() {\n        updateDownloadState()\n    }\n\n    override fun onReload() {\n        updateDownloadState()\n    }\n\n    override fun onChange() {\n        updateDownloadState()\n    }\n\n    override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n        updateDownloadState()\n    }\n\n    override fun onRenameLabel(from: String, to: String) {}\n\n    override fun onUpdateLabels() {}\n\n    private fun onGetGalleryDetailSuccess(result: GalleryDetail) {\n        mGalleryDetail = result\n        updateDownloadState()\n        adjustViewVisibility(STATE_NORMAL, true)\n        bindViewSecond()\n    }\n\n    private fun onGetGalleryDetailFailure(e: Exception) {\n        e.printStackTrace()\n        if (null != mTip) {\n            val error = ExceptionUtils.getReadableString(e)\n            mTip!!.text = error\n            adjustViewVisibility(STATE_FAILED, true)\n        }\n    }\n\n    private fun onRateGallerySuccess(result: RateGalleryParser.Result) {\n        if (mGalleryDetail != null) {\n            mGalleryDetail!!.rating = result.rating\n            mGalleryDetail!!.ratingCount = result.ratingCount\n        }\n        // Update UI\n        if (mRatingText != null && mRating != null) {\n            mRatingText!!.text = getAllRatingText(result.rating, result.ratingCount)\n            mRating!!.rating = result.rating\n        }\n    }\n\n    private fun onModifyFavoritesSuccess(addOrRemove: Boolean) {\n        mModifyingFavorites = false\n        if (mGalleryDetail != null) {\n            mGalleryDetail!!.isFavorited = !addOrRemove && mGalleryDetail!!.favoriteName != null\n            updateFavoriteDrawable()\n        }\n    }\n\n    private fun onModifyFavoritesFailure() {\n        mModifyingFavorites = false\n    }\n\n    private fun onModifyFavoritesCancel() {\n        mModifyingFavorites = false\n    }\n\n    override fun onProvideAssistContent(outContent: AssistContent) {\n        super.onProvideAssistContent(outContent)\n        val url = galleryDetailUrl\n        if (url != null) {\n            outContent.webUri = url.toUri()\n        }\n    }\n\n    @IntDef(STATE_INIT, STATE_NORMAL, STATE_REFRESH, STATE_REFRESH_HEADER, STATE_FAILED)\n    @Retention(AnnotationRetention.SOURCE)\n    private annotation class State\n\n    private class ExitTransaction(\n        private val mThumb: View,\n    ) : TransitionHelper {\n        override fun onTransition(\n            context: Context,\n            transaction: FragmentTransaction,\n            exit: Fragment,\n            enter: Fragment,\n        ): Boolean {\n            if (enter !is GalleryListScene &&\n                enter !is DownloadsScene &&\n                enter !is FavoritesScene &&\n                enter !is HistoryScene\n            ) {\n                return false\n            }\n            ViewCompat.getTransitionName(mThumb)?.let {\n                exit.sharedElementReturnTransition =\n                    TransitionInflater.from(context).inflateTransition(R.transition.trans_move)\n                exit.exitTransition =\n                    TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)\n                enter.sharedElementEnterTransition =\n                    TransitionInflater.from(context).inflateTransition(R.transition.trans_move)\n                enter.enterTransition =\n                    TransitionInflater.from(context).inflateTransition(R.transition.trans_fade)\n                transaction.addSharedElement(mThumb, it)\n            }\n            return true\n        }\n    }\n\n    private inner class VoteTagListener(context: Context) : EhCallback<GalleryDetailScene?, Pair<String, Array<GalleryTagGroup>?>>(context) {\n        override fun onSuccess(result: Pair<String, Array<GalleryTagGroup>?>) {\n            if (result.first.isNotEmpty()) {\n                showTip(result.first, LENGTH_SHORT)\n            } else {\n                mGalleryDetail?.tags = result.second\n                bindTags(result.second)\n                showTip(R.string.tag_vote_successfully, LENGTH_SHORT)\n            }\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(R.string.vote_failed, LENGTH_LONG)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private class DownloadArchiveListener(\n        context: Context,\n        private val info: GalleryInfo,\n    ) : EhCallback<GalleryDetailScene?, String?>(context) {\n        override fun onSuccess(result: String?) {\n            result?.let {\n                val uri = it.toUri()\n                val intent = Intent().apply {\n                    action = Intent.ACTION_VIEW\n                    setDataAndType(uri, \"application/zip\")\n                }\n                val name = \"${info.gid}-${EhUtils.getSuitableTitle(info)}.zip\"\n                try {\n                    try {\n                        application.topActivity!!.startActivity(intent)\n                        application.topActivity!!.addTextToClipboard(name, false)\n                    } catch (_: ActivityNotFoundException) {\n                        val r = DownloadManager.Request(uri)\n                        r.setDestinationInExternalPublicDir(\n                            Environment.DIRECTORY_DOWNLOADS,\n                            FileUtils.sanitizeFilename(name),\n                        )\n                        r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)\n                        application.getSystemService<DownloadManager>()!!.enqueue(r)\n                    }\n                } catch (e: Throwable) {\n                    e.printStackTrace()\n                    ExceptionUtils.throwIfFatal(e)\n                }\n            }\n            showTip(R.string.download_archive_started, LENGTH_SHORT)\n        }\n\n        override fun onFailure(e: Exception) {\n            if (e is EhException) {\n                showTip(ExceptionUtils.getReadableString(e), LENGTH_LONG)\n            } else {\n                showTip(R.string.download_archive_failure, LENGTH_LONG)\n                e.printStackTrace()\n            }\n        }\n\n        override fun onCancel() {}\n    }\n\n    private inner class DeleteDialogHelper(\n        private val mGalleryInfo: GalleryInfo,\n        private val mBuilder: CheckBoxDialogBuilder,\n    ) : DialogInterface.OnClickListener {\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            if (which != DialogInterface.BUTTON_POSITIVE) {\n                return\n            }\n            // Delete\n            // DownloadManager Actions\n            EhDownloadManager.deleteDownload(mGalleryInfo.gid)\n            // Delete image files\n            val checked = mBuilder.isChecked\n            Settings.putRemoveImageFiles(checked)\n            if (checked) {\n                val file = SpiderDen.getGalleryDownloadDir(mGalleryInfo.gid)\n                // DB Actions\n                EhDownloadManager.removeDownloadDirname(mGalleryInfo.gid)\n                // Other Actions\n                lifecycleScope.launchIO {\n                    runCatching {\n                        file?.delete()\n                    }\n                }\n            }\n        }\n    }\n\n    private inner class GetGalleryDetailListener(context: Context) : EhCallback<GalleryDetailScene?, GalleryDetail>(context) {\n        override fun onSuccess(result: GalleryDetail) {\n            application.removeGlobalStuff(this)\n            // Put gallery detail to cache\n            galleryDetailCache.put(result.gid, result)\n            // Add history\n            // DB Actions\n            EhDB.putHistoryInfo(result)\n            // Notify success\n            val scene = this@GalleryDetailScene\n            scene.onGetGalleryDetailSuccess(result)\n        }\n\n        override fun onFailure(e: Exception) {\n            application.removeGlobalStuff(this)\n            val scene = this@GalleryDetailScene\n            scene.onGetGalleryDetailFailure(e)\n        }\n\n        override fun onCancel() {\n            application.removeGlobalStuff(this)\n        }\n    }\n\n    private inner class RateGalleryListener(\n        context: Context,\n    ) : EhCallback<GalleryDetailScene?, RateGalleryParser.Result>(context) {\n        override fun onSuccess(result: RateGalleryParser.Result) {\n            showTip(R.string.rate_successfully, LENGTH_SHORT)\n            val scene = this@GalleryDetailScene\n            scene.onRateGallerySuccess(result)\n        }\n\n        override fun onFailure(e: Exception) {\n            e.printStackTrace()\n            showTip(R.string.rate_failed, LENGTH_LONG)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private inner class ModifyFavoritesListener(\n        context: Context,\n        private val mAddOrRemove: Boolean,\n    ) : EhCallback<GalleryDetailScene?, Unit>(context) {\n        override fun onSuccess(result: Unit) {\n            showTip(\n                if (mAddOrRemove) R.string.remove_from_favorite_success else R.string.add_to_favorite_success,\n                LENGTH_SHORT,\n            )\n            val scene = this@GalleryDetailScene\n            scene.onModifyFavoritesSuccess(mAddOrRemove)\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(\n                if (mAddOrRemove) R.string.remove_from_favorite_failure else R.string.add_to_favorite_failure,\n                LENGTH_LONG,\n            )\n            val scene = this@GalleryDetailScene\n            scene.onModifyFavoritesFailure()\n        }\n\n        override fun onCancel() {\n            val scene = this@GalleryDetailScene\n            scene.onModifyFavoritesCancel()\n        }\n    }\n\n    private inner class ArchiveListDialogHelper :\n        AdapterView.OnItemClickListener,\n        DialogInterface.OnDismissListener,\n        EhClient.Callback<ArchiveParser.Result> {\n        private var mProgressView: CircularProgressIndicator? = null\n        private var mErrorText: TextView? = null\n        private var mListView: ListView? = null\n        private var mRequest: EhRequest? = null\n        private var mDialog: Dialog? = null\n        fun setDialog(dialog: Dialog?, url: String?) {\n            mDialog = dialog\n            mProgressView = ViewUtils.`$$`(dialog, R.id.progress) as CircularProgressIndicator\n            mErrorText = ViewUtils.`$$`(dialog, R.id.text) as TextView\n            mListView = ViewUtils.`$$`(dialog, R.id.list_view) as ListView\n            mListView!!.onItemClickListener = this\n            val context = context\n            if (context != null) {\n                if (mArchiveList == null) {\n                    mErrorText!!.visibility = View.GONE\n                    mListView!!.visibility = View.GONE\n                    mRequest = EhRequest().setMethod(EhClient.METHOD_ARCHIVE_LIST)\n                        .setArgs(url!!, mGid, mToken)\n                        .setCallback(this)\n                    mRequest!!.enqueue(this@GalleryDetailScene)\n                } else {\n                    bind(mArchiveList, mCurrentFunds)\n                }\n            }\n        }\n\n        private fun bind(data: List<ArchiveParser.Archive>?, funds: HomeParser.Funds?) {\n            if (null == mDialog || null == mProgressView || null == mErrorText || null == mListView) {\n                return\n            }\n            if (data.isNullOrEmpty()) {\n                mProgressView!!.visibility = View.GONE\n                mErrorText!!.visibility = View.VISIBLE\n                mListView!!.visibility = View.GONE\n                mErrorText!!.setText(R.string.no_archives)\n            } else {\n                val nameArray = data.map {\n                    it.run {\n                        if (isHAtH) {\n                            val costStr =\n                                if (cost == \"Free\") resources.getString(R.string.archive_free) else cost\n                            \"[H@H] $name [$size] [$costStr]\"\n                        } else {\n                            val nameStr =\n                                resources.getString(if (res == \"org\") R.string.archive_original else R.string.archive_resample)\n                            val costStr =\n                                if (cost == \"Free!\") resources.getString(R.string.archive_free) else cost\n                            \"$nameStr [$size] [$costStr]\"\n                        }\n                    }\n                }.toTypedArray()\n                mProgressView!!.visibility = View.GONE\n                mErrorText!!.visibility = View.GONE\n                mListView!!.visibility = View.VISIBLE\n                mListView!!.adapter =\n                    ArrayAdapter(mDialog!!.context, R.layout.item_select_dialog, nameArray)\n                if (funds != null) {\n                    var fundsGP = funds.fundsGP.toString()\n                    // Ex GP numbers are rounded down to the nearest thousand\n                    if (EhUtils.isExHentai) {\n                        fundsGP += \"+\"\n                    }\n                    mDialog!!.setTitle(getString(R.string.current_funds, fundsGP, funds.fundsC))\n                }\n            }\n        }\n\n        override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {\n            val context = context\n            val activity = mainActivity\n            if (null != context && null != activity && null != mArchiveList && position < mArchiveList!!.size) {\n                val res = mArchiveList!![position].res\n                val isHAtH = mArchiveList!![position].isHAtH\n                val request = EhRequest()\n                request.setMethod(EhClient.METHOD_DOWNLOAD_ARCHIVE)\n                request.setArgs(\n                    mGalleryDetail!!.gid,\n                    mGalleryDetail!!.token!!,\n                    res,\n                    isHAtH,\n                )\n                request.setCallback(DownloadArchiveListener(context, mGalleryDetail!!))\n                request.enqueue(this@GalleryDetailScene)\n            }\n            if (mDialog != null) {\n                mDialog!!.dismiss()\n                mDialog = null\n            }\n        }\n\n        override fun onDismiss(dialog: DialogInterface) {\n            if (mRequest != null) {\n                mRequest!!.cancel()\n                mRequest = null\n            }\n            mDialog = null\n            mProgressView = null\n            mErrorText = null\n            mListView = null\n        }\n\n        override fun onSuccess(result: ArchiveParser.Result) {\n            if (mRequest != null) {\n                mRequest = null\n                mArchiveList = result.archiveList\n                mCurrentFunds = result.funds\n                bind(result.archiveList, result.funds)\n            }\n        }\n\n        override fun onFailure(e: Exception) {\n            mRequest = null\n            val context = context\n            if (null != context && null != mProgressView && null != mErrorText && null != mListView) {\n                mProgressView!!.visibility = View.GONE\n                mErrorText!!.visibility = View.VISIBLE\n                mListView!!.visibility = View.GONE\n                mErrorText!!.text = ExceptionUtils.getReadableString(e)\n            }\n        }\n\n        override fun onCancel() {\n            mRequest = null\n        }\n    }\n\n    private inner class TorrentListDialogHelper :\n        AdapterView.OnItemClickListener,\n        DialogInterface.OnDismissListener,\n        EhClient.Callback<List<TorrentParser.Result>> {\n        private var mProgressView: CircularProgressIndicator? = null\n        private var mErrorText: TextView? = null\n        private var mListView: ListView? = null\n        private var mRequest: EhRequest? = null\n        private var mDialog: Dialog? = null\n        fun setDialog(dialog: Dialog?, url: String?) {\n            mDialog = dialog\n            mProgressView = ViewUtils.`$$`(dialog, R.id.progress) as CircularProgressIndicator\n            mErrorText = ViewUtils.`$$`(dialog, R.id.text) as TextView\n            mListView = ViewUtils.`$$`(dialog, R.id.list_view) as ListView\n            mListView!!.onItemClickListener = this\n            val context = context\n            if (context != null) {\n                if (mTorrentList == null) {\n                    mErrorText!!.visibility = View.GONE\n                    mListView!!.visibility = View.GONE\n                    mRequest = EhRequest().setMethod(EhClient.METHOD_GET_TORRENT_LIST)\n                        .setArgs(url!!, mGid, mToken)\n                        .setCallback(this)\n                    mRequest!!.enqueue(this@GalleryDetailScene)\n                } else {\n                    bind(mTorrentList)\n                }\n            }\n        }\n\n        private fun bind(data: List<TorrentParser.Result>?) {\n            if (null == mDialog || null == mProgressView || null == mErrorText || null == mListView) {\n                return\n            }\n            if (data.isNullOrEmpty()) {\n                mProgressView!!.visibility = View.GONE\n                mErrorText!!.visibility = View.VISIBLE\n                mListView!!.visibility = View.GONE\n                mErrorText!!.setText(R.string.no_torrents)\n            } else {\n                val nameArray = data.map { it.format() }.toTypedArray()\n                mProgressView!!.visibility = View.GONE\n                mErrorText!!.visibility = View.GONE\n                mListView!!.visibility = View.VISIBLE\n                mListView!!.adapter =\n                    ArrayAdapter(mDialog!!.context, R.layout.item_select_dialog, nameArray)\n            }\n        }\n\n        override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {\n            val context = context\n            if (null != context && null != mTorrentList && position < mTorrentList!!.size) {\n                val url = mTorrentList!![position].url\n                val name = mTorrentList!![position].name\n                // TODO: Don't use buggy system download service\n                val r =\n                    DownloadManager.Request(url.replace(\"exhentai.org/torrent\", \"ehtracker.org/get\").toUri())\n                r.setDestinationInExternalPublicDir(\n                    Environment.DIRECTORY_DOWNLOADS,\n                    FileUtils.sanitizeFilename(\"$name.torrent\"),\n                )\n                r.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)\n                r.addRequestHeader(\"Cookie\", EhCookieStore.getCookieHeader(url.toHttpUrl()))\n                val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager\n                try {\n                    dm.enqueue(r)\n                    showTip(R.string.download_torrent_started, LENGTH_SHORT)\n                } catch (e: Throwable) {\n                    e.printStackTrace()\n                    ExceptionUtils.throwIfFatal(e)\n                    showTip(R.string.download_torrent_failure, LENGTH_SHORT)\n                }\n            }\n            if (mDialog != null) {\n                mDialog!!.dismiss()\n                mDialog = null\n            }\n        }\n\n        override fun onDismiss(dialog: DialogInterface) {\n            if (mRequest != null) {\n                mRequest!!.cancel()\n                mRequest = null\n            }\n            mDialog = null\n            mProgressView = null\n            mErrorText = null\n            mListView = null\n        }\n\n        override fun onSuccess(result: List<TorrentParser.Result>) {\n            if (mRequest != null) {\n                mRequest = null\n                mTorrentList = result\n                bind(result)\n            }\n        }\n\n        override fun onFailure(e: Exception) {\n            mRequest = null\n            val context = context\n            if (null != context && null != mProgressView && null != mErrorText && null != mListView) {\n                mProgressView!!.visibility = View.GONE\n                mErrorText!!.visibility = View.VISIBLE\n                mListView!!.visibility = View.GONE\n                mErrorText!!.text = ExceptionUtils.getReadableString(e)\n            }\n        }\n\n        override fun onCancel() {\n            mRequest = null\n        }\n    }\n\n    private inner class RateDialogHelper :\n        OnUserRateListener,\n        DialogInterface.OnClickListener {\n        private var mRatingBar: GalleryRatingBar? = null\n        private var mRatingText: TextView? = null\n        fun setDialog(dialog: Dialog?, rating: Float) {\n            mRatingText = ViewUtils.`$$`(dialog, R.id.rating_text) as TextView\n            mRatingBar = ViewUtils.`$$`(dialog, R.id.rating_view) as GalleryRatingBar\n            mRatingText!!.setText(getRatingText(rating))\n            mRatingBar!!.rating = rating\n            mRatingBar!!.setOnUserRateListener(this)\n        }\n\n        override fun onUserRate(rating: Float) {\n            if (null != mRatingText) {\n                mRatingText!!.setText(getRatingText(rating))\n            }\n        }\n\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            val context = context\n            val activity = mainActivity\n            if (null == context || null == activity || which != DialogInterface.BUTTON_POSITIVE || null == mGalleryDetail || null == mRatingBar) {\n                return\n            }\n            val request = EhRequest()\n                .setMethod(EhClient.METHOD_GET_RATE_GALLERY)\n                .setArgs(\n                    mGalleryDetail!!.apiUid,\n                    mGalleryDetail!!.apiKey!!,\n                    mGalleryDetail!!.gid,\n                    mGalleryDetail!!.token!!,\n                    mRatingBar!!.rating,\n                )\n                .setCallback(\n                    RateGalleryListener(context),\n                )\n            request.enqueue(this@GalleryDetailScene)\n        }\n    }\n\n    companion object {\n        const val KEY_ACTION = \"action\"\n        const val ACTION_GALLERY_INFO = \"action_gallery_info\"\n        const val ACTION_GID_TOKEN = \"action_gid_token\"\n        const val KEY_GALLERY_INFO = \"gallery_info\"\n        const val KEY_GID = \"gid\"\n        const val KEY_TOKEN = \"token\"\n        const val KEY_PAGE = \"page\"\n        private const val REQUEST_CODE_COMMENT_GALLERY = 0\n        private const val STATE_INIT = -1\n        private const val STATE_NORMAL = 0\n        private const val STATE_REFRESH = 1\n        private const val STATE_REFRESH_HEADER = 2\n        private const val STATE_FAILED = 3\n        private const val TAG_STATUS_UP = \"↑\"\n        private const val TAG_STATUS_DN = \"↓\"\n        private const val KEY_GALLERY_DETAIL = \"gallery_detail\"\n        private const val KEY_REQUEST_ID = \"request_id\"\n        private const val TRANSITION_ANIMATION_DISABLED = true\n        private fun getArtist(tagGroups: Array<GalleryTagGroup>?): String? {\n            if (null == tagGroups) {\n                return null\n            }\n            for (tagGroup in tagGroups) {\n                if (\"artist\" == tagGroup.groupName && tagGroup.isNotEmpty()) {\n                    var tagStr = tagGroup[0]\n                    while (tagStr.startsWith(\"_\")) {\n                        tagStr = tagStr.substring(2)\n                    }\n                    return tagStr\n                }\n            }\n            return null\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryHolder.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.card.MaterialCardView\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.widget.SimpleRatingView\nimport com.hippo.widget.LoadImageView\n\ninternal class GalleryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n    val thumb: LoadImageView = itemView.findViewById(R.id.thumb)\n    val title: TextView = itemView.findViewById(R.id.title)\n    val uploader: TextView? = itemView.findViewById(R.id.uploader)\n    val note: TextView? = itemView.findViewById(R.id.note)\n    val rating: SimpleRatingView = itemView.findViewById(R.id.rating)\n    val category: TextView = itemView.findViewById(R.id.category)\n    val posted: TextView? = itemView.findViewById(R.id.posted)\n    val pages: TextView = itemView.findViewById(R.id.pages)\n    val simpleLanguage: TextView = itemView.findViewById(R.id.simple_language)\n    val favourited: ImageView? = itemView.findViewById(R.id.favourited)\n    val downloaded: ImageView? = itemView.findViewById(R.id.downloaded)\n    val card: MaterialCardView = itemView.findViewById(R.id.card)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryInfoScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.LinearDividerItemDecoration\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.UrlOpener\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.client.thumbUrl\nimport com.hippo.util.addTextToClipboard\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.ViewUtils\nimport rikka.core.res.resolveColor\n\nclass GalleryInfoScene : ToolbarScene() {\n    private var mKeys: ArrayList<String> = arrayListOf()\n    private var mValues: ArrayList<String?> = arrayListOf()\n    private var mRecyclerView: RecyclerView? = null\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    private fun handlerArgs(args: Bundle?) {\n        args?.getParcelableCompat<GalleryDetail>(KEY_GALLERY_DETAIL)?.let {\n            mKeys.add(getString(R.string.header_key))\n            mValues.add(getString(R.string.header_value))\n            mKeys.add(getString(R.string.key_gid))\n            mValues.add(it.gid.toString())\n            mKeys.add(getString(R.string.key_token))\n            mValues.add(it.token)\n            mKeys.add(getString(R.string.key_url))\n            mValues.add(EhUrl.getGalleryDetailUrl(it.gid, it.token))\n            mKeys.add(getString(R.string.key_title))\n            mValues.add(it.title)\n            mKeys.add(getString(R.string.key_title_jpn))\n            mValues.add(it.titleJpn)\n            mKeys.add(getString(R.string.key_thumb))\n            mValues.add(it.thumbUrl!!)\n            mKeys.add(getString(R.string.key_category))\n            mValues.add(EhUtils.getCategory(it.category))\n            mKeys.add(getString(R.string.key_uploader))\n            mValues.add(it.uploader)\n            mKeys.add(getString(R.string.key_posted))\n            mValues.add(it.posted)\n            mKeys.add(getString(R.string.key_parent))\n            mValues.add(it.parent)\n            mKeys.add(getString(R.string.key_visible))\n            mValues.add(it.visible)\n            mKeys.add(getString(R.string.key_language))\n            mValues.add(it.language)\n            mKeys.add(getString(R.string.key_pages))\n            mValues.add(it.pages.toString())\n            mKeys.add(getString(R.string.key_size))\n            mValues.add(it.size)\n            mKeys.add(getString(R.string.key_favorite_count))\n            mValues.add(it.favoriteCount.toString())\n            mKeys.add(getString(R.string.key_favorited))\n            mValues.add(java.lang.Boolean.toString(it.isFavorited))\n            mKeys.add(getString(R.string.key_favorite_name))\n            mValues.add(it.favoriteName)\n            mKeys.add(getString(R.string.key_rating_count))\n            mValues.add(it.ratingCount.toString())\n            mKeys.add(getString(R.string.key_rating))\n            mValues.add(it.rating.toString())\n            mKeys.add(getString(R.string.key_torrents))\n            mValues.add(it.torrentCount.toString())\n            mKeys.add(getString(R.string.key_torrent_url))\n            mValues.add(it.torrentUrl)\n        }\n    }\n\n    private fun onInit() {\n        handlerArgs(arguments)\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mKeys = savedInstanceState.getStringArrayList(KEY_KEYS) as ArrayList<String>\n        mValues = savedInstanceState.getStringArrayList(KEY_VALUES) as ArrayList<String?>\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        outState.putStringArrayList(KEY_KEYS, mKeys)\n        outState.putStringArrayList(KEY_VALUES, mValues)\n    }\n\n    override fun onCreateViewWithToolbar(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_gallery_info, container, false)\n        val context = requireContext()\n        mRecyclerView = ViewUtils.`$$`(view, R.id.recycler_view) as EasyRecyclerView\n        val adapter = InfoAdapter()\n        mRecyclerView!!.adapter = adapter\n        mRecyclerView!!.layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(context, 1f),\n        )\n        val keylineMargin = context.resources.getDimensionPixelOffset(R.dimen.keyline_margin)\n        decoration.setPadding(keylineMargin)\n        mRecyclerView!!.addItemDecoration(decoration)\n        mRecyclerView!!.clipToPadding = false\n        mRecyclerView!!.setHasFixedSize(true)\n        return view\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        setTitle(R.string.gallery_info)\n        setNavigationIcon(R.drawable.v_arrow_left_dark_x24)\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        if (null != mRecyclerView) {\n            mRecyclerView!!.stopScroll()\n            mRecyclerView = null\n        }\n    }\n\n    fun onItemClick(position: Int): Boolean {\n        val context = context\n        return if (null != context && 0 != position) {\n            if (position == INDEX_PARENT) {\n                UrlOpener.openUrl(context, mValues[position], true)\n            } else {\n                requireActivity().addTextToClipboard(mValues[position], false)\n                if (position == INDEX_URL) {\n                    // Save it to avoid detect the gallery\n                    Settings.putClipboardTextHashCode(mValues[position].hashCode())\n                }\n            }\n            true\n        } else {\n            false\n        }\n    }\n\n    override fun onNavigationClick() {\n        onBackPressed()\n    }\n\n    private class InfoHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        val key: TextView = itemView.findViewById(R.id.key)\n        val value: TextView = itemView.findViewById(R.id.value)\n    }\n\n    private inner class InfoAdapter : RecyclerView.Adapter<InfoHolder>() {\n        private val mInflater: LayoutInflater = layoutInflater\n\n        override fun getItemViewType(position: Int): Int = if (position == 0) {\n            TYPE_HEADER\n        } else {\n            TYPE_DATA\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfoHolder = InfoHolder(\n            mInflater.inflate(\n                if (viewType == TYPE_HEADER) R.layout.item_gallery_info_header else R.layout.item_gallery_info_data,\n                parent,\n                false,\n            ),\n        )\n\n        override fun onBindViewHolder(holder: InfoHolder, position: Int) {\n            holder.key.text = mKeys[position]\n            holder.value.text = mValues[position]\n            holder.itemView.isEnabled = position != 0\n            holder.itemView.setOnClickListener { onItemClick(position) }\n        }\n\n        override fun getItemCount(): Int = mKeys.size.coerceAtMost(mValues.size)\n    }\n\n    companion object {\n        const val KEY_GALLERY_DETAIL = \"gallery_detail\"\n        const val KEY_KEYS = \"keys\"\n        const val KEY_VALUES = \"values\"\n        private const val INDEX_URL = 3\n        private const val INDEX_PARENT = 10\n        private const val TYPE_HEADER = 0\n        private const val TYPE_DATA = 1\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryListScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.animation.Animator\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.content.res.Resources\nimport android.net.Uri\nimport android.os.Bundle\nimport android.text.InputType\nimport android.text.Spannable\nimport android.text.SpannableStringBuilder\nimport android.text.TextUtils\nimport android.text.style.ImageSpan\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.MotionEvent\nimport android.view.View\nimport android.view.ViewConfiguration\nimport android.view.ViewGroup\nimport android.view.ViewPropertyAnimator\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.activity.result.PickVisualMediaRequest\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly\nimport androidx.annotation.IntDef\nimport androidx.appcompat.app.AlertDialog\nimport androidx.appcompat.content.res.AppCompatResources\nimport androidx.appcompat.widget.PopupMenu\nimport androidx.appcompat.widget.Toolbar\nimport androidx.core.view.GravityCompat\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.WindowInsetsAnimationCompat\nimport androidx.drawerlayout.widget.DrawerLayout\nimport androidx.lifecycle.lifecycleScope\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.datepicker.CalendarConstraints\nimport com.google.android.material.datepicker.CalendarConstraints.DateValidator\nimport com.google.android.material.datepicker.CompositeDateValidator\nimport com.google.android.material.datepicker.DateValidatorPointBackward\nimport com.google.android.material.datepicker.DateValidatorPointForward\nimport com.google.android.material.datepicker.MaterialDatePicker\nimport com.google.android.material.floatingactionbutton.FloatingActionButton\nimport com.hippo.app.EditTextCheckBoxDialogBuilder\nimport com.hippo.app.EditTextDialogBuilder\nimport com.hippo.drawable.AddDeleteDrawable\nimport com.hippo.drawable.DrawerArrowDrawable\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.FastScroller.OnDragHandlerListener\nimport com.hippo.easyrecyclerview.LinearDividerItemDecoration\nimport com.hippo.ehviewer.EhApplication.Companion.favouriteStatusRouter\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.FavouriteStatusRouter\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.WindowInsetsAnimationHelper\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.data.ListUrlBuilder\nimport com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_SUBSCRIPTION\nimport com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_TOPLIST\nimport com.hippo.ehviewer.client.data.ListUrlBuilder.Companion.MODE_WHATS_HOT\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.client.parser.GalleryDetailUrlParser\nimport com.hippo.ehviewer.client.parser.GalleryListParser\nimport com.hippo.ehviewer.client.parser.GalleryPageUrlParser\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.dao.QuickSearch\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener\nimport com.hippo.ehviewer.ui.CommonOperations\nimport com.hippo.ehviewer.ui.GalleryActivity\nimport com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter\nimport com.hippo.ehviewer.widget.GalleryInfoContentHelper\nimport com.hippo.ehviewer.widget.SearchBar\nimport com.hippo.ehviewer.widget.SearchBar.OnStateChangeListener\nimport com.hippo.ehviewer.widget.SearchBar.Suggestion\nimport com.hippo.ehviewer.widget.SearchBar.SuggestionProvider\nimport com.hippo.ehviewer.widget.SearchLayout\nimport com.hippo.scene.Announcer\nimport com.hippo.scene.SceneFragment\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.util.launchIO\nimport com.hippo.util.toEpochMillis\nimport com.hippo.util.withUIContext\nimport com.hippo.view.BringOutTransition\nimport com.hippo.view.ViewTransition\nimport com.hippo.widget.ContentLayout\nimport com.hippo.widget.FabLayout\nimport com.hippo.widget.FabLayout.OnClickFabListener\nimport com.hippo.widget.FabLayout.OnExpandListener\nimport com.hippo.widget.SearchBarMover\nimport com.hippo.yorozuya.AnimationUtils\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.SimpleAnimatorListener\nimport com.hippo.yorozuya.ViewUtils\nimport kotlin.time.Clock\nimport kotlinx.datetime.DateTimeUnit\nimport kotlinx.datetime.LocalDate\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.minus\nimport kotlinx.datetime.todayIn\nimport rikka.core.res.resolveColor\n\nclass GalleryListScene :\n    BaseScene(),\n    OnDragHandlerListener,\n    OnStateChangeListener,\n    SearchLayout.Helper,\n    SearchBarMover.Helper,\n    SearchBar.Helper,\n    View.OnClickListener,\n    OnClickFabListener,\n    OnExpandListener {\n    private val mDownloadManager = DownloadManager\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    private val mDownloadInfoListener: DownloadInfoListener = object : DownloadInfoListener {\n        override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n            mAdapter?.notifyDataSetChanged()\n        }\n        override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {}\n        override fun onUpdateAll() {}\n        override fun onReload() {\n            mAdapter?.notifyDataSetChanged()\n        }\n        override fun onChange() {\n            mAdapter?.notifyDataSetChanged()\n        }\n        override fun onRenameLabel(from: String, to: String) {}\n        override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n            mAdapter?.notifyDataSetChanged()\n        }\n        override fun onUpdateLabels() {}\n    }\n\n    private val mFavouriteStatusRouter: FavouriteStatusRouter = favouriteStatusRouter\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    private val mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener =\n        FavouriteStatusRouter.Listener { _: Long, _: Int ->\n            mAdapter?.notifyDataSetChanged()\n        }\n\n    private val mOnScrollListener: RecyclerView.OnScrollListener =\n        object : RecyclerView.OnScrollListener() {\n            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {}\n            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {\n                if (dy >= mHideActionFabSlop) {\n                    hideActionFab()\n                } else if (dy <= -mHideActionFabSlop / 2) {\n                    showActionFab()\n                }\n            }\n        }\n\n    private val mActionFabAnimatorListener: Animator.AnimatorListener =\n        object : SimpleAnimatorListener() {\n            override fun onAnimationEnd(animation: Animator) {\n                if (null != mFabLayout) {\n                    (mFabLayout!!.primaryFab as View?)!!.visibility = View.INVISIBLE\n                }\n            }\n        }\n\n    private val mSearchFabAnimatorListener: Animator.AnimatorListener =\n        object : SimpleAnimatorListener() {\n            override fun onAnimationEnd(animation: Animator) {\n                if (null != mSearchFab) {\n                    mSearchFab!!.visibility = View.INVISIBLE\n                }\n            }\n        }\n\n    private val selectImageLauncher =\n        registerForActivityResult<PickVisualMediaRequest, Uri>(\n            ActivityResultContracts.PickVisualMedia(),\n        ) { result: Uri? -> mSearchLayout?.setImageUri(result) }\n\n    private lateinit var mUrlBuilder: ListUrlBuilder\n    private lateinit var mQuickSearchList: MutableList<QuickSearch>\n    private var mRecyclerView: EasyRecyclerView? = null\n    private var mAdapter: GalleryListAdapter? = null\n    private var mHelper: GalleryListHelper? = null\n    private var mViewTransition: ViewTransition? = null\n    private var mSearchBar: SearchBar? = null\n    private var mSearchBarMover: SearchBarMover? = null\n    private var mSearchFab: View? = null\n    private var mSearchLayout: SearchLayout? = null\n    private var mLeftDrawable: DrawerArrowDrawable? = null\n    private var mRightDrawable: AddDeleteDrawable? = null\n    private var fabAnimator: ViewPropertyAnimator? = null\n    private var mActionFabDrawable: AddDeleteDrawable? = null\n    private var mFabLayout: FabLayout? = null\n    private var mHideActionFabSlop = 0\n    private var mShowActionFab = true\n    private var mDrawerViewTransition: ViewTransition? = null\n    private var mItemTouchHelper: ItemTouchHelper? = null\n\n    @State\n    private var mState = STATE_NORMAL\n\n    // Double click to exit\n    private var mPressBackTime: Long = 0\n    private var mNavCheckedId = 0\n    private var mHasFirstRefresh = false\n    private var mIsTopList = false\n\n    override fun getNavCheckedItem(): Int = mNavCheckedId\n\n    private fun handleArgs(args: Bundle?) {\n        args ?: return\n        mUrlBuilder = when (args.getString(KEY_ACTION)) {\n            ACTION_HOMEPAGE -> ListUrlBuilder()\n            ACTION_SUBSCRIPTION -> ListUrlBuilder(MODE_SUBSCRIPTION)\n            ACTION_WHATS_HOT -> ListUrlBuilder(MODE_WHATS_HOT)\n            ACTION_TOP_LIST -> ListUrlBuilder(MODE_TOPLIST, mKeyword = Settings.defaultTopList)\n            ACTION_LIST_URL_BUILDER -> args.getParcelableCompat<ListUrlBuilder>(KEY_LIST_URL_BUILDER)\n                ?.copy() ?: ListUrlBuilder()\n            else -> throw IllegalStateException(\"Wrong KEY_ACTION:${args.getString(KEY_ACTION)} when handle args!\")\n        }\n    }\n\n    override fun onNewArguments(args: Bundle) {\n        handleArgs(args)\n        onUpdateUrlBuilder()\n        mHelper?.refresh()\n        setState(STATE_NORMAL)\n        mSearchBarMover?.showSearchBar()\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        mDownloadManager.addDownloadInfoListener(mDownloadInfoListener)\n        mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    override fun onResume() {\n        super.onResume()\n        mAdapter?.type = Settings.listMode\n    }\n\n    private fun onInit() {\n        handleArgs(arguments)\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)\n        mUrlBuilder = savedInstanceState.getParcelableCompat(KEY_LIST_URL_BUILDER)!!\n        mState = savedInstanceState.getInt(KEY_STATE)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        val hasFirstRefresh: Boolean = if (mHelper != null && 1 == mHelper!!.shownViewIndex) {\n            false\n        } else {\n            mHasFirstRefresh\n        }\n        outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh)\n        outState.putParcelable(KEY_LIST_URL_BUILDER, mUrlBuilder)\n        outState.putInt(KEY_STATE, mState)\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener)\n        mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener)\n    }\n\n    private fun setSearchBarHint(searchBar: SearchBar) {\n        searchBar.setEditTextHint(getString(if (EhUtils.isExHentai) R.string.gallery_list_search_bar_hint_exhentai else R.string.gallery_list_search_bar_hint_e_hentai))\n    }\n\n    private fun setSearchBarSuggestionProvider(searchBar: SearchBar) {\n        searchBar.setSuggestionProvider(object : SuggestionProvider {\n            override fun providerSuggestions(text: String): List<Suggestion>? {\n                val result1 = GalleryDetailUrlParser.parse(text, false)\n                if (result1 != null) {\n                    return listOf<Suggestion>(\n                        GalleryDetailUrlSuggestion(\n                            result1.gid,\n                            result1.token,\n                        ),\n                    )\n                }\n                val result2 = GalleryPageUrlParser.parse(text, false)\n                if (result2 != null) {\n                    return listOf<Suggestion>(\n                        GalleryPageUrlSuggestion(\n                            result2.gid,\n                            result2.pToken,\n                            result2.page,\n                        ),\n                    )\n                }\n                return null\n            }\n        })\n    }\n\n    private fun wrapTagKeyword(keyword: String): String = if (keyword.endsWith(':')) {\n        keyword\n    } else if (keyword.contains(\" \")) {\n        val tag = keyword.substringAfter(':')\n        val prefix = keyword.dropLast(tag.length)\n        \"$prefix\\\"$tag$\\\"\"\n    } else {\n        \"$keyword$\"\n    }\n\n    // Update search bar title, drawer checked item\n    private fun onUpdateUrlBuilder() {\n        val resources = resourcesOrNull\n        if (resources == null || mSearchLayout == null || mFabLayout == null) {\n            return\n        }\n        var keyword = mUrlBuilder.keyword\n        val category = mUrlBuilder.category\n        val mode = mUrlBuilder.mode\n        val isPopular = mode == MODE_WHATS_HOT\n        val isTopList = mode == MODE_TOPLIST\n\n        if (isTopList != mIsTopList) {\n            mIsTopList = isTopList\n            recreateDrawerView()\n            mFabLayout!!.getSecondaryFabAt(0)!!.setImageResource(if (isTopList) R.drawable.ic_baseline_format_list_numbered_24 else R.drawable.v_magnify_x24)\n        }\n\n        // Update fab visibility\n        mFabLayout!!.setSecondaryFabVisibilityAt(1, !isPopular)\n        mFabLayout!!.setSecondaryFabVisibilityAt(2, !isTopList && !isPopular)\n\n        // Update normal search mode\n        mSearchLayout!!.setNormalSearchMode(if (mode == MODE_SUBSCRIPTION) R.id.search_subscription_search else R.id.search_normal_search)\n\n        // Update search edit text\n        if (!TextUtils.isEmpty(keyword) && null != mSearchBar && !mIsTopList) {\n            if (mode == ListUrlBuilder.MODE_TAG) {\n                keyword = wrapTagKeyword(keyword!!)\n            }\n            mSearchBar!!.setText(keyword!!)\n            mSearchBar!!.cursorToEnd()\n        }\n\n        // Update title\n        val title = getSuitableTitleForUrlBuilder(resources, mUrlBuilder, true) ?: resources.getString(R.string.search)\n        mSearchBar?.setTitle(title)\n\n        // Update nav checked item\n        val checkedItemId: Int = when (mode) {\n            ListUrlBuilder.MODE_NORMAL -> if (EhUtils.NONE == category && TextUtils.isEmpty(keyword)) R.id.nav_homepage else 0\n            MODE_SUBSCRIPTION -> R.id.nav_subscription\n            MODE_WHATS_HOT -> R.id.nav_whats_hot\n            MODE_TOPLIST -> R.id.nav_toplist\n            ListUrlBuilder.MODE_TAG, ListUrlBuilder.MODE_UPLOADER, ListUrlBuilder.MODE_IMAGE_SEARCH -> 0\n            else -> throw IllegalStateException(\"Unexpected value: $mode\")\n        }\n        setNavCheckedItem(checkedItemId)\n        mNavCheckedId = checkedItemId\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_gallery_list, container, false)\n        val context = requireContext()\n        mHideActionFabSlop = ViewConfiguration.get(context).scaledTouchSlop\n        mShowActionFab = true\n        val mainLayout = ViewUtils.`$$`(view, R.id.main_layout)\n        val mContentLayout = ViewUtils.`$$`(mainLayout, R.id.content_layout) as ContentLayout\n        mRecyclerView = mContentLayout.recyclerView\n        val fastScroller = mContentLayout.fastScroller\n        mSearchLayout = ViewUtils.`$$`(mainLayout, R.id.search_layout) as SearchLayout\n        mSearchBar = ViewUtils.`$$`(mainLayout, R.id.search_bar) as SearchBar\n        mFabLayout = ViewUtils.`$$`(mainLayout, R.id.fab_layout) as FabLayout\n        mSearchFab = ViewUtils.`$$`(mainLayout, R.id.search_fab)\n        ViewCompat.setWindowInsetsAnimationCallback(\n            view,\n            WindowInsetsAnimationHelper(\n                WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP,\n                mFabLayout,\n                mSearchFab!!.parent as View,\n            ),\n        )\n        val paddingTopSB = resources.getDimensionPixelOffset(R.dimen.gallery_padding_top_search_bar)\n        val paddingBottomFab = resources.getDimensionPixelOffset(R.dimen.gallery_padding_bottom_fab)\n        mViewTransition = BringOutTransition(mContentLayout, mSearchLayout)\n        mHelper = GalleryListHelper()\n        mContentLayout.setHelper(mHelper!!)\n        mContentLayout.fastScroller.setOnDragHandlerListener(this)\n        mContentLayout.setFitPaddingTop(paddingTopSB)\n        mAdapter = GalleryListAdapter(\n            inflater,\n            resources,\n            mRecyclerView!!,\n            Settings.listMode,\n        )\n        mRecyclerView!!.clipToPadding = false\n        mRecyclerView!!.clipChildren = false\n        mRecyclerView!!.addOnScrollListener(mOnScrollListener)\n        fastScroller.setPadding(\n            fastScroller.paddingLeft,\n            fastScroller.paddingTop + paddingTopSB,\n            fastScroller.paddingRight,\n            fastScroller.paddingBottom,\n        )\n        mLeftDrawable = DrawerArrowDrawable(context, theme.resolveColor(android.R.attr.colorControlNormal))\n        mRightDrawable = AddDeleteDrawable(context, theme.resolveColor(android.R.attr.colorControlNormal))\n        mSearchBar!!.setLeftDrawable(mLeftDrawable!!)\n        mSearchBar!!.setRightDrawable(mRightDrawable!!)\n        mSearchBar!!.setHelper(this)\n        mSearchBar!!.setOnStateChangeListener(this)\n        setSearchBarHint(mSearchBar!!)\n        setSearchBarSuggestionProvider(mSearchBar!!)\n        mSearchLayout!!.setHelper(this)\n        mSearchLayout!!.setPadding(\n            mSearchLayout!!.paddingLeft,\n            mSearchLayout!!.paddingTop + paddingTopSB,\n            mSearchLayout!!.paddingRight,\n            mSearchLayout!!.paddingBottom + paddingBottomFab,\n        )\n        mFabLayout!!.setAutoCancel(true)\n        mFabLayout!!.isExpanded = false\n        mFabLayout!!.setHidePrimaryFab(false)\n        mFabLayout!!.setOnClickFabListener(this)\n        mFabLayout!!.setOnExpandListener(this)\n        addAboveSnackView(mFabLayout!!)\n        mActionFabDrawable = AddDeleteDrawable(context, context.getColor(R.color.primary_drawable_dark))\n        mFabLayout!!.primaryFab!!.setImageDrawable(mActionFabDrawable)\n        mSearchFab!!.setOnClickListener(this)\n        mSearchBarMover = SearchBarMover(this, mSearchBar, mRecyclerView, mSearchLayout)\n\n        // Update list url builder\n        onUpdateUrlBuilder()\n\n        // Restore state\n        val newState = mState\n        mState = STATE_NORMAL\n        setState(newState, false)\n\n        // Only refresh for the first time\n        if (!mHasFirstRefresh) {\n            mHasFirstRefresh = true\n            mHelper!!.firstRefresh()\n        }\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        if (null != mSearchBarMover) {\n            mSearchBarMover!!.cancelAnimation()\n            mSearchBarMover = null\n        }\n        if (null != mHelper) {\n            mHelper!!.destroy()\n            if (1 == mHelper!!.shownViewIndex) {\n                mHasFirstRefresh = false\n            }\n        }\n        if (null != mRecyclerView) {\n            mRecyclerView!!.stopScroll()\n            mRecyclerView = null\n        }\n        if (null != mFabLayout) {\n            removeAboveSnackView(mFabLayout!!)\n            mFabLayout = null\n        }\n        mAdapter = null\n        mSearchLayout = null\n        mSearchBar = null\n        mSearchFab = null\n        mViewTransition = null\n        mLeftDrawable = null\n        mRightDrawable = null\n        mActionFabDrawable = null\n    }\n\n    private fun updateDrawerView(animation: Boolean) {\n        if (null == mDrawerViewTransition) {\n            return\n        }\n        if (mIsTopList || mQuickSearchList.isNotEmpty()) {\n            mDrawerViewTransition!!.showView(0, animation)\n        } else {\n            mDrawerViewTransition!!.showView(1, animation)\n        }\n    }\n\n    private fun showQuickSearchTipDialog() {\n        val context = context ?: return\n        AlertDialog.Builder(context)\n            .setTitle(R.string.readme)\n            .setMessage(R.string.add_quick_search_tip)\n            .setPositiveButton(android.R.string.ok, null)\n            .show()\n    }\n\n    private fun showAddQuickSearchDialog(adapter: QsDrawerAdapter) {\n        val context = context\n        if (null == context || null == mHelper) {\n            return\n        }\n\n        // Can't add image search as quick search\n        if (ListUrlBuilder.MODE_IMAGE_SEARCH == mUrlBuilder.mode) {\n            showTip(R.string.image_search_not_quick_search, LENGTH_LONG)\n            return\n        }\n\n        // Get next gid\n        val gi = mHelper!!.firstVisibleItem\n        val next = if (gi != null) \"@\" + (gi.gid + 1) else null\n\n        // Check duplicate\n        for (q in mQuickSearchList) {\n            if (mUrlBuilder.equalsQuickSearch(q)) {\n                val i = q.name!!.lastIndexOf(\"@\")\n                if (i != -1 && q.name!!.substring(i) == next) {\n                    showTip(getString(R.string.duplicate_quick_search, q.name), LENGTH_LONG)\n                    return\n                }\n            }\n        }\n\n        val builder = EditTextCheckBoxDialogBuilder(\n            context,\n            getSuitableTitleForUrlBuilder(context.resources, mUrlBuilder, false),\n            getString(R.string.quick_search),\n            getString(R.string.save_progress),\n            Settings.qSSaveProgress,\n        )\n        builder.setTitle(R.string.add_quick_search_dialog_title)\n        builder.setPositiveButton(android.R.string.ok, null)\n        val dialog = builder.show()\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {\n            lifecycleScope.launchIO {\n                var text = builder.text.trim { it <= ' ' }\n\n                // Check name empty\n                if (TextUtils.isEmpty(text)) {\n                    withUIContext {\n                        builder.setError(getString(R.string.name_is_empty))\n                    }\n                    return@launchIO\n                }\n\n                // Add gid\n                val checked = builder.isChecked\n                Settings.putQSSaveProgress(checked)\n                if (checked && next != null) {\n                    text += next\n                }\n\n                // Check name duplicate\n                for ((_, name) in mQuickSearchList) {\n                    if (text == name) {\n                        withUIContext {\n                            builder.setError(getString(R.string.duplicate_name))\n                        }\n                        return@launchIO\n                    }\n                }\n                withUIContext {\n                    builder.setError(null)\n                }\n                dialog.dismiss()\n                val quickSearch = mUrlBuilder.toQuickSearch()\n                quickSearch.name = text\n                mQuickSearchList.add(quickSearch)\n                // DB Actions\n                EhDB.insertQuickSearch(quickSearch)\n                withUIContext {\n                    adapter.notifyItemInserted(mQuickSearchList.size - 1)\n                    updateDrawerView(true)\n                }\n            }\n        }\n    }\n\n    override fun onCreateDrawerView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.drawer_list_rv, container, false)\n        val toolbar = ViewUtils.`$$`(view, R.id.toolbar) as Toolbar\n        val tip = ViewUtils.`$$`(view, R.id.tip) as TextView\n        val recyclerView = view.findViewById<EasyRecyclerView>(R.id.recycler_view_drawer)\n        mDrawerViewTransition = ViewTransition(recyclerView, tip)\n        recyclerView.layoutManager = LinearLayoutManager(requireContext())\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(requireContext(), 1f),\n        )\n        decoration.setShowLastDivider(true)\n        recyclerView.addItemDecoration(decoration)\n        val qsDrawerAdapter = QsDrawerAdapter(inflater)\n        qsDrawerAdapter.setHasStableIds(true)\n        mItemTouchHelper = ItemTouchHelper(GalleryListQSItemTouchHelperCallback(qsDrawerAdapter))\n        mItemTouchHelper!!.attachToRecyclerView(recyclerView)\n        lifecycleScope.launchIO {\n            // DB Actions\n            mQuickSearchList = EhDB.allQuickSearch.toMutableList()\n            withUIContext {\n                recyclerView.adapter = qsDrawerAdapter\n                updateDrawerView(false)\n            }\n        }\n        tip.setText(R.string.quick_search_tip)\n        toolbar.setTitle(if (mIsTopList) R.string.toplist else R.string.quick_search)\n        if (!mIsTopList) toolbar.inflateMenu(R.menu.drawer_gallery_list)\n        toolbar.setOnMenuItemClickListener { item: MenuItem ->\n            when (item.itemId) {\n                R.id.action_add -> showAddQuickSearchDialog(qsDrawerAdapter)\n                R.id.action_help -> showQuickSearchTipDialog()\n            }\n            true\n        }\n        return view\n    }\n\n    private fun checkDoubleClickExit(): Boolean {\n        if (stackIndex != 0) {\n            return false\n        }\n        val time = System.currentTimeMillis()\n        return if (time - mPressBackTime > BACK_PRESSED_INTERVAL) {\n            // It is the last scene\n            mPressBackTime = time\n            showTip(R.string.press_twice_exit, LENGTH_SHORT)\n            true\n        } else {\n            false\n        }\n    }\n\n    override fun onBackPressed() {\n        if (null != mFabLayout && mFabLayout!!.isExpanded) {\n            mFabLayout!!.setExpanded(expanded = false, animation = true)\n            return\n        }\n        var handle = false\n        when (mState) {\n            STATE_NORMAL -> handle = checkDoubleClickExit()\n            STATE_SIMPLE_SEARCH, STATE_SEARCH -> {\n                setState(STATE_NORMAL)\n                handle = true\n            }\n            STATE_SEARCH_SHOW_LIST -> {\n                setState(STATE_SEARCH)\n                handle = true\n            }\n        }\n        if (!handle) {\n            finish()\n        }\n    }\n\n    fun onItemClick(view: View, position: Int) {\n        if (null == mHelper || null == mRecyclerView) {\n            return\n        }\n        val gi = mHelper!!.getDataAtEx(position) ?: return\n        val args = Bundle()\n        args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO)\n        args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi)\n        val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)\n        view.findViewById<View>(R.id.thumb)?.let {\n            announcer.setTranHelper(EnterGalleryDetailTransaction(it))\n        }\n        startScene(announcer)\n    }\n\n    override fun onClick(v: View) {\n        if (STATE_NORMAL != mState && null != mSearchBar) {\n            mSearchBar!!.applySearch()\n            hideSoftInput()\n        }\n    }\n\n    override fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton) {\n        if (STATE_NORMAL == mState) {\n            view.toggle()\n        }\n    }\n\n    private fun showGoToDialog() {\n        val context = context\n        if (null == context || null == mHelper) {\n            return\n        }\n        if (mIsTopList) {\n            val page = mHelper!!.pageForTop + 1\n            val pages = mHelper!!.pages\n            val hint = getString(R.string.go_to_hint, page, pages)\n            val builder = EditTextDialogBuilder(context, null, hint)\n            builder.editText.inputType =\n                InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL\n            val dialog = builder.setTitle(R.string.go_to)\n                .setPositiveButton(android.R.string.ok, null)\n                .show()\n            dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {\n                val text = builder.text.trim { it <= ' ' }\n                val goTo: Int = try {\n                    text.toInt() - 1\n                } catch (_: NumberFormatException) {\n                    builder.setError(getString(R.string.error_invalid_number))\n                    return@setOnClickListener\n                }\n                if (goTo < 0 || goTo >= pages) {\n                    builder.setError(getString(R.string.error_out_of_range))\n                    return@setOnClickListener\n                }\n                builder.setError(null)\n                mHelper!!.goTo(goTo)\n                dialog.dismiss()\n            }\n        } else {\n            val initial = LocalDate(2007, 3, 21)\n            val yesterday = Clock.System.todayIn(TimeZone.UTC).minus(1, DateTimeUnit.DAY)\n            val initialMillis = initial.toEpochMillis()\n            val yesterdayMillis = yesterday.toEpochMillis()\n            val listValidators = ArrayList<DateValidator>()\n            listValidators.add(DateValidatorPointForward.from(initialMillis))\n            listValidators.add(DateValidatorPointBackward.before(yesterdayMillis))\n            val constraintsBuilder = CalendarConstraints.Builder()\n                .setStart(initialMillis)\n                .setEnd(yesterdayMillis)\n                .setValidator(CompositeDateValidator.allOf(listValidators))\n            val datePicker = MaterialDatePicker.Builder.datePicker()\n                .setCalendarConstraints(constraintsBuilder.build())\n                .setTitleText(R.string.go_to)\n                .setSelection(yesterdayMillis)\n                .build()\n            datePicker.show(requireActivity().supportFragmentManager, \"date-picker\")\n            datePicker.addOnPositiveButtonClickListener { v: Long? ->\n                mHelper!!.goTo(\n                    v!!,\n                    true,\n                )\n            }\n        }\n    }\n\n    private fun showGidDialog() {\n        val context = context\n        if (null == context || null == mHelper) {\n            return\n        }\n        val builder = EditTextDialogBuilder(context, null, getString(R.string.go_to_gid))\n        val dialog = builder.setTitle(R.string.go_to)\n            .setPositiveButton(android.R.string.ok, null)\n            .show()\n        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {\n            var text = builder.text.trim { it <= ' ' }\n            if (TextUtils.isEmpty(text)) text = \"0\"\n            val goTo: Int = try {\n                text.toInt() + 1\n            } catch (_: NumberFormatException) {\n                builder.setError(getString(R.string.error_invalid_number))\n                return@setOnClickListener\n            }\n            if (goTo < 1) {\n                builder.setError(getString(R.string.error_out_of_range))\n                return@setOnClickListener\n            }\n            builder.setError(null)\n            mHelper!!.goTo(goTo.toString(), goTo != 1)\n            dialog.dismiss()\n        }\n    }\n\n    override fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int) {\n        if (null == mHelper) {\n            return\n        }\n        when (position) {\n            // Open right\n            0 -> openDrawer(GravityCompat.END)\n            // Go to\n            1 -> {\n                if (!mIsTopList || mHelper!!.canGoTo()) showGoToDialog()\n            }\n            // Last page\n            2 -> showGidDialog()\n            // Refresh\n            3 -> mHelper!!.refresh()\n        }\n        view.isExpanded = false\n    }\n\n    override fun onExpand(expanded: Boolean) {\n        if (null == mActionFabDrawable) {\n            return\n        }\n        if (expanded) {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)\n            mActionFabDrawable!!.setDelete(ANIMATE_TIME)\n        } else {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)\n            mActionFabDrawable!!.setAdd(ANIMATE_TIME)\n        }\n    }\n\n    fun onItemLongClick(position: Int): Boolean {\n        val context = context\n        val activity = mainActivity\n        if (null == context || null == activity || null == mHelper) {\n            return false\n        }\n        val gi = mHelper!!.getDataAtEx(position) ?: return true\n        val downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID\n        val favourited = gi.favoriteSlot != -2\n        val items = if (downloaded) {\n            arrayOf<CharSequence>(\n                context.getString(R.string.read),\n                context.getString(R.string.delete_downloads),\n                context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),\n                context.getString(R.string.download_move_dialog_title),\n            )\n        } else {\n            arrayOf<CharSequence>(\n                context.getString(R.string.read),\n                context.getString(R.string.download),\n                context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),\n            )\n        }\n        val icons = if (downloaded) {\n            intArrayOf(\n                R.drawable.v_book_open_x24,\n                R.drawable.v_delete_x24,\n                if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,\n                R.drawable.v_folder_move_x24,\n            )\n        } else {\n            intArrayOf(\n                R.drawable.v_book_open_x24,\n                R.drawable.v_download_x24,\n                if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,\n            )\n        }\n        AlertDialog.Builder(context)\n            .setTitle(EhUtils.getSuitableTitle(gi))\n            .setAdapter(\n                SelectItemWithIconAdapter(\n                    context,\n                    items,\n                    icons,\n                ),\n            ) { _: DialogInterface?, which: Int ->\n                when (which) {\n                    0 -> {\n                        val intent = Intent(activity, GalleryActivity::class.java)\n                        intent.action = GalleryActivity.ACTION_EH\n                        intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi)\n                        startActivity(intent)\n                    }\n                    1 -> if (downloaded) {\n                        AlertDialog.Builder(context)\n                            .setTitle(R.string.download_remove_dialog_title)\n                            .setMessage(\n                                getString(\n                                    R.string.download_remove_dialog_message,\n                                    gi.title,\n                                ),\n                            )\n                            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->\n                                // DownloadManager Actions\n                                mDownloadManager.deleteDownload(\n                                    gi.gid,\n                                )\n                            }\n                            .show()\n                    } else {\n                        // CommonOperations Actions\n                        CommonOperations.startDownload(activity, gi, false)\n                    }\n                    2 -> if (favourited) {\n                        // CommonOperations Actions\n                        CommonOperations.removeFromFavorites(\n                            activity,\n                            gi,\n                            RemoveFromFavoriteListener(context),\n                        )\n                    } else {\n                        // CommonOperations Actions\n                        CommonOperations.addToFavorites(\n                            activity,\n                            gi,\n                            AddToFavoriteListener(context),\n                            false,\n                        )\n                    }\n                    3 -> {\n                        val labelRawList = mDownloadManager.labelList\n                        val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)\n                        labelList.add(getString(R.string.default_download_label_name))\n                        var i = 0\n                        val n = labelRawList.size\n                        while (i < n) {\n                            labelRawList[i].label?.let { labelList.add(it) }\n                            i++\n                        }\n                        val labels = labelList.toTypedArray()\n                        val helper = MoveDialogHelper(labels, gi)\n                        AlertDialog.Builder(context)\n                            .setTitle(R.string.download_move_dialog_title)\n                            .setItems(labels, helper)\n                            .show()\n                    }\n                }\n            }.show()\n        return true\n    }\n\n    private fun showActionFab() {\n        if (null != mFabLayout && STATE_NORMAL == mState && !mShowActionFab) {\n            mShowActionFab = true\n            val fab: View? = mFabLayout!!.primaryFab\n            fabAnimator?.cancel()\n            fab!!.visibility = View.VISIBLE\n            fab.rotation = -45.0f\n            fabAnimator = fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)\n                .setDuration(ANIMATE_TIME).setStartDelay(0L)\n                .setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR)\n            fabAnimator!!.start()\n        }\n    }\n\n    private fun hideActionFab() {\n        if (null != mFabLayout && STATE_NORMAL == mState && mShowActionFab) {\n            mShowActionFab = false\n            val fab: View? = mFabLayout!!.primaryFab\n            fabAnimator?.cancel()\n            fabAnimator =\n                fab!!.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)\n                    .setDuration(ANIMATE_TIME).setStartDelay(0L)\n                    .setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR)\n            fabAnimator!!.start()\n        }\n    }\n\n    private fun selectSearchFab(animation: Boolean) {\n        if (null == mFabLayout || null == mSearchFab) {\n            return\n        }\n        mShowActionFab = false\n        if (animation) {\n            val fab: View? = mFabLayout!!.primaryFab\n            val delay: Long\n            if (View.INVISIBLE == fab!!.visibility) {\n                delay = 0L\n            } else {\n                delay = ANIMATE_TIME\n                mFabLayout!!.setExpanded(expanded = false, animation = true)\n                fab.animate().scaleX(0.0f).scaleY(0.0f).setListener(mActionFabAnimatorListener)\n                    .setDuration(ANIMATE_TIME).setStartDelay(0L)\n                    .setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()\n            }\n            mSearchFab!!.visibility = View.VISIBLE\n            mSearchFab!!.rotation = -45.0f\n            mSearchFab!!.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)\n                .setDuration(ANIMATE_TIME).setStartDelay(delay)\n                .setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()\n        } else {\n            mFabLayout!!.setExpanded(expanded = false, animation = false)\n            val fab: View? = mFabLayout!!.primaryFab\n            fab!!.visibility = View.INVISIBLE\n            fab.scaleX = 0.0f\n            fab.scaleY = 0.0f\n            mSearchFab!!.visibility = View.VISIBLE\n            mSearchFab!!.scaleX = 1.0f\n            mSearchFab!!.scaleY = 1.0f\n        }\n    }\n\n    private fun selectActionFab(animation: Boolean) {\n        if (null == mFabLayout || null == mSearchFab) {\n            return\n        }\n        mShowActionFab = true\n        if (animation) {\n            val delay: Long\n            if (View.INVISIBLE == mSearchFab!!.visibility) {\n                delay = 0L\n            } else {\n                delay = ANIMATE_TIME\n                mSearchFab!!.animate().scaleX(0.0f).scaleY(0.0f)\n                    .setListener(mSearchFabAnimatorListener)\n                    .setDuration(ANIMATE_TIME).setStartDelay(0L)\n                    .setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR).start()\n            }\n            val fab: View? = mFabLayout!!.primaryFab\n            fab!!.visibility = View.VISIBLE\n            fab.rotation = -45.0f\n            fab.animate().scaleX(1.0f).scaleY(1.0f).rotation(0.0f).setListener(null)\n                .setDuration(ANIMATE_TIME).setStartDelay(delay)\n                .setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR).start()\n        } else {\n            mFabLayout!!.setExpanded(expanded = false, animation = false)\n            val fab: View? = mFabLayout!!.primaryFab\n            fab!!.visibility = View.VISIBLE\n            fab.scaleX = 1.0f\n            fab.scaleY = 1.0f\n            mSearchFab!!.visibility = View.INVISIBLE\n            mSearchFab!!.scaleX = 0.0f\n            mSearchFab!!.scaleY = 0.0f\n        }\n    }\n\n    private fun setState(@State state: Int) {\n        setState(state, true)\n    }\n\n    @SuppressLint(\"SwitchIntDef\")\n    private fun setState(@State state: Int, animation: Boolean) {\n        if (null == mSearchBar || null == mSearchBarMover || null == mViewTransition || null == mSearchLayout) {\n            return\n        }\n        if (mState != state) {\n            val oldState = mState\n            mState = state\n            when (oldState) {\n                STATE_NORMAL -> when (state) {\n                    STATE_SIMPLE_SEARCH -> {\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH_LIST, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                        selectSearchFab(animation)\n                    }\n                    STATE_SEARCH -> {\n                        mViewTransition!!.showView(1, animation)\n                        mSearchLayout!!.scrollSearchContainerToTop()\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                        selectSearchFab(animation)\n                    }\n                    STATE_SEARCH_SHOW_LIST -> {\n                        mViewTransition!!.showView(1, animation)\n                        mSearchLayout!!.scrollSearchContainerToTop()\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH_LIST, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                        selectSearchFab(animation)\n                    }\n                }\n                STATE_SIMPLE_SEARCH -> when (state) {\n                    STATE_NORMAL -> {\n                        mSearchBar!!.setState(SearchBar.STATE_NORMAL, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                        selectActionFab(animation)\n                    }\n                    STATE_SEARCH -> {\n                        mViewTransition!!.showView(1, animation)\n                        mSearchLayout!!.scrollSearchContainerToTop()\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                    }\n                    STATE_SEARCH_SHOW_LIST -> {\n                        mViewTransition!!.showView(1, animation)\n                        mSearchLayout!!.scrollSearchContainerToTop()\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH_LIST, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                    }\n                }\n                STATE_SEARCH -> when (state) {\n                    STATE_NORMAL -> {\n                        mViewTransition!!.showView(0, animation)\n                        mSearchBar!!.setState(SearchBar.STATE_NORMAL, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                        selectActionFab(animation)\n                    }\n                    STATE_SIMPLE_SEARCH -> {\n                        mViewTransition!!.showView(0, animation)\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH_LIST, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                    }\n                    STATE_SEARCH_SHOW_LIST -> {\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH_LIST, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                    }\n                }\n                STATE_SEARCH_SHOW_LIST -> when (state) {\n                    STATE_NORMAL -> {\n                        mViewTransition!!.showView(0, animation)\n                        mSearchBar!!.setState(SearchBar.STATE_NORMAL, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                        selectActionFab(animation)\n                    }\n                    STATE_SIMPLE_SEARCH -> {\n                        mViewTransition!!.showView(0, animation)\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH_LIST, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                    }\n                    STATE_SEARCH -> {\n                        mSearchBar!!.setState(SearchBar.STATE_SEARCH, animation)\n                        mSearchBarMover!!.returnSearchBarPosition()\n                    }\n                }\n            }\n        }\n    }\n\n    override fun onClickTitle() {\n        if (mState == STATE_NORMAL) {\n            setState(STATE_SIMPLE_SEARCH)\n        }\n    }\n\n    override fun onClickLeftIcon() {\n        if (null == mSearchBar) {\n            return\n        }\n        if (mSearchBar!!.getState() == SearchBar.STATE_NORMAL) {\n            toggleDrawer(GravityCompat.START)\n        } else {\n            setState(STATE_NORMAL)\n        }\n    }\n\n    override fun onClickRightIcon() {\n        if (null == mSearchBar) {\n            return\n        }\n        if (mSearchBar!!.getState() == SearchBar.STATE_NORMAL) {\n            setState(STATE_SEARCH)\n        } else {\n            if (mSearchBar!!.getEditText().length() == 0) {\n                setState(STATE_NORMAL)\n            } else {\n                // Clear\n                mSearchBar!!.setText(\"\")\n            }\n        }\n    }\n\n    override fun onSearchEditTextClick() {\n        if (mState == STATE_SEARCH) {\n            setState(STATE_SEARCH_SHOW_LIST)\n        }\n    }\n\n    override fun onApplySearch(query: String) {\n        if (null == mHelper || null == mSearchLayout) {\n            return\n        }\n        if (mState == STATE_SEARCH || mState == STATE_SEARCH_SHOW_LIST) {\n            try {\n                mSearchLayout!!.formatListUrlBuilder(mUrlBuilder, query)\n            } catch (e: EhException) {\n                showTip(e.message, LENGTH_LONG)\n                return\n            }\n        } else {\n            val oldMode = mUrlBuilder.mode\n            // If it's MODE_SUBSCRIPTION, keep it\n            val newMode =\n                if (oldMode == MODE_SUBSCRIPTION) MODE_SUBSCRIPTION else ListUrlBuilder.MODE_NORMAL\n            mUrlBuilder.reset()\n            mUrlBuilder.mode = newMode\n            mUrlBuilder.keyword = query\n        }\n        onUpdateUrlBuilder()\n        mHelper!!.refresh()\n        setState(STATE_NORMAL)\n    }\n\n    override fun onSearchEditTextBackPressed() {\n        onBackPressed()\n    }\n\n    override fun onReceiveContent(uri: Uri?) {\n        if (null == mSearchLayout || null == uri) {\n            return\n        }\n        mSearchLayout!!.setSearchMode(SearchLayout.SEARCH_MODE_IMAGE)\n        mSearchLayout!!.setImageUri(uri)\n        setState(STATE_SEARCH)\n    }\n\n    override fun onStartDragHandler() {\n        // Lock right drawer\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)\n    }\n\n    override fun onEndDragHandler() {\n        // Restore right drawer\n        setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)\n        if (null != mSearchBarMover) {\n            mSearchBarMover!!.returnSearchBarPosition()\n        }\n    }\n\n    override fun onStateChange(searchBar: SearchBar, newState: Int, oldState: Int, animation: Boolean) {\n        if (null == mLeftDrawable || null == mRightDrawable) {\n            return\n        }\n\n        when (oldState) {\n            SearchBar.STATE_NORMAL -> {\n                mLeftDrawable!!.setArrow(if (animation) ANIMATE_TIME else 0)\n                mRightDrawable!!.setDelete(if (animation) ANIMATE_TIME else 0)\n            }\n            SearchBar.STATE_SEARCH -> if (newState == SearchBar.STATE_NORMAL) {\n                mLeftDrawable!!.setMenu(if (animation) ANIMATE_TIME else 0)\n                mRightDrawable!!.setAdd(if (animation) ANIMATE_TIME else 0)\n            }\n            SearchBar.STATE_SEARCH_LIST -> if (newState == SearchBar.STATE_NORMAL) {\n                mLeftDrawable!!.setMenu(if (animation) ANIMATE_TIME else 0)\n                mRightDrawable!!.setAdd(if (animation) ANIMATE_TIME else 0)\n            }\n        }\n\n        if (newState == STATE_NORMAL || newState == STATE_SIMPLE_SEARCH) {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.END)\n        } else {\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)\n            setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.END)\n        }\n    }\n\n    override fun onChangeSearchMode() {\n        if (null != mSearchBarMover) {\n            mSearchBarMover!!.showSearchBar()\n        }\n    }\n\n    override fun onSelectImage() {\n        val builder = PickVisualMediaRequest.Builder()\n        builder.setMediaType(ImageOnly)\n        selectImageLauncher.launch(builder.build())\n    }\n\n    // SearchBarMover.Helper\n    override fun isValidView(recyclerView: RecyclerView): Boolean = (mState == STATE_NORMAL && recyclerView == mRecyclerView) ||\n        (mState == STATE_SEARCH && recyclerView == mSearchLayout)\n\n    // SearchBarMover.Helper\n    override fun getValidRecyclerView(): RecyclerView? = if (mState == STATE_NORMAL || mState == STATE_SIMPLE_SEARCH) {\n        mRecyclerView\n    } else {\n        mSearchLayout\n    }\n\n    // SearchBarMover.Helper\n    override fun forceShowSearchBar(): Boolean = mState == STATE_SIMPLE_SEARCH || mState == STATE_SEARCH_SHOW_LIST\n\n    private fun onGetGalleryListSuccess(result: GalleryListParser.Result, taskId: Int) {\n        if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {\n            val emptyString =\n                getString(if (mUrlBuilder.mode == MODE_SUBSCRIPTION && result.noWatchedTags) R.string.gallery_list_empty_hit_subscription else R.string.gallery_list_empty_hit)\n            mHelper!!.setEmptyString(emptyString)\n            if (mIsTopList) {\n                mHelper!!.onGetPageData(\n                    taskId,\n                    result.pages,\n                    result.nextPage,\n                    null,\n                    null,\n                    result.galleryInfoList,\n                )\n            } else {\n                mHelper!!.onGetPageData(\n                    taskId,\n                    0,\n                    0,\n                    result.prev,\n                    result.next,\n                    result.galleryInfoList,\n                )\n            }\n        }\n    }\n\n    private fun onGetGalleryListFailure(e: Exception, taskId: Int) {\n        if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {\n            mHelper!!.onGetException(taskId, e)\n        }\n    }\n\n    @IntDef(STATE_NORMAL, STATE_SIMPLE_SEARCH, STATE_SEARCH, STATE_SEARCH_SHOW_LIST)\n    @Retention(AnnotationRetention.SOURCE)\n    private annotation class State\n    private inner class GetGalleryListListener(\n        context: Context,\n        private val mTaskId: Int,\n    ) : EhCallback<GalleryListScene, GalleryListParser.Result>(context) {\n        override fun onSuccess(result: GalleryListParser.Result) {\n            val scene = this@GalleryListScene\n            scene.onGetGalleryListSuccess(result, mTaskId)\n        }\n\n        override fun onFailure(e: Exception) {\n            val scene = this@GalleryListScene\n            scene.onGetGalleryListFailure(e, mTaskId)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private class AddToFavoriteListener(context: Context) : EhCallback<GalleryListScene, Unit>(context) {\n        override fun onSuccess(result: Unit) {\n            showTip(R.string.add_to_favorite_success, LENGTH_SHORT)\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(R.string.add_to_favorite_failure, LENGTH_LONG)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private class RemoveFromFavoriteListener(context: Context) : EhCallback<GalleryListScene, Unit>(context) {\n        override fun onSuccess(result: Unit) {\n            showTip(R.string.remove_from_favorite_success, LENGTH_SHORT)\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(R.string.remove_from_favorite_failure, LENGTH_LONG)\n        }\n\n        override fun onCancel() {}\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private inner class QsDrawerHolder(\n        itemView: View,\n    ) : RecyclerView.ViewHolder(itemView),\n        View.OnTouchListener {\n        val key: TextView = ViewUtils.`$$`(itemView, R.id.tv_key) as TextView\n        val option: ImageView = ViewUtils.`$$`(itemView, R.id.iv_option) as ImageView\n\n        init {\n            option.setOnTouchListener(this)\n        }\n\n        override fun onTouch(v: View, event: MotionEvent): Boolean {\n            if (mItemTouchHelper != null && event.action == MotionEvent.ACTION_DOWN) {\n                mItemTouchHelper!!.startDrag(this)\n            }\n            return false\n        }\n    }\n\n    private inner class MoveDialogHelper(\n        private val mLabels: Array<String>,\n        private val mGi: GalleryInfo,\n    ) : DialogInterface.OnClickListener {\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            // Cancel check mode\n            context ?: return\n            mRecyclerView?.outOfCustomChoiceMode()\n            val downloadInfo = mDownloadManager.getDownloadInfo(mGi.gid) ?: return\n            val label = if (which == 0) null else mLabels[which]\n            // DownloadManager Actions\n            mDownloadManager.changeLabel(listOf(downloadInfo), label)\n        }\n    }\n\n    private inner class QsDrawerAdapter(private val mInflater: LayoutInflater) : RecyclerView.Adapter<QsDrawerHolder>() {\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QsDrawerHolder {\n            val holder = QsDrawerHolder(mInflater.inflate(R.layout.item_drawer_list, parent, false))\n            if (!mIsTopList) {\n                holder.itemView.setOnClickListener {\n                    if (null == mHelper) {\n                        return@setOnClickListener\n                    }\n                    val quickSearch = mQuickSearchList[holder.bindingAdapterPosition]\n                    mUrlBuilder.set(quickSearch)\n                    onUpdateUrlBuilder()\n                    val i = quickSearch.name!!.lastIndexOf(\"@\")\n                    mHelper!!.goTo(if (i != -1) quickSearch.name!!.substring(i + 1) else null, true)\n                    setState(STATE_NORMAL)\n                    closeDrawer(GravityCompat.END)\n                }\n                holder.itemView.setOnLongClickListener {\n                    val index = holder.bindingAdapterPosition\n                    val quickSearch = mQuickSearchList[index]\n                    val popupMenu = PopupMenu(requireContext(), holder.option)\n                    popupMenu.inflate(R.menu.quicksearch_option)\n                    popupMenu.show()\n                    popupMenu.setOnMenuItemClickListener(\n                        object : PopupMenu.OnMenuItemClickListener {\n                            override fun onMenuItemClick(item: MenuItem): Boolean {\n                                if (item.itemId == R.id.menu_qs_remove) {\n                                    AlertDialog.Builder(requireContext())\n                                        .setTitle(R.string.delete_quick_search_title)\n                                        .setMessage(getString(R.string.delete_quick_search_message, quickSearch.name))\n                                        .setPositiveButton(R.string.delete) { _: DialogInterface?, _: Int ->\n                                            mQuickSearchList.removeAt(index)\n                                            notifyItemRemoved(index)\n                                            updateDrawerView(true)\n                                            lifecycleScope.launchIO {\n                                                // DB Actions\n                                                EhDB.deleteQuickSearch(quickSearch)\n                                            }\n                                        }\n                                        .setNegativeButton(android.R.string.cancel, null)\n                                        .show()\n                                    return true\n                                }\n                                return false\n                            }\n                        },\n                    )\n                    return@setOnLongClickListener true\n                }\n            } else {\n                val keywords = intArrayOf(15, 13, 12, 11)\n                holder.itemView.setOnClickListener {\n                    if (null == mHelper) {\n                        return@setOnClickListener\n                    }\n                    val keyword = keywords[holder.bindingAdapterPosition].toString()\n                    Settings.putDefaultTopList(keyword)\n                    mUrlBuilder.keyword = keyword\n                    onUpdateUrlBuilder()\n                    mHelper!!.refresh()\n                    setState(STATE_NORMAL)\n                    closeDrawer(GravityCompat.END)\n                }\n            }\n            return holder\n        }\n\n        override fun onBindViewHolder(holder: QsDrawerHolder, position: Int) {\n            if (!mIsTopList) {\n                holder.key.text = mQuickSearchList[position].name\n            } else {\n                val toplists = intArrayOf(\n                    R.string.toplist_yesterday,\n                    R.string.toplist_pastmonth,\n                    R.string.toplist_pastyear,\n                    R.string.toplist_alltime,\n                )\n                holder.key.text = getString(toplists[position])\n                holder.option.visibility = View.GONE\n            }\n        }\n\n        override fun getItemId(position: Int): Long = if (mIsTopList) position.toLong() else mQuickSearchList[position].id!!\n\n        override fun getItemCount(): Int = if (mIsTopList) 4 else mQuickSearchList.size\n    }\n\n    private abstract inner class UrlSuggestion : Suggestion() {\n        override fun getText(textView: TextView): CharSequence? = if (textView.id == android.R.id.text1) {\n            val bookImage =\n                AppCompatResources.getDrawable(textView.context, R.drawable.v_book_open_x24)\n            val ssb = SpannableStringBuilder(\"    \")\n            ssb.append(getString(R.string.gallery_list_search_bar_open_gallery))\n            val imageSize = (textView.textSize * 1.25).toInt()\n            if (bookImage != null) {\n                bookImage.setBounds(0, 0, imageSize, imageSize)\n                ssb.setSpan(ImageSpan(bookImage), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)\n            }\n            ssb\n        } else {\n            null\n        }\n\n        override fun onClick() {\n            startScene(createAnnouncer())\n            if (mState == STATE_SIMPLE_SEARCH) {\n                setState(STATE_NORMAL)\n            } else if (mState == STATE_SEARCH_SHOW_LIST) {\n                setState(STATE_SEARCH)\n            }\n        }\n\n        abstract fun createAnnouncer(): Announcer\n    }\n\n    private inner class GalleryDetailUrlSuggestion(\n        private val mGid: Long,\n        private val mToken: String,\n    ) : UrlSuggestion() {\n        override fun createAnnouncer(): Announcer {\n            val args = Bundle()\n            args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)\n            args.putLong(GalleryDetailScene.KEY_GID, mGid)\n            args.putString(GalleryDetailScene.KEY_TOKEN, mToken)\n            return Announcer(GalleryDetailScene::class.java).setArgs(args)\n        }\n    }\n\n    private inner class GalleryPageUrlSuggestion(\n        private val mGid: Long,\n        private val mPToken: String,\n        private val mPage: Int,\n    ) : UrlSuggestion() {\n        override fun createAnnouncer(): Announcer {\n            val args = Bundle()\n            args.putString(ProgressScene.KEY_ACTION, ProgressScene.ACTION_GALLERY_TOKEN)\n            args.putLong(ProgressScene.KEY_GID, mGid)\n            args.putString(ProgressScene.KEY_PTOKEN, mPToken)\n            args.putInt(ProgressScene.KEY_PAGE, mPage)\n            return Announcer(ProgressScene::class.java).setArgs(args)\n        }\n    }\n\n    private inner class GalleryListAdapter(\n        inflater: LayoutInflater,\n        resources: Resources,\n        recyclerView: RecyclerView,\n        type: Int,\n    ) : GalleryAdapter(inflater, resources, recyclerView, type, true) {\n        override fun getItemCount(): Int = mHelper?.size() ?: 0\n\n        override fun onItemClick(view: View, position: Int) {\n            this@GalleryListScene.onItemClick(view, position)\n        }\n\n        override fun onItemLongClick(view: View, position: Int): Boolean = this@GalleryListScene.onItemLongClick(position)\n\n        override fun getDataAt(position: Int): GalleryInfo? = mHelper?.getDataAtEx(position)\n    }\n\n    private inner class GalleryListHelper : GalleryInfoContentHelper() {\n        override fun getPageData(\n            taskId: Int,\n            type: Int,\n            page: Int,\n            index: String?,\n            isNext: Boolean,\n        ) {\n            val activity = mainActivity\n            if (null == activity || null == mHelper) {\n                return\n            }\n            if (mIsTopList) {\n                mUrlBuilder.setJumpTo(page.toString())\n            } else {\n                mUrlBuilder.setIndex(index, isNext)\n                mUrlBuilder.setJumpTo(jumpTo)\n            }\n            val url = mUrlBuilder.build()\n            val request = EhRequest()\n            request.setMethod(EhClient.METHOD_GET_GALLERY_LIST)\n            request.setCallback(\n                GetGalleryListListener(context, taskId),\n            )\n            request.setArgs(url)\n            request.enqueue(this@GalleryListScene)\n        }\n\n        override val context\n            get() = requireContext()\n\n        @SuppressLint(\"NotifyDataSetChanged\")\n        override fun notifyDataSetChanged() {\n            mAdapter?.notifyDataSetChanged()\n        }\n\n        override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) {\n            mAdapter?.notifyItemRangeInserted(positionStart, itemCount)\n        }\n\n        override fun onShowView(hiddenView: View, shownView: View) {\n            mSearchBarMover?.showSearchBar()\n            showActionFab()\n        }\n\n        override fun isDuplicate(d1: GalleryInfo?, d2: GalleryInfo?): Boolean = d1?.gid == d2?.gid && d1 != null && d2 != null\n\n        override fun onScrollToPosition(position: Int) {\n            if (0 == position) {\n                mSearchBarMover?.showSearchBar()\n                showActionFab()\n            }\n        }\n    }\n\n    private inner class GalleryListQSItemTouchHelperCallback(private val mAdapter: QsDrawerAdapter) : ItemTouchHelper.Callback() {\n        override fun getMovementFlags(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n        ): Int = makeMovementFlags(\n            ItemTouchHelper.UP or ItemTouchHelper.DOWN,\n            0,\n        )\n\n        override fun isLongPressDragEnabled(): Boolean = false\n\n        override fun onMove(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n            target: RecyclerView.ViewHolder,\n        ): Boolean {\n            val fromPosition = viewHolder.bindingAdapterPosition\n            val toPosition = target.bindingAdapterPosition\n            if (fromPosition == toPosition) {\n                return false\n            }\n            val item = mQuickSearchList.removeAt(fromPosition)\n            mQuickSearchList.add(toPosition, item)\n            mAdapter.notifyItemMoved(fromPosition, toPosition)\n            lifecycleScope.launchIO {\n                // DB Actions\n                EhDB.moveQuickSearch(fromPosition, toPosition)\n            }\n            return true\n        }\n\n        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}\n    }\n\n    companion object {\n        const val KEY_ACTION = \"action\"\n        const val ACTION_HOMEPAGE = \"action_homepage\"\n        const val ACTION_SUBSCRIPTION = \"action_subscription\"\n        const val ACTION_WHATS_HOT = \"action_whats_hot\"\n        const val ACTION_TOP_LIST = \"action_top_list\"\n        const val ACTION_LIST_URL_BUILDER = \"action_list_url_builder\"\n        const val KEY_LIST_URL_BUILDER = \"list_url_builder\"\n        const val KEY_HAS_FIRST_REFRESH = \"has_first_refresh\"\n        const val KEY_STATE = \"state\"\n        private const val BACK_PRESSED_INTERVAL = 2000\n        private const val STATE_NORMAL = 0\n        private const val STATE_SIMPLE_SEARCH = 1\n        private const val STATE_SEARCH = 2\n        private const val STATE_SEARCH_SHOW_LIST = 3\n        private const val ANIMATE_TIME = 300L\n\n        private fun getSuitableTitleForUrlBuilder(\n            resources: Resources,\n            urlBuilder: ListUrlBuilder,\n            appName: Boolean,\n        ): String? {\n            val keyword = urlBuilder.keyword\n            val category = urlBuilder.category\n            return if (ListUrlBuilder.MODE_NORMAL == urlBuilder.mode &&\n                EhUtils.NONE == category &&\n                TextUtils.isEmpty(keyword) &&\n                urlBuilder.advanceSearch == -1 &&\n                urlBuilder.minRating == -1 &&\n                urlBuilder.pageFrom == -1 &&\n                urlBuilder.pageTo == -1\n            ) {\n                resources.getString(if (appName) R.string.app_name else R.string.homepage)\n            } else if (MODE_SUBSCRIPTION == urlBuilder.mode &&\n                EhUtils.NONE == category &&\n                TextUtils.isEmpty(keyword) &&\n                urlBuilder.advanceSearch == -1 &&\n                urlBuilder.minRating == -1 &&\n                urlBuilder.pageFrom == -1 &&\n                urlBuilder.pageTo == -1\n            ) {\n                resources.getString(R.string.subscription)\n            } else if (MODE_WHATS_HOT == urlBuilder.mode) {\n                resources.getString(R.string.whats_hot)\n            } else if (MODE_TOPLIST == urlBuilder.mode) {\n                when (urlBuilder.keyword) {\n                    \"11\" -> resources.getString(R.string.toplist_alltime)\n                    \"12\" -> resources.getString(R.string.toplist_pastyear)\n                    \"13\" -> resources.getString(R.string.toplist_pastmonth)\n                    \"15\" -> resources.getString(R.string.toplist_yesterday)\n                    else -> null\n                }\n            } else if (!TextUtils.isEmpty(keyword)) {\n                keyword\n            } else if (MathUtils.hammingWeight(category) == 1) {\n                EhUtils.getCategory(category)\n            } else {\n                null\n            }\n        }\n\n        fun startScene(scene: SceneFragment, lub: ListUrlBuilder?) {\n            scene.startScene(getStartAnnouncer(lub))\n        }\n\n        fun getStartAnnouncer(lub: ListUrlBuilder?): Announcer {\n            val args = Bundle()\n            args.putString(KEY_ACTION, ACTION_LIST_URL_BUILDER)\n            args.putParcelable(KEY_LIST_URL_BUILDER, lub)\n            return Announcer(GalleryListScene::class.java).setArgs(args)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/GalleryPreviewsScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.annotation.SuppressLint\nimport android.app.Dialog\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport androidx.appcompat.app.AlertDialog\nimport androidx.recyclerview.widget.RecyclerView\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.MarginItemDecoration\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.data.GalleryDetail\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.data.GalleryPreview\nimport com.hippo.ehviewer.client.data.PreviewSet\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.ehviewer.ui.GalleryActivity\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.widget.ContentLayout\nimport com.hippo.widget.ContentLayout.ContentHelper\nimport com.hippo.widget.LoadImageView\nimport com.hippo.widget.Slider\nimport com.hippo.widget.recyclerview.AutoGridLayoutManager\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.ViewUtils\nimport java.util.Locale\n\nclass GalleryPreviewsScene : ToolbarScene() {\n    private var mGalleryInfo: GalleryInfo? = null\n    private var mRecyclerView: EasyRecyclerView? = null\n    private var mAdapter: GalleryPreviewAdapter? = null\n    private var mHelper: GalleryPreviewHelper? = null\n    private var mHasFirstRefresh = false\n    private var mScrollTo = 0\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    private fun onInit() {\n        val args = arguments ?: return\n        mGalleryInfo = args.getParcelableCompat(KEY_GALLERY_INFO)\n        mScrollTo = args.getInt(KEY_SCROLL_TO)\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO)\n        mHasFirstRefresh = savedInstanceState.getBoolean(KEY_HAS_FIRST_REFRESH)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        val hasFirstRefresh: Boolean = if (mHelper != null && 1 == mHelper!!.shownViewIndex) {\n            false\n        } else {\n            mHasFirstRefresh\n        }\n        outState.putBoolean(KEY_HAS_FIRST_REFRESH, hasFirstRefresh)\n        outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo)\n    }\n\n    override fun onCreateViewWithToolbar(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val mContentLayout = inflater.inflate(\n            R.layout.scene_gallery_previews,\n            container,\n            false,\n        ) as ContentLayout\n        mContentLayout.hideFastScroll()\n        mRecyclerView = mContentLayout.recyclerView\n        mAdapter = GalleryPreviewAdapter()\n        mRecyclerView!!.adapter = mAdapter\n        val columnWidth = Settings.previewSize\n        val layoutManager =\n            AutoGridLayoutManager(context, columnWidth, LayoutUtils.dp2pix(context, 16f))\n        layoutManager.setStrategy(AutoGridLayoutManager.STRATEGY_SUITABLE_SIZE)\n        mRecyclerView!!.layoutManager = layoutManager\n        mRecyclerView!!.clipToPadding = false\n        val padding = LayoutUtils.dp2pix(context, 4f)\n        val decoration = MarginItemDecoration(padding, padding, padding, padding, padding)\n        mRecyclerView!!.addItemDecoration(decoration)\n        mHelper = GalleryPreviewHelper()\n        mContentLayout.setHelper(mHelper!!)\n\n        // Only refresh for the first time\n        if (!mHasFirstRefresh) {\n            mHasFirstRefresh = true\n            if (mScrollTo == -1) {\n                mHelper!!.goTo(1)\n                mScrollTo = 0\n            } else {\n                mHelper!!.firstRefresh()\n            }\n        }\n        return mContentLayout\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        if (null != mHelper) {\n            if (1 == mHelper!!.shownViewIndex) {\n                mHasFirstRefresh = false\n            }\n        }\n        if (null != mRecyclerView) {\n            mRecyclerView!!.stopScroll()\n            mRecyclerView = null\n        }\n        mAdapter = null\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        setTitle(R.string.gallery_previews)\n        setNavigationIcon(R.drawable.v_arrow_left_dark_x24)\n    }\n\n    override fun getMenuResId(): Int = if ((mGalleryInfo as GalleryDetail).previewPages > 1) R.menu.scene_gallery_previews else 0\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        val context = context ?: return false\n        val id = item.itemId\n        if (id == R.id.action_go_to) {\n            if (mHelper == null) {\n                return true\n            }\n            val pages = mHelper!!.pages\n            if (pages > 1 && mHelper!!.canGoTo()) {\n                val helper = GoToDialogHelper(pages, mHelper!!.pageForTop)\n                val dialog = AlertDialog.Builder(context).setTitle(R.string.go_to)\n                    .setView(R.layout.dialog_go_to)\n                    .setPositiveButton(android.R.string.ok, null)\n                    .create()\n                dialog.show()\n                helper.setDialog(dialog)\n            }\n            return true\n        }\n        return false\n    }\n\n    override fun onNavigationClick() {\n        onBackPressed()\n    }\n\n    fun onItemClick(position: Int): Boolean {\n        val context = context\n        if (null != context && null != mHelper && null != mGalleryInfo) {\n            val p = mHelper!!.getDataAtEx(position)\n            if (p != null) {\n                val intent = Intent(context, GalleryActivity::class.java)\n                intent.action = GalleryActivity.ACTION_EH\n                intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, mGalleryInfo)\n                intent.putExtra(GalleryActivity.KEY_PAGE, p.position)\n                startActivity(intent)\n            }\n        }\n        return true\n    }\n\n    private fun onGetPreviewSetSuccess(result: Pair<PreviewSet, Int>, taskId: Int) {\n        if (null != mHelper && mHelper!!.isCurrentTask(taskId) && null != mGalleryInfo) {\n            val previewSet = result.first\n            val size = previewSet.size()\n            val list = ArrayList<GalleryPreview>(size)\n            for (i in 0 until size) {\n                list.add(previewSet.getGalleryPreview(mGalleryInfo!!.gid, i))\n            }\n            mHelper!!.onGetPageData(\n                taskId,\n                result.second,\n                0,\n                null,\n                null,\n                list as List<GalleryPreview>,\n            )\n            if (mScrollTo != 0 && mScrollTo < size) {\n                mHelper!!.scrollTo(mScrollTo)\n                mScrollTo = 0\n            }\n        }\n    }\n\n    private fun onGetPreviewSetFailure(e: Exception, taskId: Int) {\n        if (mHelper != null && mHelper!!.isCurrentTask(taskId)) {\n            mHelper!!.onGetException(taskId, e)\n        }\n    }\n\n    private class GalleryPreviewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        var image: LoadImageView = itemView.findViewById(R.id.image)\n        var text: TextView = itemView.findViewById(R.id.text)\n    }\n\n    private inner class GetPreviewSetListener(\n        context: Context,\n        private val mTaskId: Int,\n    ) : EhCallback<GalleryPreviewsScene, Pair<PreviewSet, Int>>(context) {\n        override fun onSuccess(result: Pair<PreviewSet, Int>) {\n            val scene = this@GalleryPreviewsScene\n            scene.onGetPreviewSetSuccess(result, mTaskId)\n        }\n\n        override fun onFailure(e: Exception) {\n            val scene = this@GalleryPreviewsScene\n            scene.onGetPreviewSetFailure(e, mTaskId)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private inner class GalleryPreviewAdapter : RecyclerView.Adapter<GalleryPreviewHolder>() {\n        private val mInflater: LayoutInflater = layoutInflater\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GalleryPreviewHolder = GalleryPreviewHolder(\n            mInflater.inflate(\n                R.layout.item_gallery_preview,\n                parent,\n                false,\n            ),\n        )\n\n        @SuppressLint(\"SetTextI18n\")\n        override fun onBindViewHolder(holder: GalleryPreviewHolder, position: Int) {\n            mHelper?.getDataAtEx(position)?.let {\n                it.load(holder.image)\n                holder.text.text = (it.position + 1).toString()\n            }\n            holder.itemView.setOnClickListener { onItemClick(holder.bindingAdapterPosition) }\n        }\n\n        override fun getItemCount(): Int = if (mHelper != null) mHelper!!.size() else 0\n    }\n\n    private inner class GalleryPreviewHelper : ContentHelper<GalleryPreview>() {\n        override fun getPageData(\n            taskId: Int,\n            type: Int,\n            page: Int,\n            index: String?,\n            isNext: Boolean,\n        ) {\n            val activity = mainActivity\n            if (null == activity || null == mGalleryInfo) {\n                onGetException(taskId, EhException(getString(R.string.error_cannot_find_gallery)))\n                return\n            }\n            val url =\n                EhUrl.getGalleryDetailUrl(mGalleryInfo!!.gid, mGalleryInfo!!.token, page, false)\n            val request = EhRequest()\n            request.setMethod(EhClient.METHOD_GET_PREVIEW_SET)\n            request.setCallback(\n                GetPreviewSetListener(context, taskId),\n            )\n            request.setArgs(url)\n            request.enqueue(this@GalleryPreviewsScene)\n        }\n\n        override val context\n            get() = this@GalleryPreviewsScene.requireContext()\n\n        @SuppressLint(\"NotifyDataSetChanged\")\n        override fun notifyDataSetChanged() {\n            if (mAdapter != null) {\n                mAdapter!!.notifyDataSetChanged()\n            }\n        }\n\n        override fun notifyItemRangeInserted(positionStart: Int, itemCount: Int) {\n            if (mAdapter != null) {\n                mAdapter!!.notifyItemRangeInserted(positionStart, itemCount)\n            }\n        }\n\n        override fun isDuplicate(d1: GalleryPreview, d2: GalleryPreview): Boolean = false\n    }\n\n    private inner class GoToDialogHelper(private val mPages: Int, private val mCurrentPage: Int) :\n        View.OnClickListener,\n        DialogInterface.OnDismissListener {\n        private var mSlider: Slider? = null\n        private var mDialog: Dialog? = null\n        fun setDialog(dialog: AlertDialog) {\n            mDialog = dialog\n            (ViewUtils.`$$`(dialog, R.id.start) as TextView).text =\n                String.format(Locale.US, \"%d\", 1)\n            (ViewUtils.`$$`(dialog, R.id.end) as TextView).text =\n                String.format(Locale.US, \"%d\", mPages)\n            mSlider = ViewUtils.`$$`(dialog, R.id.slider) as Slider\n            mSlider!!.setRange(1, mPages)\n            mSlider!!.progress = mCurrentPage + 1\n            dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this)\n            dialog.setOnDismissListener(this)\n        }\n\n        override fun onClick(v: View) {\n            if (null == mSlider) {\n                return\n            }\n            val page = mSlider!!.progress - 1\n            if (page in 0 until mPages && mHelper != null) {\n                mHelper!!.goTo(page)\n                if (mDialog != null) {\n                    mDialog!!.dismiss()\n                    mDialog = null\n                }\n            } else {\n                showTip(R.string.error_out_of_range, LENGTH_LONG)\n            }\n        }\n\n        override fun onDismiss(dialog: DialogInterface) {\n            mDialog = null\n            mSlider = null\n        }\n    }\n\n    companion object {\n        const val KEY_GALLERY_INFO = \"gallery_info\"\n        const val KEY_SCROLL_TO = \"scroll_to\"\n        private const val KEY_HAS_FIRST_REFRESH = \"has_first_refresh\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/HistoryScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.Intent\nimport android.os.Bundle\nimport android.text.TextUtils\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.content.ContextCompat\nimport androidx.core.view.GravityCompat\nimport androidx.core.view.ViewCompat\nimport androidx.lifecycle.lifecycleScope\nimport androidx.paging.Pager\nimport androidx.paging.PagingConfig\nimport androidx.paging.PagingDataAdapter\nimport androidx.paging.cachedIn\nimport androidx.recyclerview.widget.DiffUtil\nimport androidx.recyclerview.widget.ItemTouchHelper\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.FastScroller\nimport com.hippo.easyrecyclerview.HandlerDrawable\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.EhDB\nimport com.hippo.ehviewer.FavouriteStatusRouter\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.ehviewer.client.getThumbKey\nimport com.hippo.ehviewer.client.thumbUrl\nimport com.hippo.ehviewer.dao.DownloadInfo\nimport com.hippo.ehviewer.dao.HistoryInfo\nimport com.hippo.ehviewer.download.DownloadManager\nimport com.hippo.ehviewer.download.DownloadManager.DownloadInfoListener\nimport com.hippo.ehviewer.ui.CommonOperations\nimport com.hippo.ehviewer.ui.GalleryActivity\nimport com.hippo.ehviewer.ui.dialog.SelectItemWithIconAdapter\nimport com.hippo.ehviewer.widget.SimpleRatingView\nimport com.hippo.scene.Announcer\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport com.hippo.view.ViewTransition\nimport com.hippo.widget.LoadImageView\nimport com.hippo.widget.recyclerview.AutoStaggeredGridLayoutManager\nimport com.hippo.yorozuya.ViewUtils\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport rikka.core.res.resolveColor\n\n@SuppressLint(\"NotifyDataSetChanged\")\nclass HistoryScene : ToolbarScene() {\n    private var mRecyclerView: EasyRecyclerView? = null\n    private val mAdapter: HistoryAdapter by lazy {\n        HistoryAdapter(object : DiffUtil.ItemCallback<HistoryInfo>() {\n            override fun areItemsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean = oldItem.gid == newItem.gid\n\n            override fun areContentsTheSame(oldItem: HistoryInfo, newItem: HistoryInfo): Boolean = oldItem.gid == newItem.gid\n        })\n    }\n    private val mDownloadManager = DownloadManager\n    private val mDownloadInfoListener: DownloadInfoListener by lazy {\n        object : DownloadInfoListener {\n            override fun onAdd(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n                mAdapter.notifyDataSetChanged()\n            }\n\n            override fun onUpdate(info: DownloadInfo, list: List<DownloadInfo>) {}\n            override fun onUpdateAll() {}\n            override fun onReload() {\n                mAdapter.notifyDataSetChanged()\n            }\n\n            override fun onChange() {\n                mAdapter.notifyDataSetChanged()\n            }\n\n            override fun onRenameLabel(from: String, to: String) {}\n            override fun onRemove(info: DownloadInfo, list: List<DownloadInfo>, position: Int) {\n                mAdapter.notifyDataSetChanged()\n            }\n\n            override fun onUpdateLabels() {}\n        }\n    }\n    private val mFavouriteStatusRouter = EhApplication.favouriteStatusRouter\n    private val mFavouriteStatusRouterListener: FavouriteStatusRouter.Listener by lazy {\n        FavouriteStatusRouter.Listener { _: Long, _: Int ->\n            mAdapter.notifyDataSetChanged()\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mDownloadManager.removeDownloadInfoListener(mDownloadInfoListener)\n        mFavouriteStatusRouter.removeListener(mFavouriteStatusRouterListener)\n    }\n\n    override fun getNavCheckedItem(): Int = R.id.nav_history\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        mDownloadManager.addDownloadInfoListener(mDownloadInfoListener)\n        mFavouriteStatusRouter.addListener(mFavouriteStatusRouterListener)\n    }\n\n    override fun onCreateViewWithToolbar(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? {\n        val view = inflater.inflate(R.layout.scene_history, container, false)\n        val content = ViewUtils.`$$`(view, R.id.content)\n        val recyclerView = ViewUtils.`$$`(content, R.id.recycler_view) as EasyRecyclerView\n        val mFastScroller = ViewUtils.`$$`(content, R.id.fast_scroller) as FastScroller\n        val mTip = ViewUtils.`$$`(view, R.id.tip) as TextView\n        val mViewTransition = ViewTransition(content, mTip)\n        val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_history)\n        drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n        mTip.setCompoundDrawables(null, drawable, null, null)\n        val historyData = Pager(\n            PagingConfig(20),\n        ) {\n            // DB Actions\n            EhDB.historyLazyList\n        }.flow.cachedIn(viewLifecycleOwner.lifecycleScope)\n        recyclerView.adapter = mAdapter\n        val layoutManager = AutoStaggeredGridLayoutManager(\n            0,\n            StaggeredGridLayoutManager.VERTICAL,\n        )\n        layoutManager.setColumnSize(Settings.detailSize)\n        layoutManager.setStrategy(AutoStaggeredGridLayoutManager.STRATEGY_MIN_SIZE)\n        recyclerView.layoutManager = layoutManager\n        recyclerView.clipToPadding = false\n        recyclerView.clipChildren = false\n        val itemTouchHelper = ItemTouchHelper(HistoryItemTouchHelperCallback())\n        itemTouchHelper.attachToRecyclerView(recyclerView)\n        mFastScroller.attachToRecyclerView(recyclerView)\n        mRecyclerView = recyclerView\n        val handlerDrawable = HandlerDrawable()\n        handlerDrawable.setColor(theme.resolveColor(R.attr.widgetColorThemeAccent))\n        mFastScroller.setHandlerDrawable(handlerDrawable)\n        viewLifecycleOwner.lifecycleScope.launch {\n            historyData.collectLatest { value ->\n                mAdapter.submitData(\n                    value,\n                )\n            }\n        }\n        viewLifecycleOwner.lifecycleScope.launch {\n            mAdapter.onPagesUpdatedFlow.collectLatest {\n                if (mAdapter.itemCount == 0) {\n                    mViewTransition.showView(1, true)\n                } else {\n                    mViewTransition.showView(0, true)\n                }\n            }\n        }\n        return view\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        setTitle(R.string.history)\n        setNavigationIcon(R.drawable.ic_baseline_menu_24)\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mRecyclerView?.stopScroll()\n        mRecyclerView = null\n    }\n\n    @SuppressLint(\"RtlHardcoded\")\n    override fun onNavigationClick() {\n        toggleDrawer(GravityCompat.START)\n    }\n\n    override fun getMenuResId(): Int = R.menu.scene_history\n\n    private fun showClearAllDialog() {\n        AlertDialog.Builder(requireContext())\n            .setMessage(R.string.clear_all_history)\n            .setPositiveButton(R.string.clear_all) { _: DialogInterface?, which: Int ->\n                if (DialogInterface.BUTTON_POSITIVE != which) {\n                    return@setPositiveButton\n                }\n                lifecycleScope.launchIO {\n                    // DB Actions\n                    EhDB.clearHistoryInfo()\n                    withUIContext {\n                        mAdapter.refresh()\n                    }\n                }\n            }\n            .setNegativeButton(android.R.string.cancel, null)\n            .show()\n    }\n\n    override fun onMenuItemClick(item: MenuItem): Boolean {\n        val id = item.itemId\n        if (id == R.id.action_clear_all) {\n            showClearAllDialog()\n            return true\n        }\n        return false\n    }\n\n    fun onItemClick(view: View, gi: GalleryInfo): Boolean {\n        val args = Bundle()\n        args.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GALLERY_INFO)\n        args.putParcelable(GalleryDetailScene.KEY_GALLERY_INFO, gi)\n        val announcer = Announcer(GalleryDetailScene::class.java).setArgs(args)\n        val thumb: View? = view.findViewById(R.id.thumb)\n        thumb?.let { announcer.setTranHelper(EnterGalleryDetailTransaction(thumb)) }\n        startScene(announcer)\n        return true\n    }\n\n    fun onItemLongClick(gi: GalleryInfo): Boolean {\n        val context = requireContext()\n        val activity = mainActivity ?: return false\n        val downloaded = mDownloadManager.getDownloadState(gi.gid) != DownloadInfo.STATE_INVALID\n        val favourited = gi.favoriteSlot != -2\n        val items = if (downloaded) {\n            arrayOf<CharSequence>(\n                context.getString(R.string.read),\n                context.getString(R.string.delete_downloads),\n                context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),\n                context.getString(R.string.delete),\n                context.getString(R.string.download_move_dialog_title),\n            )\n        } else {\n            arrayOf<CharSequence>(\n                context.getString(R.string.read),\n                context.getString(R.string.download),\n                context.getString(if (favourited) R.string.remove_from_favourites else R.string.add_to_favourites),\n                context.getString(R.string.delete),\n            )\n        }\n        val icons = if (downloaded) {\n            intArrayOf(\n                R.drawable.v_book_open_x24,\n                R.drawable.v_delete_x24,\n                if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,\n                R.drawable.v_delete_x24,\n                R.drawable.v_folder_move_x24,\n            )\n        } else {\n            intArrayOf(\n                R.drawable.v_book_open_x24,\n                R.drawable.v_download_x24,\n                if (favourited) R.drawable.v_heart_broken_x24 else R.drawable.v_heart_x24,\n                R.drawable.v_delete_x24,\n            )\n        }\n        AlertDialog.Builder(context)\n            .setTitle(EhUtils.getSuitableTitle(gi))\n            .setAdapter(\n                SelectItemWithIconAdapter(\n                    context,\n                    items,\n                    icons,\n                ),\n            ) { _: DialogInterface?, which: Int ->\n                when (which) {\n                    0 -> {\n                        val intent = Intent(activity, GalleryActivity::class.java)\n                        intent.action = GalleryActivity.ACTION_EH\n                        intent.putExtra(GalleryActivity.KEY_GALLERY_INFO, gi)\n                        startActivity(intent)\n                    }\n                    1 -> if (downloaded) {\n                        AlertDialog.Builder(context)\n                            .setTitle(R.string.download_remove_dialog_title)\n                            .setMessage(\n                                getString(\n                                    R.string.download_remove_dialog_message,\n                                    gi.title,\n                                ),\n                            )\n                            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->\n                                // DownloadManager Actions\n                                mDownloadManager.deleteDownload(\n                                    gi.gid,\n                                )\n                            }\n                            .show()\n                    } else {\n                        // CommonOperations Actions\n                        CommonOperations.startDownload(activity, gi, false)\n                    }\n                    2 -> if (favourited) {\n                        // CommonOperations Actions\n                        CommonOperations.removeFromFavorites(\n                            activity,\n                            gi,\n                            RemoveFromFavoriteListener(context),\n                        )\n                    } else {\n                        // CommonOperations Actions\n                        CommonOperations.addToFavorites(\n                            activity,\n                            gi,\n                            AddToFavoriteListener(context),\n                            false,\n                        )\n                    }\n                    3 -> {\n                        lifecycleScope.launchIO {\n                            val hi: HistoryInfo? = gi as? HistoryInfo\n                            // DB Actions\n                            hi?.let { EhDB.deleteHistoryInfo(hi) }\n                            withUIContext {\n                                mAdapter.refresh()\n                            }\n                        }\n                    }\n                    4 -> {\n                        val labelRawList = mDownloadManager.labelList\n                        val labelList: MutableList<String> = ArrayList(labelRawList.size + 1)\n                        labelList.add(getString(R.string.default_download_label_name))\n                        var i = 0\n                        val n = labelRawList.size\n                        while (i < n) {\n                            labelRawList[i].label?.let { labelList.add(it) }\n                            i++\n                        }\n                        val labels = labelList.toTypedArray()\n                        val helper = MoveDialogHelper(labels, gi)\n                        AlertDialog.Builder(context)\n                            .setTitle(R.string.download_move_dialog_title)\n                            .setItems(labels, helper)\n                            .show()\n                    }\n                }\n            }.show()\n        return true\n    }\n\n    private class AddToFavoriteListener(context: Context) : EhCallback<GalleryListScene?, Unit>(context) {\n        override fun onSuccess(result: Unit) {\n            showTip(R.string.add_to_favorite_success, LENGTH_SHORT)\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(R.string.add_to_favorite_failure, LENGTH_LONG)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private class RemoveFromFavoriteListener(context: Context) : EhCallback<GalleryListScene?, Unit>(context) {\n        override fun onSuccess(result: Unit) {\n            showTip(R.string.remove_from_favorite_success, LENGTH_SHORT)\n        }\n\n        override fun onFailure(e: Exception) {\n            showTip(R.string.remove_from_favorite_failure, LENGTH_LONG)\n        }\n\n        override fun onCancel() {}\n    }\n\n    private class HistoryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        val card: View = itemView.findViewById(R.id.card)\n        val thumb: LoadImageView = itemView.findViewById(R.id.thumb)\n        val title: TextView = itemView.findViewById(R.id.title)\n        val uploader: TextView = itemView.findViewById(R.id.uploader)\n        val rating: SimpleRatingView = itemView.findViewById(R.id.rating)\n        val category: TextView = itemView.findViewById(R.id.category)\n        val posted: TextView = itemView.findViewById(R.id.posted)\n        val simpleLanguage: TextView = itemView.findViewById(R.id.simple_language)\n        val pages: TextView = itemView.findViewById(R.id.pages)\n        val downloaded: ImageView = itemView.findViewById(R.id.downloaded)\n        val favourited: ImageView = itemView.findViewById(R.id.favourited)\n    }\n\n    private inner class MoveDialogHelper(\n        private val mLabels: Array<String>,\n        private val mGi: GalleryInfo,\n    ) : DialogInterface.OnClickListener {\n        override fun onClick(dialog: DialogInterface, which: Int) {\n            val downloadInfo = mDownloadManager.getDownloadInfo(mGi.gid) ?: return\n            val label = if (which == 0) null else mLabels[which]\n            // DownloadManager Actions\n            mDownloadManager.changeLabel(listOf(downloadInfo), label)\n        }\n    }\n\n    private inner class HistoryAdapter(diffCallback: DiffUtil.ItemCallback<HistoryInfo>) : PagingDataAdapter<HistoryInfo, HistoryHolder>(diffCallback) {\n        private val mInflater: LayoutInflater = layoutInflater\n        private val mListThumbWidth: Int\n        private val mListThumbHeight: Int\n\n        init {\n            @SuppressLint(\"InflateParams\")\n            val calculator =\n                mInflater.inflate(R.layout.item_gallery_list_thumb_height, null)\n            ViewUtils.measureView(calculator, 1024, ViewGroup.LayoutParams.WRAP_CONTENT)\n            mListThumbHeight = calculator.measuredHeight\n            mListThumbWidth = mListThumbHeight * 2 / 3\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryHolder {\n            val holder = HistoryHolder(mInflater.inflate(R.layout.item_history, parent, false))\n            val lp = holder.thumb.layoutParams\n            lp.width = mListThumbWidth\n            lp.height = mListThumbHeight\n            holder.thumb.layoutParams = lp\n            return holder\n        }\n\n        override fun onBindViewHolder(holder: HistoryHolder, position: Int) {\n            val gi: GalleryInfo? = getItem(position)\n            gi ?: return\n            gi.thumb?.let {\n                holder.thumb.load(getThumbKey(gi.gid), gi.thumbUrl!!, hardware = false)\n            }\n            holder.title.text = EhUtils.getSuitableTitle(gi)\n            holder.uploader.text = gi.uploader\n            holder.rating.rating = gi.rating\n            val category = holder.category\n            val newCategoryText = EhUtils.getCategory(gi.category)\n            if (!newCategoryText.contentEquals(category.text)) {\n                category.text = newCategoryText\n                category.setBackgroundColor(EhUtils.getCategoryColor(gi.category))\n            }\n            holder.posted.text = gi.posted\n            holder.pages.text = null\n            holder.pages.visibility = View.GONE\n            if (TextUtils.isEmpty(gi.simpleLanguage)) {\n                holder.simpleLanguage.text = null\n                holder.simpleLanguage.visibility = View.GONE\n            } else {\n                holder.simpleLanguage.text = gi.simpleLanguage\n                holder.simpleLanguage.visibility = View.VISIBLE\n            }\n            holder.downloaded.visibility =\n                if (mDownloadManager.containDownloadInfo(gi.gid)) View.VISIBLE else View.GONE\n            holder.favourited.visibility =\n                if (gi.favoriteSlot != -2) View.VISIBLE else View.GONE\n            // Update transition name\n            ViewCompat.setTransitionName(\n                holder.thumb,\n                TransitionNameFactory.getThumbTransitionName(gi.gid),\n            )\n            holder.card.setOnClickListener { onItemClick(holder.itemView, gi) }\n            holder.card.setOnLongClickListener { onItemLongClick(gi) }\n        }\n    }\n\n    private inner class HistoryItemTouchHelperCallback : ItemTouchHelper.Callback() {\n        override fun getMovementFlags(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n        ): Int = makeMovementFlags(0, ItemTouchHelper.LEFT)\n\n        override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float = 0.3f\n\n        override fun onMove(\n            recyclerView: RecyclerView,\n            viewHolder: RecyclerView.ViewHolder,\n            target: RecyclerView.ViewHolder,\n        ): Boolean = false\n\n        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {\n            val mPosition = viewHolder.bindingAdapterPosition\n            lifecycleScope.launchIO {\n                val info: HistoryInfo? = mAdapter.peek(mPosition)\n                // DB Actions\n                info?.let { EhDB.deleteHistoryInfo(info) }\n                withUIContext {\n                    mAdapter.refresh()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/ProgressScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TextView\nimport androidx.core.content.ContextCompat\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhClient\nimport com.hippo.ehviewer.client.EhRequest\nimport com.hippo.scene.Announcer\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.view.ViewTransition\nimport com.hippo.yorozuya.ViewUtils\nimport kotlinx.coroutines.DelicateCoroutinesApi\n\n/**\n * Only show a progress with jobs in background\n */\nclass ProgressScene :\n    BaseScene(),\n    View.OnClickListener {\n    private var mValid = false\n    private var mError: String? = null\n    private var mAction: String? = null\n    private var mGid: Long = 0\n    private var mPToken: String? = null\n    private var mPage = 0\n    private var mTip: TextView? = null\n    private var mViewTransition: ViewTransition? = null\n\n    override fun needShowLeftDrawer(): Boolean = false\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (savedInstanceState == null) {\n            onInit()\n        } else {\n            onRestore(savedInstanceState)\n        }\n    }\n\n    @OptIn(DelicateCoroutinesApi::class)\n    private fun doJobs(): Boolean {\n        val context = context\n        val activity = mainActivity\n        if (null == context || null == activity) {\n            return false\n        }\n        if (ACTION_GALLERY_TOKEN == mAction) {\n            if (mGid == -1L || mPToken == null || mPage == -1) {\n                return false\n            }\n            val request = EhRequest()\n                .setMethod(EhClient.METHOD_GET_GALLERY_TOKEN)\n                .setArgs(mGid, mPToken!!, mPage)\n                .setCallback(\n                    GetGalleryTokenListener(context),\n                )\n            request.enqueue()\n            return true\n        }\n        return false\n    }\n\n    private fun handleArgs(args: Bundle?): Boolean {\n        if (args == null) {\n            return false\n        }\n        mAction = args.getString(KEY_ACTION)\n        if (ACTION_GALLERY_TOKEN == mAction) {\n            mGid = args.getLong(KEY_GID, -1)\n            mPToken = args.getString(KEY_PTOKEN, null)\n            mPage = args.getInt(KEY_PAGE, -1)\n            return mGid != -1L && mPToken != null && mPage != -1\n        }\n        return false\n    }\n\n    private fun onInit() {\n        mValid = handleArgs(arguments)\n        if (mValid) {\n            mValid = doJobs()\n        }\n        if (!mValid) {\n            mError = getString(R.string.error_something_wrong_happened)\n        }\n    }\n\n    private fun onRestore(savedInstanceState: Bundle) {\n        mValid = savedInstanceState.getBoolean(KEY_VALID)\n        mError = savedInstanceState.getString(KEY_ERROR)\n        mAction = savedInstanceState.getString(KEY_ACTION)\n        mGid = savedInstanceState.getLong(KEY_GID, -1)\n        mPToken = savedInstanceState.getString(KEY_PTOKEN, null)\n        mPage = savedInstanceState.getInt(KEY_PAGE, -1)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        outState.putBoolean(KEY_VALID, mValid)\n        outState.putString(KEY_ERROR, mError)\n        outState.putString(KEY_ACTION, mAction)\n        outState.putLong(KEY_GID, mGid)\n        outState.putString(KEY_PTOKEN, mPToken)\n        outState.putInt(KEY_PAGE, mPage)\n    }\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? {\n        val view = inflater.inflate(R.layout.scene_progress, container, false)\n        val progress = ViewUtils.`$$`(view, R.id.progress)\n        mTip = ViewUtils.`$$`(view, R.id.tip) as TextView\n        val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.big_sad_pandroid)\n        drawable!!.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n        mTip!!.setCompoundDrawables(null, drawable, null, null)\n        mTip!!.setOnClickListener(this)\n        mTip!!.text = mError\n        mViewTransition = ViewTransition(progress, mTip)\n        if (mValid) {\n            mViewTransition!!.showView(0, false)\n        } else {\n            mViewTransition!!.showView(1, false)\n        }\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mTip = null\n        mViewTransition = null\n    }\n\n    override fun onClick(v: View) {\n        if (mTip === v) {\n            if (doJobs()) {\n                mValid = true\n                // Show progress\n                if (null != mViewTransition) {\n                    mViewTransition!!.showView(0, true)\n                }\n            }\n        }\n    }\n\n    private fun onGetGalleryTokenSuccess(result: String) {\n        val arg = Bundle()\n        arg.putString(GalleryDetailScene.KEY_ACTION, GalleryDetailScene.ACTION_GID_TOKEN)\n        arg.putLong(GalleryDetailScene.KEY_GID, mGid)\n        arg.putString(GalleryDetailScene.KEY_TOKEN, result)\n        arg.putInt(GalleryDetailScene.KEY_PAGE, mPage)\n        startScene(Announcer(GalleryDetailScene::class.java).setArgs(arg))\n        finish()\n    }\n\n    private fun onGetGalleryTokenFailure(e: Exception) {\n        mValid = false\n        val context = context\n        if (null != context && null != mViewTransition && null != mTip) {\n            // Show tip\n            mError = ExceptionUtils.getReadableString(e)\n            mViewTransition!!.showView(1)\n            mTip!!.text = mError\n        }\n    }\n\n    private inner class GetGalleryTokenListener(\n        context: Context,\n    ) : EhCallback<ProgressScene, String>(context) {\n        override fun onSuccess(result: String) {\n            val scene = this@ProgressScene\n            scene.onGetGalleryTokenSuccess(result)\n        }\n\n        override fun onFailure(e: Exception) {\n            val scene = this@ProgressScene\n            scene.onGetGalleryTokenFailure(e)\n        }\n\n        override fun onCancel() {}\n    }\n\n    companion object {\n        const val KEY_ACTION = \"action\"\n        const val ACTION_GALLERY_TOKEN = \"gallery_token\"\n        const val KEY_GID = \"gid\"\n        const val KEY_PTOKEN = \"ptoken\"\n        const val KEY_PAGE = \"page\"\n        private const val KEY_VALID = \"valid\"\n        private const val KEY_ERROR = \"error\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/SecurityScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.content.Context\nimport android.hardware.Sensor\nimport android.hardware.SensorManager\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport androidx.biometric.BiometricManager\nimport androidx.biometric.BiometricPrompt\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.widget.lockpattern.LockPatternUtils\nimport com.hippo.widget.lockpattern.LockPatternView\nimport com.hippo.widget.lockpattern.LockPatternView.OnPatternListener\nimport com.hippo.yorozuya.ObjectUtils\nimport com.hippo.yorozuya.ViewUtils\nimport java.util.concurrent.Executors\n\nclass SecurityScene :\n    SolidScene(),\n    OnPatternListener {\n    private var mPatternView: LockPatternView? = null\n    private var mSensorManager: SensorManager? = null\n    private var mAccelerometer: Sensor? = null\n    private var promptInfo: BiometricPrompt.PromptInfo? = null\n    private var biometricPrompt: BiometricPrompt? = null\n    private var canAuthenticate = false\n    private var mRetryTimes = 0\n\n    override fun needShowLeftDrawer(): Boolean = false\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        val context = requireContext()\n        mSensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager\n        mAccelerometer = mSensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)\n\n        mRetryTimes = savedInstanceState?.getInt(KEY_RETRY_TIMES) ?: MAX_RETRY_TIMES\n\n        canAuthenticate = Settings.enableFingerprint &&\n            BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) == BiometricManager.BIOMETRIC_SUCCESS\n        biometricPrompt = BiometricPrompt(\n            this,\n            Executors.newSingleThreadExecutor(),\n            object : BiometricPrompt.AuthenticationCallback() {\n                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {\n                    startSceneForCheckStep(CHECK_STEP_SECURITY, arguments)\n                    finish()\n                }\n            },\n        )\n        promptInfo = BiometricPrompt.PromptInfo.Builder()\n            .setTitle(getString(R.string.app_name))\n            .setNegativeButtonText(getString(android.R.string.cancel))\n            .setConfirmationRequired(false)\n            .build()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        mSensorManager = null\n        mAccelerometer = null\n    }\n\n    override fun onResume() {\n        super.onResume()\n        if (canAuthenticate) {\n            startBiometricPrompt()\n        }\n    }\n\n    private fun startBiometricPrompt() {\n        biometricPrompt!!.authenticate(promptInfo!!)\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        outState.putInt(KEY_RETRY_TIMES, mRetryTimes)\n    }\n\n    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {\n        val view = inflater.inflate(R.layout.scene_security, container, false)\n        if (canAuthenticate) {\n            view.setOnClickListener { startBiometricPrompt() }\n        }\n        mPatternView = ViewUtils.`$$`(view, R.id.pattern_view) as LockPatternView\n        mPatternView!!.setOnPatternListener(this)\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mPatternView = null\n    }\n\n    override fun onPatternStart() {}\n    override fun onPatternCleared() {}\n    override fun onPatternCellAdded(pattern: List<LockPatternView.Cell>) {}\n\n    override fun onPatternDetected(pattern: List<LockPatternView.Cell>) {\n        val activity = mainActivity\n        if (null == activity || null == mPatternView) {\n            return\n        }\n        val enteredPatter = LockPatternUtils.patternToString(pattern)\n        val targetPatter = Settings.security\n        if (ObjectUtils.equal(enteredPatter, targetPatter)) {\n            startSceneForCheckStep(CHECK_STEP_SECURITY, arguments)\n            finish()\n        } else {\n            mPatternView!!.setDisplayMode(LockPatternView.DisplayMode.Wrong)\n            mRetryTimes--\n            if (mRetryTimes <= 0) {\n                finish()\n            }\n        }\n    }\n\n    companion object {\n        private const val KEY_RETRY_TIMES = \"retry_times\"\n        private const val MAX_RETRY_TIMES = 5\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/SelectSiteScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport com.google.android.material.button.MaterialButton\nimport com.google.android.material.button.MaterialButtonToggleGroup\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.yorozuya.ViewUtils\n\nclass SelectSiteScene :\n    SolidScene(),\n    View.OnClickListener {\n    private var mButtonGroup: MaterialButtonToggleGroup? = null\n    private var mOk: View? = null\n\n    override fun needShowLeftDrawer(): Boolean = false\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_select_site, container, false)\n        mButtonGroup = ViewUtils.`$$`(view, R.id.button_group) as MaterialButtonToggleGroup\n        (ViewUtils.`$$`(view, R.id.site_ex) as MaterialButton).isChecked = true\n        mOk = ViewUtils.`$$`(view, R.id.ok)\n        mOk!!.setOnClickListener(this)\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mButtonGroup = null\n        mOk = null\n    }\n\n    override fun onClick(v: View) {\n        val id = mButtonGroup?.checkedButtonId ?: return\n        if (v == mOk) {\n            Settings.putSelectSite(false)\n            Settings.putGallerySite(if (id == R.id.site_ex) EhUrl.SITE_EX else EhUrl.SITE_E)\n            startSceneForCheckStep(CHECK_STEP_SELECT_SITE, arguments)\n            finish()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/SignInScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.graphics.Paint\nimport android.os.Bundle\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.inputmethod.EditorInfo\nimport android.widget.EditText\nimport android.widget.TextView\nimport android.widget.TextView.OnEditorActionListener\nimport androidx.appcompat.app.AlertDialog\nimport androidx.lifecycle.lifecycleScope\nimport com.google.android.material.textfield.TextInputLayout\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.UrlOpener\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhEngine\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.scene.Announcer\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport com.hippo.yorozuya.ViewUtils\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\n\nclass SignInScene :\n    SolidScene(),\n    OnEditorActionListener,\n    View.OnClickListener {\n    private var mProgress: View? = null\n    private var mUsernameLayout: TextInputLayout? = null\n    private var mPasswordLayout: TextInputLayout? = null\n    private var mUsername: EditText? = null\n    private var mPassword: EditText? = null\n    private var mRegister: View? = null\n    private var mSignIn: View? = null\n    private var mSignInViaWebView: TextView? = null\n    private var mSignInViaCookies: TextView? = null\n    private var mSkipSigningIn: TextView? = null\n    private var mSignInJob: Job? = null\n\n    override fun needShowLeftDrawer(): Boolean = false\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        val view = inflater.inflate(R.layout.scene_login, container, false)\n        val loginForm = ViewUtils.`$$`(view, R.id.login_form)\n        mProgress = ViewUtils.`$$`(view, R.id.progress)\n        mUsernameLayout = ViewUtils.`$$`(loginForm, R.id.username_layout) as TextInputLayout\n        mUsername = mUsernameLayout!!.editText!!\n        mPasswordLayout = ViewUtils.`$$`(loginForm, R.id.password_layout) as TextInputLayout\n        mPassword = mPasswordLayout!!.editText!!\n        mRegister = ViewUtils.`$$`(loginForm, R.id.register)\n        mSignIn = ViewUtils.`$$`(loginForm, R.id.sign_in)\n        mSignInViaWebView = ViewUtils.`$$`(loginForm, R.id.sign_in_via_webview) as TextView\n        mSignInViaCookies = ViewUtils.`$$`(loginForm, R.id.sign_in_via_cookies) as TextView\n        mSkipSigningIn = ViewUtils.`$$`(loginForm, R.id.tourist_mode) as TextView\n        mSignInViaWebView!!.run {\n            paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG or Paint.ANTI_ALIAS_FLAG\n        }\n        mSignInViaCookies!!.run {\n            paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG or Paint.ANTI_ALIAS_FLAG\n        }\n        mSkipSigningIn!!.run {\n            paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG or Paint.ANTI_ALIAS_FLAG\n        }\n        mPassword!!.setOnEditorActionListener(this)\n        mRegister!!.setOnClickListener(this)\n        mSignIn!!.setOnClickListener(this)\n        mSignInViaWebView!!.setOnClickListener(this)\n        mSignInViaCookies!!.setOnClickListener(this)\n        mSkipSigningIn!!.setOnClickListener(this)\n        return view\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mProgress = null\n        mUsernameLayout = null\n        mPasswordLayout = null\n        mUsername = null\n        mPassword = null\n        mRegister = null\n        mSignIn = null\n        mSignInViaWebView = null\n        mSignInViaCookies = null\n        mSkipSigningIn = null\n        mSignInJob = null\n    }\n\n    private fun showProgress() {\n        if (mProgress?.visibility == View.VISIBLE) return\n        mProgress?.apply {\n            alpha = 0f\n            visibility = View.VISIBLE\n            animate().alpha(1f).setDuration(500).start()\n        }\n    }\n\n    private fun hideProgress() {\n        mProgress?.visibility = View.GONE\n    }\n\n    override fun onSceneResult(requestCode: Int, resultCode: Int, data: Bundle?) {\n        when (requestCode) {\n            REQUEST_CODE_WEBVIEW -> if (resultCode == RESULT_OK) {\n                getProfile()\n            }\n            REQUEST_CODE_COOKIE -> if (resultCode == RESULT_OK) {\n                finishSignIn()\n            }\n            else -> super.onSceneResult(requestCode, resultCode, data)\n        }\n    }\n\n    override fun onClick(v: View) {\n        val activity = mainActivity ?: return\n        when (v) {\n            mRegister ->\n                UrlOpener.openUrl(activity, EhUrl.URL_REGISTER, false)\n            mSignIn ->\n                signIn()\n            mSignInViaCookies ->\n                startScene(Announcer(CookieSignInScene::class.java).setRequestCode(this, REQUEST_CODE_COOKIE))\n            mSignInViaWebView ->\n                startScene(Announcer(WebViewSignInScene::class.java).setRequestCode(this, REQUEST_CODE_WEBVIEW))\n            mSkipSigningIn -> {\n                lifecycleScope.launchIO {\n                    EhUtils.signOut()\n                    // Set gallery size SITE_E if skip sign in\n                    Settings.putGallerySite(EhUrl.SITE_E)\n                    Settings.putSelectSite(false)\n                    finishSignIn(false)\n                }\n            }\n        }\n    }\n\n    override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {\n        if (v == mPassword) {\n            if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {\n                signIn()\n                return true\n            }\n        }\n        return false\n    }\n\n    private fun signIn() {\n        if (mSignInJob?.isActive == true ||\n            mUsername == null ||\n            mPassword == null ||\n            mUsernameLayout == null ||\n            mPasswordLayout == null\n        ) {\n            return\n        }\n        val username = mUsername!!.text.toString()\n        val password = mPassword!!.text.toString()\n        if (username.isEmpty()) {\n            mUsernameLayout!!.error = getString(R.string.error_username_cannot_empty)\n            return\n        } else {\n            mUsernameLayout!!.error = null\n        }\n        if (password.isEmpty()) {\n            mPasswordLayout!!.error = getString(R.string.error_password_cannot_empty)\n            return\n        } else {\n            mPasswordLayout!!.error = null\n        }\n        hideSoftInput()\n        showProgress()\n        mSignInJob = viewLifecycleOwner.lifecycleScope.launchIO {\n            EhUtils.signOut()\n            runCatching {\n                EhEngine.signIn(username, password)\n            }.onFailure {\n                withUIContext {\n                    hideProgress()\n                    showResultErrorDialog(it)\n                }\n            }.onSuccess {\n                getProfile()\n            }\n        }\n    }\n\n    private fun getProfile() {\n        lifecycleScope.launchIO {\n            withUIContext {\n                showProgress()\n            }\n            runCatching {\n                EhEngine.getProfile().run {\n                    Settings.putDisplayName(displayName)\n                    Settings.putAvatar(avatar)\n                }\n            }.onFailure {\n                withUIContext {\n                    hideProgress()\n                    showResultErrorDialog(it)\n                }\n            }.onSuccess {\n                finishSignIn()\n            }\n        }\n    }\n\n    private fun finishSignIn(signedIn: Boolean = true) {\n        lifecycleScope.launchIO {\n            withUIContext {\n                showProgress()\n            }\n            if (signedIn) {\n                runCatching {\n                    // For the `star` cookie, https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873\n                    EhEngine.getNews(false)\n                    EhCookieStore.copyCookie(EhUrl.DOMAIN_E, EhUrl.DOMAIN_EX, EhCookieStore.KEY_STAR)\n\n                    // Get cookies for image limits\n                    launch { runCatching { EhEngine.getUConfig(EhUrl.URL_UCONFIG_E) } }\n\n                    // Sad panda check\n                    EhEngine.getUConfig(EhUrl.URL_UCONFIG_EX)\n                }.onFailure {\n                    Settings.putGallerySite(EhUrl.SITE_E)\n                    Settings.putSelectSite(false)\n                }\n            }\n            withUIContext {\n                Settings.putNeedSignIn(false)\n                updateAvatar()\n                if (null != mainActivity) {\n                    startSceneForCheckStep(CHECK_STEP_SIGN_IN, arguments)\n                }\n                finish()\n            }\n        }\n    }\n\n    private fun showResultErrorDialog(e: Throwable) {\n        AlertDialog.Builder(requireContext())\n            .setTitle(R.string.sign_in_failed)\n            .setMessage(\"${ExceptionUtils.getReadableString(e)}\\n\\n${getString(R.string.sign_in_failed_tip)}\")\n            .setPositiveButton(R.string.get_it, null)\n            .show()\n    }\n\n    companion object {\n        private const val REQUEST_CODE_COOKIE = 0\n        private const val REQUEST_CODE_WEBVIEW = 1\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/SolidScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.os.Bundle\nimport android.util.Log\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.scene.Announcer\n\n/**\n * Scene for safety, can't be covered\n */\nopen class SolidScene : BaseScene() {\n    fun startSceneForCheckStep(checkStep: Int, args: Bundle?) {\n        when (checkStep) {\n            CHECK_STEP_SECURITY -> {\n                if (EhUtils.needSignedIn()) {\n                    startScene(Announcer(SignInScene::class.java).setArgs(args), true)\n                } else {\n                    startSceneForCheckStep(CHECK_STEP_SIGN_IN, args)\n                }\n            }\n            CHECK_STEP_SIGN_IN -> {\n                if (Settings.selectSite) {\n                    startScene(Announcer(SelectSiteScene::class.java).setArgs(args), true)\n                } else {\n                    startSceneForCheckStep(CHECK_STEP_SELECT_SITE, args)\n                }\n            }\n            CHECK_STEP_SELECT_SITE -> {\n                var targetScene: String? = null\n                var targetArgs: Bundle? = null\n                if (null != args) {\n                    targetScene = args.getString(KEY_TARGET_SCENE)\n                    targetArgs = args.getBundle(KEY_TARGET_ARGS)\n                }\n                var clazz: Class<*>? = null\n                if (targetScene != null) {\n                    try {\n                        clazz = Class.forName(targetScene)\n                    } catch (_: ClassNotFoundException) {\n                        Log.e(TAG, \"Can't find class with name: $targetScene\")\n                    }\n                }\n                if (clazz != null) {\n                    startScene(Announcer(clazz).setArgs(targetArgs))\n                } else {\n                    val newArgs = Bundle()\n                    newArgs.putString(GalleryListScene.KEY_ACTION, Settings.launchPageGalleryListSceneAction)\n                    startScene(Announcer(GalleryListScene::class.java).setArgs(newArgs))\n                }\n            }\n        }\n    }\n\n    companion object {\n        const val CHECK_STEP_SECURITY = 0\n        const val CHECK_STEP_SIGN_IN = 1\n        const val CHECK_STEP_SELECT_SITE = 2\n        const val KEY_TARGET_SCENE = \"target_scene\"\n        const val KEY_TARGET_ARGS = \"target_args\"\n        private val TAG = SolidScene::class.java.simpleName\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/ToolbarScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.MenuItem\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.appcompat.widget.Toolbar\nimport com.hippo.ehviewer.R\n\nabstract class ToolbarScene : BaseScene() {\n    private var mToolbar: Toolbar? = null\n    private var mTempTitle: CharSequence? = null\n\n    open fun onCreateViewWithToolbar(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? = null\n\n    override var needWhiteStatusBar = false\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View? {\n        val view = inflater.inflate(R.layout.scene_toolbar, container, false)\n        val toolbar = view.findViewById<Toolbar>(R.id.toolbar)\n        val contentPanel = view.findViewById<FrameLayout>(R.id.content_panel)\n        val contentView = onCreateViewWithToolbar(inflater, contentPanel, savedInstanceState)\n        return if (contentView == null) {\n            null\n        } else {\n            mToolbar = toolbar\n            contentPanel.addView(contentView, 0)\n            view\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mToolbar = null\n    }\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        mToolbar?.apply {\n            mTempTitle?.let { title = it }\n            val menuResId = getMenuResId()\n            if (menuResId != 0) {\n                inflateMenu(menuResId)\n                setOnMenuItemClickListener { item: MenuItem -> onMenuItemClick(item) }\n            }\n            setNavigationOnClickListener { onNavigationClick() }\n        }\n    }\n\n    open fun getMenuResId(): Int = 0\n\n    open fun onMenuItemClick(item: MenuItem): Boolean = false\n\n    open fun onNavigationClick() {}\n\n    fun setNavigationIcon(@DrawableRes resId: Int) {\n        mToolbar?.setNavigationIcon(resId)\n    }\n\n    fun setTitle(@StringRes resId: Int) {\n        setTitle(getString(resId))\n    }\n\n    fun setTitle(title: CharSequence) {\n        mToolbar?.title = title\n        mTempTitle = title\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/TransitionNameFactory.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nobject TransitionNameFactory {\n    fun getThumbTransitionName(gid: Long): String = \"thumb:$gid\"\n\n    fun getTitleTransitionName(gid: Long): String = \"title:$gid\"\n\n    fun getUploaderTransitionName(gid: Long): String = \"uploader:$gid\"\n\n    fun getCategoryTransitionName(gid: Long): String = \"category:$gid\"\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/ui/scene/WebViewSignInScene.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.ui.scene\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.webkit.CookieManager\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.lifecycle.lifecycleScope\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.ehviewer.util.setDefaultSettings\nimport com.hippo.ehviewer.widget.DialogWebChromeClient\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport okhttp3.Cookie\nimport okhttp3.HttpUrl\nimport okhttp3.HttpUrl.Companion.toHttpUrlOrNull\nimport rikka.core.res.resolveColor\n\nclass WebViewSignInScene : SolidScene() {\n    private var mWebView: WebView? = null\n\n    override fun needShowLeftDrawer(): Boolean = false\n\n    override fun onCreateView(\n        inflater: LayoutInflater,\n        container: ViewGroup?,\n        savedInstanceState: Bundle?,\n    ): View {\n        // http://stackoverflow.com/questions/32284642/how-to-handle-an-uncatched-exception\n        CookieManager.getInstance().apply {\n            flush()\n            removeAllCookies(null)\n            removeSessionCookies(null)\n        }\n        return WebView(requireContext()).apply {\n            setBackgroundColor(theme.resolveColor(android.R.attr.colorBackground))\n            setDefaultSettings()\n            webViewClient = LoginWebViewClient()\n            webChromeClient = DialogWebChromeClient(context)\n            loadUrl(EhUrl.URL_SIGN_IN)\n            mWebView = this\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        mWebView?.destroy()\n        mWebView = null\n    }\n\n    private inner class LoginWebViewClient : WebViewClient() {\n        fun parseCookies(url: HttpUrl?, cookieStrings: String?): List<Cookie> {\n            if (cookieStrings == null) {\n                return emptyList()\n            }\n            var cookies: MutableList<Cookie>? = null\n            val pieces =\n                cookieStrings.split(\";\".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()\n            for (piece in pieces) {\n                val cookie = Cookie.parse(url!!, piece) ?: continue\n                if (cookies == null) {\n                    cookies = ArrayList()\n                }\n                cookies.add(cookie)\n            }\n            return cookies ?: emptyList()\n        }\n\n        private suspend fun addCookie(domain: String, cookie: Cookie) {\n            EhCookieStore.addCookie(\n                EhCookieStore.newCookie(\n                    cookie,\n                    domain,\n                    forcePersistent = true,\n                    forceLongLive = true,\n                    forceNotHostOnly = true,\n                ),\n            )\n        }\n\n        override fun onPageFinished(view: WebView, url: String) {\n            val httpUrl = url.toHttpUrlOrNull() ?: return\n            val cookieString = CookieManager.getInstance().getCookie(EhUrl.HOST_E)\n            val cookies = parseCookies(httpUrl, cookieString)\n            var getId = false\n            var getHash = false\n            for (cookie in cookies) {\n                if (EhCookieStore.KEY_IPB_MEMBER_ID == cookie.name) {\n                    getId = true\n                } else if (EhCookieStore.KEY_IPB_PASS_HASH == cookie.name) {\n                    getHash = true\n                }\n            }\n            if (getId && getHash) {\n                viewLifecycleOwner.lifecycleScope.launchIO {\n                    EhUtils.signOut()\n                    cookies.forEach {\n                        addCookie(EhUrl.DOMAIN_EX, it)\n                        addCookie(EhUrl.DOMAIN_E, it)\n                    }\n                    withUIContext {\n                        setResult(RESULT_OK, null)\n                        finish()\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/util/WebViewExtensions.kt",
    "content": "package com.hippo.ehviewer.util\n\nimport android.annotation.SuppressLint\nimport android.webkit.WebSettings\nimport android.webkit.WebView\nimport androidx.webkit.WebViewCompat\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.Settings\n\nprivate const val MINIMUM_WEBVIEW_VERSION = 118\nval WebViewVersion = run {\n    val context = EhApplication.application\n    val uaVersion = runCatching {\n        Regex(\"Chrome/(\\\\d+)\")\n            .find(WebSettings.getDefaultUserAgent(context))\n            ?.groupValues\n            ?.get(1)\n            ?.toIntOrNull()\n    }.getOrNull()\n    val pkgVersion = WebViewCompat\n        .getCurrentWebViewPackage(context)\n        ?.versionName\n        ?.substringBefore('.')\n        ?.toIntOrNull()\n    uaVersion ?: pkgVersion ?: MINIMUM_WEBVIEW_VERSION\n}\nval isWebViewOutdated = WebViewVersion < MINIMUM_WEBVIEW_VERSION\n\n@SuppressLint(\"SetJavaScriptEnabled\")\nfun WebView.setDefaultSettings() = settings.run {\n    builtInZoomControls = true\n    displayZoomControls = false\n    javaScriptEnabled = true\n    domStorageEnabled = true\n    userAgentString = Settings.userAgent!!\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/AdvanceSearchTable.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimport android.widget.CheckBox\nimport android.widget.EditText\nimport android.widget.LinearLayout\nimport android.widget.Spinner\nimport android.widget.TextView\nimport com.hippo.ehviewer.R\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.yorozuya.NumberUtils\n\nclass AdvanceSearchTable @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : LinearLayout(context, attrs) {\n    private var mSh: CheckBox\n    private var mSto: CheckBox\n    private var mSr: CheckBox\n    private var mMinRating: Spinner\n    private var mSp: CheckBox\n    private var mSpf: EditText\n    private var mSpt: EditText\n    private var mSfl: CheckBox\n    private var mSfu: CheckBox\n    private var mSft: CheckBox\n\n    init {\n        orientation = VERTICAL\n        val inflater = LayoutInflater.from(context)\n        inflater.inflate(R.layout.widget_advance_search_table, this)\n        val row0 = getChildAt(0) as ViewGroup\n        mSh = row0.getChildAt(0) as CheckBox\n        mSto = row0.getChildAt(1) as CheckBox\n        val row1 = getChildAt(1) as ViewGroup\n        mSr = row1.getChildAt(0) as CheckBox\n        mMinRating = row1.getChildAt(1) as Spinner\n        val row2 = getChildAt(2) as ViewGroup\n        mSp = row2.getChildAt(0) as CheckBox\n        mSpf = row2.getChildAt(1) as EditText\n        mSpt = row2.getChildAt(3) as EditText\n        val row4 = getChildAt(4) as ViewGroup\n        mSfl = row4.getChildAt(0) as CheckBox\n        mSfu = row4.getChildAt(1) as CheckBox\n        mSft = row4.getChildAt(2) as CheckBox\n        mSpt.setOnEditorActionListener { v: TextView, _: Int, _: KeyEvent? ->\n            val nextView = v.focusSearch(FOCUS_DOWN)\n            nextView?.requestFocus(FOCUS_DOWN)\n            true\n        }\n    }\n\n    var advanceSearch: Int\n        get() {\n            var advanceSearch = 0\n            if (mSh.isChecked) advanceSearch = advanceSearch or SH\n            if (mSto.isChecked) advanceSearch = advanceSearch or STO\n            if (mSfl.isChecked) advanceSearch = advanceSearch or SFL\n            if (mSfu.isChecked) advanceSearch = advanceSearch or SFU\n            if (mSft.isChecked) advanceSearch = advanceSearch or SFT\n            return advanceSearch\n        }\n        set(advanceSearch) {\n            mSh.isChecked = NumberUtils.int2boolean(advanceSearch and SH)\n            mSto.isChecked = NumberUtils.int2boolean(advanceSearch and STO)\n            mSfl.isChecked = NumberUtils.int2boolean(advanceSearch and SFL)\n            mSfu.isChecked = NumberUtils.int2boolean(advanceSearch and SFU)\n            mSft.isChecked = NumberUtils.int2boolean(advanceSearch and SFT)\n        }\n\n    var minRating: Int\n        get() {\n            val position = mMinRating.selectedItemPosition\n            return if (mSr.isChecked && position >= 0) {\n                position + 2\n            } else {\n                -1\n            }\n        }\n        set(minRating) {\n            if (minRating in 2..5) {\n                mSr.isChecked = true\n                mMinRating.setSelection(minRating - 2)\n            } else {\n                mSr.isChecked = false\n            }\n        }\n\n    var pageFrom: Int\n        get() = if (mSp.isChecked) {\n            NumberUtils.parseIntSafely(mSpf.text.toString(), -1)\n        } else {\n            -1\n        }\n\n        @SuppressLint(\"SetTextI18n\")\n        set(pageFrom) {\n            if (pageFrom > 0) {\n                mSpf.setText(pageFrom.toString())\n                mSp.isChecked = true\n            } else {\n                mSp.isChecked = false\n                mSpf.text = null\n            }\n        }\n\n    var pageTo: Int\n        get() = if (mSp.isChecked) {\n            NumberUtils.parseIntSafely(mSpt.text.toString(), -1)\n        } else {\n            -1\n        }\n\n        @SuppressLint(\"SetTextI18n\")\n        set(pageTo) {\n            if (pageTo > 0) {\n                mSpt.setText(pageTo.toString())\n                mSp.isChecked = true\n            } else {\n                mSp.isChecked = false\n            }\n        }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val state = Bundle()\n        state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState())\n        state.putInt(STATE_KEY_ADVANCE_SEARCH, advanceSearch)\n        state.putInt(STATE_KEY_MIN_RATING, minRating)\n        state.putInt(STATE_KEY_PAGE_FROM, pageFrom)\n        state.putInt(STATE_KEY_PAGE_TO, pageTo)\n        return state\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        if (state is Bundle) {\n            super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER))\n            advanceSearch = state.getInt(STATE_KEY_ADVANCE_SEARCH)\n            minRating = state.getInt(STATE_KEY_MIN_RATING)\n            pageFrom = state.getInt(STATE_KEY_PAGE_FROM)\n            pageTo = state.getInt(STATE_KEY_PAGE_TO)\n        } else {\n            super.onRestoreInstanceState(state)\n        }\n    }\n\n    companion object {\n        const val SH = 0x1\n        const val STO = 0x2\n        const val SFL = 0x100\n        const val SFU = 0x200\n        const val SFT = 0x400\n        private const val STATE_KEY_SUPER = \"super\"\n        private const val STATE_KEY_ADVANCE_SEARCH = \"advance_search\"\n        private const val STATE_KEY_MIN_RATING = \"min_rating\"\n        private const val STATE_KEY_PAGE_FROM = \"page_from\"\n        private const val STATE_KEY_PAGE_TO = \"page_to\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/CategoryTable.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.content.Context\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.TableLayout\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhUtils\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.widget.CheckTextView\nimport com.hippo.yorozuya.NumberUtils\n\nclass CategoryTable @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : TableLayout(context, attrs),\n    View.OnLongClickListener {\n    private var mDoujinshi: CheckTextView\n    private var mManga: CheckTextView\n    private var mArtistCG: CheckTextView\n    private var mGameCG: CheckTextView\n    private var mWestern: CheckTextView\n    private var mNonH: CheckTextView\n    private var mImageSets: CheckTextView\n    private var mCosplay: CheckTextView\n    private var mAsianPorn: CheckTextView\n    private var mMisc: CheckTextView\n    private var mOptions: Array<CheckTextView>\n\n    init {\n        LayoutInflater.from(context).inflate(R.layout.widget_category_table, this)\n        val row0 = getChildAt(0) as ViewGroup\n        mDoujinshi = row0.getChildAt(0) as CheckTextView\n        mManga = row0.getChildAt(1) as CheckTextView\n        val row1 = getChildAt(1) as ViewGroup\n        mArtistCG = row1.getChildAt(0) as CheckTextView\n        mGameCG = row1.getChildAt(1) as CheckTextView\n        val row2 = getChildAt(2) as ViewGroup\n        mWestern = row2.getChildAt(0) as CheckTextView\n        mNonH = row2.getChildAt(1) as CheckTextView\n        val row3 = getChildAt(3) as ViewGroup\n        mImageSets = row3.getChildAt(0) as CheckTextView\n        mCosplay = row3.getChildAt(1) as CheckTextView\n        val row4 = getChildAt(4) as ViewGroup\n        mAsianPorn = row4.getChildAt(0) as CheckTextView\n        mMisc = row4.getChildAt(1) as CheckTextView\n        mOptions = arrayOf(\n            mDoujinshi, mManga, mArtistCG, mGameCG, mWestern, mNonH, mImageSets, mCosplay, mAsianPorn, mMisc,\n        )\n        for (option in mOptions) {\n            option.setOnLongClickListener(this)\n        }\n    }\n\n    override fun onLongClick(v: View): Boolean {\n        if (v is CheckTextView) {\n            val checked = v.isChecked\n            for (option in mOptions) {\n                if (option !== v) {\n                    option.isChecked = !checked\n                }\n            }\n        }\n        return true\n    }\n\n    var category: Int\n        get() {\n            var category = 0\n            if (!mDoujinshi.isChecked) category = category or EhUtils.DOUJINSHI\n            if (!mManga.isChecked) category = category or EhUtils.MANGA\n            if (!mArtistCG.isChecked) category = category or EhUtils.ARTIST_CG\n            if (!mGameCG.isChecked) category = category or EhUtils.GAME_CG\n            if (!mWestern.isChecked) category = category or EhUtils.WESTERN\n            if (!mNonH.isChecked) category = category or EhUtils.NON_H\n            if (!mImageSets.isChecked) category = category or EhUtils.IMAGE_SET\n            if (!mCosplay.isChecked) category = category or EhUtils.COSPLAY\n            if (!mAsianPorn.isChecked) category = category or EhUtils.ASIAN_PORN\n            if (!mMisc.isChecked) category = category or EhUtils.MISC\n            return category\n        }\n        set(category) {\n            mDoujinshi.isChecked = !NumberUtils.int2boolean(category and EhUtils.DOUJINSHI)\n            mManga.isChecked = !NumberUtils.int2boolean(category and EhUtils.MANGA)\n            mArtistCG.isChecked = !NumberUtils.int2boolean(category and EhUtils.ARTIST_CG)\n            mGameCG.isChecked = !NumberUtils.int2boolean(category and EhUtils.GAME_CG)\n            mWestern.isChecked = !NumberUtils.int2boolean(category and EhUtils.WESTERN)\n            mNonH.isChecked = !NumberUtils.int2boolean(category and EhUtils.NON_H)\n            mImageSets.isChecked = !NumberUtils.int2boolean(category and EhUtils.IMAGE_SET)\n            mCosplay.isChecked = !NumberUtils.int2boolean(category and EhUtils.COSPLAY)\n            mAsianPorn.isChecked = !NumberUtils.int2boolean(category and EhUtils.ASIAN_PORN)\n            mMisc.isChecked = !NumberUtils.int2boolean(category and EhUtils.MISC)\n        }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val bundle = Bundle()\n        bundle.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState())\n        bundle.putInt(STATE_KEY_CATEGORY, category)\n        return bundle\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        if (state is Bundle) {\n            super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER))\n            category = state.getInt(STATE_KEY_CATEGORY)\n        }\n    }\n\n    companion object {\n        private const val STATE_KEY_SUPER = \"super\"\n        private const val STATE_KEY_CATEGORY = \"category\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/DialogWebChromeClient.java",
    "content": "/*\n * Copyright 2019 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.webkit.JsPromptResult;\nimport android.webkit.JsResult;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.widget.EditText;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.AlertDialog;\n\nimport com.hippo.ehviewer.R;\n\npublic class DialogWebChromeClient extends WebChromeClient {\n    private final Context context;\n\n    public DialogWebChromeClient(Context context) {\n        this.context = context;\n    }\n\n    @Override\n    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {\n        new AlertDialog.Builder(view.getContext())\n                .setMessage(message)\n                .setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm())\n                .setOnCancelListener(dialog -> result.cancel())\n                .show();\n        return true;\n    }\n\n    @Override\n    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {\n        new AlertDialog.Builder(view.getContext())\n                .setMessage(message)\n                .setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm())\n                .setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel())\n                .setOnCancelListener(dialog -> result.cancel())\n                .show();\n        return true;\n    }\n\n    @Override\n    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {\n        LayoutInflater inflater = LayoutInflater.from(context);\n        View promptView = inflater.inflate(R.layout.dialog_js_prompt, null, false);\n        TextView messageView = promptView.findViewById(R.id.message);\n        messageView.setText(message);\n        final EditText valueView = promptView.findViewById(R.id.value);\n        valueView.setText(defaultValue);\n\n        new AlertDialog.Builder(context)\n                .setView(promptView)\n                .setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm(valueView.getText().toString()))\n                .setOnCancelListener(dialog -> result.cancel())\n                .show();\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/EhStageLayout.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.coordinatorlayout.widget.CoordinatorLayout\nimport androidx.coordinatorlayout.widget.CoordinatorLayout.AttachedBehavior\nimport androidx.interpolator.view.animation.FastOutSlowInInterpolator\nimport com.google.android.material.snackbar.Snackbar.SnackbarLayout\nimport com.hippo.scene.StageLayout\nimport com.hippo.yorozuya.LayoutUtils\nimport kotlin.math.min\n\nclass EhStageLayout @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0,\n) : StageLayout(\n    context,\n    attrs,\n    defStyle,\n),\n    AttachedBehavior {\n    private var mAboveSnackViewList: MutableList<View>? = null\n\n    fun addAboveSnackView(view: View) {\n        if (null == mAboveSnackViewList) {\n            mAboveSnackViewList = ArrayList()\n        }\n        mAboveSnackViewList!!.add(view)\n    }\n\n    fun removeAboveSnackView(view: View) {\n        if (null == mAboveSnackViewList) {\n            return\n        }\n        mAboveSnackViewList!!.remove(view)\n    }\n\n    val aboveSnackViewCount: Int\n        get() = if (null == mAboveSnackViewList) 0 else mAboveSnackViewList!!.size\n\n    fun getAboveSnackViewAt(index: Int): View? = if (null == mAboveSnackViewList || index < 0 || index >= mAboveSnackViewList!!.size) {\n        null\n    } else {\n        mAboveSnackViewList!![index]\n    }\n\n    override fun getBehavior(): Behavior = Behavior()\n\n    class Behavior : CoordinatorLayout.Behavior<EhStageLayout>() {\n        @SuppressLint(\"RestrictedApi\")\n        override fun layoutDependsOn(\n            parent: CoordinatorLayout,\n            child: EhStageLayout,\n            dependency: View,\n        ): Boolean = dependency is SnackbarLayout\n\n        override fun onDependentViewChanged(\n            parent: CoordinatorLayout,\n            child: EhStageLayout,\n            dependency: View,\n        ): Boolean {\n            for (i in 0 until child.aboveSnackViewCount) {\n                val view = child.getAboveSnackViewAt(i)\n                if (view != null) {\n                    val translationY = min(\n                        0.0,\n                        (\n                            dependency.translationY - dependency.height - LayoutUtils.dp2pix(\n                                view.context,\n                                8f,\n                            )\n                            ).toDouble(),\n                    ).toFloat()\n                    view.animate().setInterpolator(FastOutSlowInInterpolator())\n                        .translationY(translationY).setDuration(150).start()\n                }\n            }\n            return false\n        }\n\n        override fun onDependentViewRemoved(\n            parent: CoordinatorLayout,\n            child: EhStageLayout,\n            dependency: View,\n        ) {\n            for (i in 0 until child.aboveSnackViewCount) {\n                child.getAboveSnackViewAt(i)?.animate()?.setInterpolator(FastOutSlowInInterpolator())?.translationY(0f)\n                    ?.setDuration(75)?.start()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/FixedThumb.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport androidx.core.content.withStyledAttributes\nimport com.hippo.ehviewer.R\nimport com.hippo.widget.LoadImageView\n\nopen class FixedThumb @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : LoadImageView(context, attrs, defStyleAttr) {\n    private var minAspect = 0f\n    private var maxAspect = 0f\n    private var alwaysCutAndScale = false\n\n    init {\n        context.withStyledAttributes(attrs, R.styleable.FixedThumb, defStyleAttr, defStyleAttr) {\n            minAspect = getFloat(R.styleable.FixedThumb_minAspect, 0f)\n            maxAspect = getFloat(R.styleable.FixedThumb_maxAspect, 0f)\n            alwaysCutAndScale = getBoolean(R.styleable.FixedThumb_alwaysCutAndScale, false)\n        }\n    }\n\n    override fun onPreSetImageDrawable(drawable: Drawable?, isTarget: Boolean) {\n        if (alwaysCutAndScale) {\n            setScaleType(ScaleType.CENTER_CROP)\n            return\n        }\n        if (isTarget && drawable != null) {\n            val width = drawable.intrinsicWidth\n            val height = drawable.intrinsicHeight\n            if (width > 0 && height > 0) {\n                val aspect = width.toFloat() / height.toFloat()\n                if (aspect < maxAspect && aspect > minAspect) {\n                    setScaleType(ScaleType.CENTER_CROP)\n                    return\n                }\n            }\n        }\n\n        setScaleType(ScaleType.FIT_CENTER)\n    }\n\n    override fun onPreSetImageResource(resId: Int, isTarget: Boolean) {\n        setScaleType(ScaleType.FIT_CENTER)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/GalleryGuideView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.util.AttributeSet;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.ehviewer.R;\nimport com.hippo.ehviewer.Settings;\nimport com.hippo.yorozuya.ViewUtils;\n\nimport rikka.core.res.ResourcesKt;\n\npublic class GalleryGuideView extends ViewGroup implements View.OnClickListener {\n    private final float[] mPoints = new float[3 * 4];\n    private int mBgColor;\n    private Paint mPaint;\n    private int mStep;\n\n    private TextView mLeftText;\n    private TextView mRightText;\n    private TextView mMenuText;\n    private TextView mProgressText;\n    private TextView mLongClickText;\n\n    public GalleryGuideView(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public GalleryGuideView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public GalleryGuideView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    private void init(Context context) {\n        mBgColor = ResourcesKt.resolveColor(context.getTheme(), R.attr.guideBackgroundColor);\n        mPaint = new Paint();\n        mPaint.setColor(ResourcesKt.resolveColor(context.getTheme(), R.attr.guideTitleColor));\n        mPaint.setStyle(Paint.Style.STROKE);\n        mPaint.setStrokeWidth(context.getResources().getDimension(R.dimen.gallery_guide_divider_width));\n        setOnClickListener(this);\n        setWillNotDraw(false);\n        bind();\n    }\n\n    private void bind() {\n        // Clear up\n        removeAllViews();\n        mLeftText = null;\n        mRightText = null;\n        mMenuText = null;\n        mProgressText = null;\n        mLongClickText = null;\n\n        switch (mStep) {\n            case 0:\n                bind1();\n                break;\n            case 1:\n            default:\n                bind2();\n                break;\n        }\n    }\n\n    private void bind1() {\n        LayoutInflater inflater = LayoutInflater.from(getContext());\n        inflater.inflate(R.layout.widget_gallery_guide_1, this);\n        mLeftText = (TextView) getChildAt(0);\n        mRightText = (TextView) getChildAt(1);\n        mMenuText = (TextView) getChildAt(2);\n        mProgressText = (TextView) getChildAt(3);\n    }\n\n    private void bind2() {\n        LayoutInflater inflater = LayoutInflater.from(getContext());\n        inflater.inflate(R.layout.widget_gallery_guide_2, this);\n        mLongClickText = (TextView) getChildAt(0);\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int widthSize = MeasureSpec.getSize(widthMeasureSpec);\n        int widthMode = MeasureSpec.getMode(widthMeasureSpec);\n        int heightSize = MeasureSpec.getSize(heightMeasureSpec);\n        int heightMode = MeasureSpec.getMode(heightMeasureSpec);\n        if (MeasureSpec.EXACTLY != widthMode || MeasureSpec.EXACTLY != heightMode) {\n            throw new IllegalStateException();\n        }\n\n        switch (mStep) {\n            case 0:\n                mLeftText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY),\n                        MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));\n                mRightText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY),\n                        MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));\n                mMenuText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY),\n                        MeasureSpec.makeMeasureSpec(heightSize / 2, MeasureSpec.EXACTLY));\n                mProgressText.measure(MeasureSpec.makeMeasureSpec(widthSize / 3, MeasureSpec.EXACTLY),\n                        MeasureSpec.makeMeasureSpec(heightSize / 2, MeasureSpec.EXACTLY));\n                break;\n            case 1:\n            default:\n                mLongClickText.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),\n                        MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));\n                break;\n        }\n\n        setMeasuredDimension(widthSize, heightSize);\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n        int width = r - l;\n        int height = b - t;\n\n        switch (mStep) {\n            case 0:\n                mLeftText.layout(0, 0, width / 3, height);\n                mRightText.layout(width * 2 / 3, 0, width, height);\n                mMenuText.layout(width / 3, 0, width * 2 / 3, height / 2);\n                mProgressText.layout(width / 3, height / 2, width * 2 / 3, height);\n                break;\n            case 1:\n            default:\n                mLongClickText.layout(0, 0, width, height);\n                break;\n        }\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        if (0 == mStep) {\n            mPoints[0] = (float) w / 3;\n            mPoints[1] = 0;\n            mPoints[2] = (float) w / 3;\n            mPoints[3] = h;\n\n            mPoints[4] = (float) (w * 2) / 3;\n            mPoints[5] = 0;\n            mPoints[6] = (float) (w * 2) / 3;\n            mPoints[7] = h;\n\n            mPoints[8] = (float) w / 3;\n            mPoints[9] = (float) h / 2;\n            mPoints[10] = (float) (w * 2) / 3;\n            mPoints[11] = (float) h / 2;\n        }\n    }\n\n    @Override\n    protected void onDraw(@NonNull Canvas canvas) {\n        super.onDraw(canvas);\n        canvas.drawColor(mBgColor);\n        if (0 == mStep) {\n            canvas.drawLines(mPoints, mPaint);\n        }\n    }\n\n    @Override\n    public void onClick(View v) {\n        switch (mStep) {\n            case 0:\n                mStep++;\n                bind();\n                break;\n            case 1:\n            default:\n                Settings.INSTANCE.putGuideGallery(false);\n                ViewUtils.removeFromParent(this);\n                break;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/GalleryHeader.java",
    "content": "/*\n * Copyright 2019 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.content.Context;\nimport android.graphics.Rect;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport androidx.core.view.DisplayCutoutCompat;\n\nimport com.hippo.ehviewer.R;\nimport com.hippo.yorozuya.ObjectUtils;\n\npublic class GalleryHeader extends ViewGroup {\n    private final Rect batteryRect = new Rect();\n    private final Rect progressRect = new Rect();\n    private final Rect clockRect = new Rect();\n    private final int[] location = new int[2];\n    private DisplayCutoutCompat displayCutout;\n    private int topInsets = 0;\n    private View battery;\n    private View progress;\n    private View clock;\n    private int lastX = 0;\n    private int lastY = 0;\n\n    public GalleryHeader(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public void setDisplayCutout(@Nullable DisplayCutoutCompat displayCutout) {\n        if (!ObjectUtils.equal(this.displayCutout, displayCutout)) {\n            this.displayCutout = displayCutout;\n            requestLayout();\n        }\n    }\n\n    public void setTopInsets(int topInsets) {\n        if (this.topInsets != topInsets) {\n            this.topInsets = topInsets;\n            requestLayout();\n        }\n    }\n\n    @Override\n    protected void onFinishInflate() {\n        super.onFinishInflate();\n\n        battery = findViewById(R.id.battery);\n        progress = findViewById(R.id.progress);\n        clock = findViewById(R.id.clock);\n    }\n\n    private void measureChild(Rect rect, View view, int width, int paddingLeft, int paddingRight) {\n        int left;\n        MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();\n        if (view == battery) {\n            left = paddingLeft + lp.leftMargin;\n        } else if (view == progress) {\n            left = paddingLeft + (width - paddingLeft - paddingRight) / 2 - view.getMeasuredWidth() / 2;\n        } else {\n            left = width - paddingRight - lp.rightMargin - view.getMeasuredWidth();\n        }\n        rect.set(left, lp.topMargin + topInsets, left + view.getMeasuredWidth(), lp.topMargin + topInsets + view.getMeasuredHeight());\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.P)\n    private boolean offsetVertically(Rect rect, View view, int width) {\n        int offset = 0;\n\n        measureChild(rect, view, width, 0, 0);\n        rect.offset(lastX, lastY);\n\n        for (Rect notch : displayCutout.getBoundingRects()) {\n            if (Rect.intersects(notch, rect)) {\n                offset = Math.max(offset, notch.bottom - lastY);\n            }\n        }\n\n        if (offset != 0) {\n            rect.offset(-lastX, -lastY);\n            rect.offset(0, offset);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.P)\n    private int getOffsetLeft(Rect rect, View view, int width) {\n        int offset = 0;\n\n        measureChild(rect, view, width, 0, 0);\n        rect.offset(lastX, lastY);\n\n        for (Rect notch : displayCutout.getBoundingRects()) {\n            if (Rect.intersects(notch, rect)) {\n                offset = Math.max(offset, notch.right - lastX);\n            }\n        }\n\n        return offset;\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.P)\n    private int getOffsetRight(Rect rect, View view, int width) {\n        int offset = 0;\n\n        measureChild(rect, view, width, 0, 0);\n        rect.offset(lastX, lastY);\n\n        for (Rect notch : displayCutout.getBoundingRects()) {\n            if (Rect.intersects(notch, rect)) {\n                offset = Math.max(offset, lastX + width - notch.left);\n            }\n        }\n\n        return offset;\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {\n            throw new IllegalStateException();\n        }\n        int width = MeasureSpec.getSize(widthMeasureSpec);\n\n        int height = 0;\n        for (int i = 0; i < getChildCount(); i++) {\n            View child = getChildAt(i);\n            measureChild(child, widthMeasureSpec, heightMeasureSpec);\n            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();\n            height = Math.max(height, child.getMeasuredHeight() + lp.topMargin + topInsets);\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && displayCutout != null) {\n            // Check progress covered\n            if (offsetVertically(progressRect, progress, width)) {\n                offsetVertically(batteryRect, battery, width);\n                offsetVertically(clockRect, clock, width);\n                height = Math.max(progressRect.bottom, Math.max(batteryRect.bottom, clockRect.bottom));\n            } else {\n                // Clamp left and right\n                int paddingLeft = getOffsetLeft(batteryRect, battery, width);\n                int paddingRight = getOffsetRight(clockRect, clock, width);\n                measureChild(batteryRect, battery, width, paddingLeft, paddingRight);\n                measureChild(progressRect, progress, width, paddingLeft, paddingRight);\n                measureChild(clockRect, clock, width, paddingLeft, paddingRight);\n            }\n        } else {\n            measureChild(batteryRect, battery, width, 0, 0);\n            measureChild(progressRect, progress, width, 0, 0);\n            measureChild(clockRect, clock, width, 0, 0);\n        }\n\n        setMeasuredDimension(width, height);\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n        battery.layout(batteryRect.left, batteryRect.top, batteryRect.right, batteryRect.bottom);\n        progress.layout(progressRect.left, progressRect.top, progressRect.right, progressRect.bottom);\n        clock.layout(clockRect.left, clockRect.top, clockRect.right, clockRect.bottom);\n\n        getLocationOnScreen(location);\n        if (lastX != location[0] || lastY != location[1]) {\n            lastX = location[0];\n            lastY = location[1];\n            requestLayout();\n        }\n    }\n\n    @Override\n    public MarginLayoutParams generateLayoutParams(AttributeSet attrs) {\n        return new MarginLayoutParams(getContext(), attrs);\n    }\n\n    @Override\n    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {\n        return p instanceof MarginLayoutParams;\n    }\n\n    @Override\n    protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {\n        if (lp instanceof MarginLayoutParams) {\n            return new MarginLayoutParams((MarginLayoutParams) lp);\n        }\n        return new MarginLayoutParams(lp);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/GalleryInfoContentHelper.kt",
    "content": "/*\n * Copyright 2019 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.annotation.SuppressLint\nimport android.os.Bundle\nimport android.os.Parcelable\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.FavouriteStatusRouter\nimport com.hippo.ehviewer.client.data.GalleryInfo\nimport com.hippo.util.toLocalDateTime\nimport com.hippo.widget.ContentLayout.ContentHelper\nimport com.hippo.yorozuya.IntIdGenerator\n\nabstract class GalleryInfoContentHelper : ContentHelper<GalleryInfo?>() {\n    var jumpTo: String? = null\n    private val listener: FavouriteStatusRouter.Listener\n\n    @SuppressLint(\"UseSparseArrays\")\n    private var map: MutableMap<Long, GalleryInfo> = HashMap()\n\n    init {\n        listener = FavouriteStatusRouter.Listener { gid: Long, slot: Int ->\n            val info = map[gid]\n            if (info != null) {\n                info.favoriteSlot = slot\n            }\n        }\n        EhApplication.favouriteStatusRouter.addListener(listener)\n    }\n\n    fun destroy() {\n        EhApplication.favouriteStatusRouter.removeListener(listener)\n    }\n\n    override fun onAddData(data: List<GalleryInfo?>) {\n        for (info in data) {\n            info?.let {\n                map[info.gid] = info\n            }\n        }\n    }\n\n    override fun onRemoveData(data: List<GalleryInfo?>) {\n        for (info in data) {\n            info?.let { map.remove(info.gid) }\n        }\n    }\n\n    override fun onClearData() {\n        map.clear()\n    }\n\n    override fun saveInstanceState(superState: Parcelable?): Parcelable {\n        val bundle = super.saveInstanceState(superState) as Bundle\n\n        // TODO It's a bad design\n        val router = EhApplication.favouriteStatusRouter\n        val id = router.saveDataMap(map)\n        bundle.putInt(KEY_DATA_MAP, id)\n        return bundle\n    }\n\n    override fun restoreInstanceState(state: Parcelable): Parcelable? {\n        val bundle = state as Bundle\n        val id = bundle.getInt(KEY_DATA_MAP, IntIdGenerator.INVALID_ID)\n        if (id != IntIdGenerator.INVALID_ID) {\n            val router = EhApplication.favouriteStatusRouter\n            val map = router.restoreDataMap(id)\n            if (map != null) {\n                this.map = map\n            }\n        }\n        return super.restoreInstanceState(state)\n    }\n\n    fun goTo(time: Long, isNext: Boolean) {\n        jumpTo = time.toLocalDateTime().date.toString()\n        if (isNext) {\n            goTo(mNext ?: \"2\", true)\n        } else {\n            goTo(mPrev, false)\n        }\n        jumpTo = null\n    }\n\n    companion object {\n        private const val KEY_DATA_MAP = \"data_map\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/GalleryRatingBar.java",
    "content": "/*\n * Copyright 2014-2016 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.util.AttributeSet;\nimport android.widget.RatingBar;\n\nimport androidx.appcompat.widget.AppCompatRatingBar;\n\npublic class GalleryRatingBar extends AppCompatRatingBar\n        implements RatingBar.OnRatingBarChangeListener {\n    private OnUserRateListener mListener;\n\n    public GalleryRatingBar(Context context) {\n        super(context);\n        init();\n    }\n\n    public GalleryRatingBar(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init();\n    }\n\n    public GalleryRatingBar(Context context, AttributeSet attrs,\n                            int defStyle) {\n        super(context, attrs, defStyle);\n        init();\n    }\n\n    private void init() {\n        setOnRatingBarChangeListener(this);\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        super.onDraw(canvas);\n\n        if (mListener != null) {\n            mListener.onUserRate(getRating());\n        }\n    }\n\n    public void setOnUserRateListener(OnUserRateListener l) {\n        mListener = l;\n    }\n\n    @Override\n    public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {\n        if (rating <= 0.0f) {\n            setRating(0.5f);\n        }\n    }\n\n    public interface OnUserRateListener {\n        void onUserRate(float rating);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/ImageSearchLayout.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.content.Context\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.os.Parcel\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.view.AbsSavedState\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.LinearLayout\nimport androidx.core.content.ContextCompat\nimport androidx.core.net.toUri\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.data.ListUrlBuilder\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.unifile.UniFile\nimport com.hippo.unifile.openInputStream\nimport com.hippo.unifile.sha1\nimport com.hippo.yorozuya.ViewUtils\nimport java.io.FileInputStream\n\nclass ImageSearchLayout @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : LinearLayout(\n    context,\n    attrs,\n    defStyleAttr,\n),\n    View.OnClickListener {\n    private var mPreview: ImageView? = null\n    private var mSelectImage: View? = null\n    private var mHelper: Helper? = null\n    private var mImageUri: Uri? = null\n\n    init {\n        orientation = VERTICAL\n        setDividerDrawable(ContextCompat.getDrawable(context, R.drawable.spacer_keyline))\n        setShowDividers(SHOW_DIVIDER_MIDDLE)\n        setClipChildren(false)\n        clipToPadding = false\n        LayoutInflater.from(context).inflate(R.layout.widget_image_search, this)\n        mPreview = ViewUtils.`$$`(this, R.id.preview) as ImageView\n        mSelectImage = ViewUtils.`$$`(this, R.id.select_image)\n        mSelectImage!!.setOnClickListener(this)\n    }\n\n    fun setHelper(helper: Helper?) {\n        mHelper = helper\n    }\n\n    override fun onClick(v: View) {\n        if (v === mSelectImage) {\n            if (null != mHelper) {\n                mHelper!!.onSelectImage()\n            }\n        }\n    }\n\n    fun setImageUri(imageUri: Uri?) {\n        if (null == imageUri) {\n            return\n        }\n        val context = context\n        UniFile.fromUri(context, imageUri)?.openInputStream().use {\n            val bitmap = BitmapFactory.decodeStream(it) ?: return\n            mImageUri = imageUri\n            mPreview!!.setImageBitmap(bitmap)\n            mPreview!!.setVisibility(VISIBLE)\n        }\n    }\n\n    private fun setImagePath(imagePath: String?) {\n        if (null == imagePath) {\n            return\n        }\n        FileInputStream(imagePath).use {\n            val bitmap = BitmapFactory.decodeStream(it) ?: return\n            mImageUri = imagePath.toUri()\n            mPreview!!.setImageBitmap(bitmap)\n            mPreview!!.setVisibility(VISIBLE)\n        }\n    }\n\n    fun formatListUrlBuilder(builder: ListUrlBuilder) {\n        if (null == mImageUri) {\n            throw EhException(context.getString(R.string.select_image_first))\n        }\n        UniFile.fromUri(context, mImageUri!!)?.sha1()?.let {\n            builder.hash = it\n        }\n    }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val superState = super.onSaveInstanceState()\n        val ss = SavedState(superState)\n        ss.imagePath = mImageUri.toString()\n        return ss\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        val ss = state as SavedState\n        super.onRestoreInstanceState(ss.superState)\n        setImagePath(ss.imagePath)\n    }\n\n    interface Helper {\n        fun onSelectImage()\n    }\n\n    private class SavedState : AbsSavedState {\n        var imagePath: String? = null\n\n        /**\n         * Constructor called from [ImageSearchLayout.onSaveInstanceState]\n         */\n        constructor(superState: Parcelable?) : super(superState)\n\n        /**\n         * Constructor called from [.CREATOR]\n         */\n        private constructor(source: Parcel) : super(source) {\n            imagePath = source.readString()\n        }\n\n        override fun writeToParcel(out: Parcel, flags: Int) {\n            super.writeToParcel(out, flags)\n            out.writeString(imagePath)\n        }\n\n        override fun describeContents(): Int = 0\n\n        companion object CREATOR : Parcelable.Creator<SavedState> {\n            override fun createFromParcel(`in`: Parcel): SavedState = SavedState(`in`)\n\n            override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/ResizeableFixedThumb.kt",
    "content": "/*\n * Copyright 2022 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.ehviewer.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport com.hippo.ehviewer.Settings\n\nclass ResizeableFixedThumb @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : FixedThumb(context, attrs) {\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        setMeasuredDimension(Settings.listThumbSize, Settings.listThumbSize / 2 * 3)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/ReversibleSeekBar.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.AppCompatSeekBar;\n\npublic class ReversibleSeekBar extends AppCompatSeekBar {\n    private boolean mReverse;\n\n    public ReversibleSeekBar(Context context) {\n        super(context);\n    }\n\n    public ReversibleSeekBar(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public ReversibleSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    public void setReverse(boolean reverse) {\n        mReverse = reverse;\n        invalidate();\n    }\n\n    @Override\n    public void draw(@NonNull Canvas canvas) {\n        boolean reverse = mReverse;\n        int saveCount = 0;\n        if (reverse) {\n            saveCount = canvas.save();\n            float px = this.getWidth() / 2.0f;\n            float py = this.getHeight() / 2.0f;\n            canvas.scale(-1, 1, px, py);\n        }\n        super.draw(canvas);\n        if (reverse) {\n            canvas.restoreToCount(saveCount);\n        }\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        boolean reverse = mReverse;\n        float x = 0.0f, y = 0.0f;\n        if (reverse) {\n            x = event.getX();\n            y = event.getY();\n            event.setLocation(getWidth() - x, y);\n        }\n        boolean result = super.onTouchEvent(event);\n        if (reverse) {\n            event.setLocation(x, y);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/SearchBar.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.animation.Animator\nimport android.animation.ObjectAnimator\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Rect\nimport android.graphics.drawable.Drawable\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.text.Editable\nimport android.text.TextWatcher\nimport android.util.AttributeSet\nimport android.view.KeyEvent\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.inputmethod.EditorInfo\nimport android.view.inputmethod.InputMethodManager\nimport android.widget.ImageView\nimport android.widget.TextView\nimport androidx.annotation.Keep\nimport androidx.core.graphics.withSave\nimport androidx.core.view.isVisible\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport androidx.recyclerview.widget.RecyclerView\nimport com.google.android.material.card.MaterialCardView\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.LinearDividerItemDecoration\nimport com.hippo.ehviewer.EhApplication.Companion.searchDatabase\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.client.EhTagDatabase\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.util.launchIO\nimport com.hippo.util.withUIContext\nimport com.hippo.view.ViewTransition\nimport com.hippo.yorozuya.AnimationUtils\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.SimpleAnimatorListener\nimport com.hippo.yorozuya.ViewUtils\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.toList\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport rikka.core.res.resolveColor\n\nclass SearchBar @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : MaterialCardView(context, attrs),\n    View.OnClickListener,\n    TextView.OnEditorActionListener,\n    TextWatcher,\n    SearchEditText.SearchEditTextListener {\n    private val mRect = Rect()\n    private var mState = STATE_NORMAL\n    private var mWidth = 0\n    private var mHeight = 0\n    private var mProgress = 0f\n    private var mMenuButton: ImageView\n    private var mTitleTextView: TextView\n    private var mActionButton: ImageView\n    private var mEditText: SearchEditText\n    private var mListContainer: View\n    private var mListView: EasyRecyclerView\n    private var mListHeader: View\n    private var mViewTransition: ViewTransition\n    private var mSuggestionAdapter: SuggestionAdapter\n    private var mSuggestionList = listOf<Suggestion>()\n    private val suggestionLock = Mutex()\n    private var mAllowEmptySearch = true\n    private var mInAnimation = false\n    private var mHelper: Helper? = null\n    private var mSuggestionProvider: SuggestionProvider? = null\n    private var mOnStateChangeListener: OnStateChangeListener? = null\n\n    init {\n        val inflater = LayoutInflater.from(context)\n        inflater.inflate(R.layout.widget_search_bar, this)\n        mMenuButton = ViewUtils.`$$`(this, R.id.search_menu) as ImageView\n        mTitleTextView = ViewUtils.`$$`(this, R.id.search_title) as TextView\n        mActionButton = ViewUtils.`$$`(this, R.id.search_action) as ImageView\n        mEditText = ViewUtils.`$$`(this, R.id.search_edit_text) as SearchEditText\n        mListContainer = ViewUtils.`$$`(this, R.id.list_container)\n        mListView = ViewUtils.`$$`(mListContainer, R.id.search_bar_list) as EasyRecyclerView\n        mListHeader = ViewUtils.`$$`(mListContainer, R.id.list_header)\n        mViewTransition = ViewTransition(mTitleTextView, mEditText)\n\n        mMenuButton.setOnClickListener(this)\n        mTitleTextView.setOnClickListener(this)\n        mActionButton.setOnClickListener(this)\n        mEditText.setSearchEditTextListener(this)\n        mEditText.setOnEditorActionListener(this)\n        mEditText.addTextChangedListener(this)\n\n        // Get base height\n        ViewUtils.measureView(\n            this,\n            LayoutParams.WRAP_CONTENT,\n            LayoutParams.WRAP_CONTENT,\n        )\n\n        mSuggestionAdapter = SuggestionAdapter(inflater)\n        mListView.adapter = mSuggestionAdapter\n        val decoration = LinearDividerItemDecoration(\n            LinearDividerItemDecoration.VERTICAL,\n            context.theme.resolveColor(R.attr.dividerColor),\n            LayoutUtils.dp2pix(context, 1f),\n        )\n        decoration.setShowLastDivider(false)\n        mListView.addItemDecoration(decoration)\n        mListView.layoutManager = LinearLayoutManager(context)\n    }\n\n    private fun addListHeader() {\n        mListHeader.visibility = VISIBLE\n    }\n\n    private fun removeListHeader() {\n        mListHeader.visibility = GONE\n    }\n\n    @SuppressLint(\"NotifyDataSetChanged\")\n    @OptIn(DelicateCoroutinesApi::class)\n    private fun updateSuggestions(scrollToTop: Boolean = true) {\n        launchIO {\n            suggestionLock.withLock {\n                mSuggestionList = mergedSuggestionFlow().toList()\n                withUIContext {\n                    if (mSuggestionList.isEmpty()) {\n                        removeListHeader()\n                    } else {\n                        addListHeader()\n                    }\n                    mSuggestionAdapter.notifyDataSetChanged()\n                }\n            }\n        }\n        if (scrollToTop) {\n            mListView.scrollToPosition(0)\n        }\n    }\n\n    private fun mergedSuggestionFlow(): Flow<Suggestion> = flow {\n        val text = mEditText.text.toString()\n        mSuggestionProvider?.run { providerSuggestions(text)?.forEach { emit(it) } }\n        searchDatabase.getSuggestions(text, 128).forEach { emit(KeywordSuggestion(it)) }\n        EhTagDatabase.takeIf { it.isInitialized() }?.run {\n            if (text.isNotEmpty() && !text.endsWith(\" \")) {\n                val keyword = text.substringAfterLast(\" \")\n                val translate =\n                    Settings.showTagTranslations && isTranslatable(context)\n                arrayOf(TYPE_EQUAL, TYPE_START, TYPE_CONTAIN).forEach { type ->\n                    suggestFlow(keyword, translate, type).collect {\n                        emit(TagSuggestion(it.first, it.second))\n                    }\n                }\n            }\n        }\n    }\n\n    fun setAllowEmptySearch(allowEmptySearch: Boolean) {\n        mAllowEmptySearch = allowEmptySearch\n    }\n\n    fun setEditTextHint(hint: CharSequence) {\n        mEditText.hint = hint\n    }\n\n    fun setHelper(helper: Helper) {\n        mHelper = helper\n    }\n\n    fun setOnStateChangeListener(listener: OnStateChangeListener) {\n        mOnStateChangeListener = listener\n    }\n\n    fun setSuggestionProvider(suggestionProvider: SuggestionProvider) {\n        mSuggestionProvider = suggestionProvider\n    }\n\n    fun setText(text: String) {\n        mEditText.setText(text)\n    }\n\n    fun cursorToEnd() {\n        mEditText.setSelection(mEditText.length())\n    }\n\n    fun setTitle(title: String) {\n        mTitleTextView.text = title\n    }\n\n    fun setLeftDrawable(drawable: Drawable) {\n        mMenuButton.setImageDrawable(drawable)\n    }\n\n    fun setRightDrawable(drawable: Drawable) {\n        mActionButton.setImageDrawable(drawable)\n    }\n\n    fun applySearch() {\n        val query = mEditText.text.toString().replace(Regex(\"\\\\p{Cntrl}\"), \"\").trim { it <= ' ' }\n        if (!mAllowEmptySearch && query.isEmpty()) {\n            return\n        }\n        // Put it into db\n        searchDatabase.addQuery(query)\n        // Callback\n        mHelper?.onApplySearch(query)\n    }\n\n    override fun onClick(v: View) {\n        if (v === mTitleTextView) {\n            mHelper?.onClickTitle()\n        } else if (v === mMenuButton) {\n            mHelper?.onClickLeftIcon()\n        } else if (v === mActionButton) {\n            mHelper?.onClickRightIcon()\n        }\n    }\n\n    override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {\n        if (v === mEditText) {\n            if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_NULL) {\n                applySearch()\n                return true\n            }\n        }\n        return false\n    }\n\n    fun getState(): Int = mState\n\n    fun setState(state: Int, animation: Boolean = true) {\n        if (mState != state) {\n            val oldState = mState\n            mState = state\n            when (oldState) {\n                STATE_NORMAL -> {\n                    mViewTransition.showView(1, animation)\n                    mEditText.requestFocus()\n                    if (state == STATE_SEARCH_LIST) {\n                        showImeAndSuggestionsList(animation)\n                    }\n                    mOnStateChangeListener?.onStateChange(this, state, oldState, animation)\n                }\n                STATE_SEARCH -> {\n                    if (state == STATE_NORMAL) {\n                        mViewTransition.showView(0, animation)\n                    } else if (state == STATE_SEARCH_LIST) {\n                        showImeAndSuggestionsList(animation)\n                    }\n                    mOnStateChangeListener?.onStateChange(this, state, oldState, animation)\n                }\n                STATE_SEARCH_LIST -> {\n                    hideImeAndSuggestionsList(animation)\n                    if (state == STATE_NORMAL) {\n                        mViewTransition.showView(0, animation)\n                    }\n                    mOnStateChangeListener?.onStateChange(this, state, oldState, animation)\n                }\n            }\n        }\n    }\n\n    @Keep\n    private fun showImeAndSuggestionsList(animation: Boolean) {\n        // Show ime\n        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager\n        imm.showSoftInput(mEditText, 0)\n        // update suggestion for show suggestions list\n        updateSuggestions()\n        // Show suggestions list\n        if (animation) {\n            val oa = ObjectAnimator.ofFloat(this, \"progress\", 1f)\n            oa.duration = ANIMATE_TIME\n            oa.interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR\n            oa.addListener(object : SimpleAnimatorListener() {\n                override fun onAnimationStart(animation: Animator) {\n                    mListContainer.visibility = VISIBLE\n                    mInAnimation = true\n                }\n\n                override fun onAnimationEnd(animation: Animator) {\n                    mInAnimation = false\n                }\n            })\n            oa.setAutoCancel(true)\n            oa.start()\n        } else {\n            mListContainer.visibility = VISIBLE\n            progress = 1f\n        }\n    }\n\n    private fun hideImeAndSuggestionsList(animation: Boolean) {\n        // Hide ime\n        val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager\n        imm.hideSoftInputFromWindow(windowToken, 0)\n        // Hide suggestions list\n        if (animation) {\n            val oa = ObjectAnimator.ofFloat(this, \"progress\", 0f)\n            oa.duration = ANIMATE_TIME\n            oa.interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR\n            oa.addListener(object : SimpleAnimatorListener() {\n                override fun onAnimationStart(animation: Animator) {\n                    mInAnimation = true\n                }\n\n                override fun onAnimationEnd(animation: Animator) {\n                    mListContainer.visibility = GONE\n                    mInAnimation = false\n                }\n            })\n            oa.setAutoCancel(true)\n            oa.start()\n        } else {\n            progress = 0f\n            mListContainer.visibility = GONE\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        if (mListContainer.isVisible && (!mInAnimation || mHeight == 0)) {\n            mWidth = right - left\n            mHeight = bottom - top\n        }\n    }\n\n    override fun getProgress(): Float = mProgress\n\n    @Keep\n    override fun setProgress(progress: Float) {\n        mProgress = progress\n        invalidate()\n    }\n\n    fun getEditText(): SearchEditText = mEditText\n\n    override fun draw(canvas: Canvas) {\n        if (mInAnimation && mHeight != 0) {\n            canvas.withSave {\n                val bottom = MathUtils.lerp(measuredHeight, mHeight, mProgress)\n                mRect.set(0, 0, mWidth, bottom)\n                setClipBounds(mRect)\n                canvas.clipRect(mRect)\n                super.draw(canvas)\n            }\n        } else {\n            clipBounds = null\n            super.draw(canvas)\n        }\n    }\n\n    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {\n        // Empty\n    }\n\n    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {\n        // Empty\n    }\n\n    override fun afterTextChanged(s: Editable) {\n        updateSuggestions()\n    }\n\n    override fun onClick() {\n        mHelper?.onSearchEditTextClick()\n    }\n\n    override fun onBackPressed() {\n        mHelper?.onSearchEditTextBackPressed()\n    }\n\n    override fun onReceiveContent(uri: Uri?) {\n        mHelper?.onReceiveContent(uri)\n    }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val state = Bundle()\n        state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState())\n        state.putInt(STATE_KEY_STATE, mState)\n        return state\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        if (state is Bundle) {\n            super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER))\n            setState(state.getInt(STATE_KEY_STATE), false)\n        }\n    }\n\n    private fun wrapTagKeyword(keyword: String): String = if (keyword.endsWith(':')) {\n        keyword\n    } else if (keyword.contains(\" \")) {\n        val tag = keyword.substringAfter(':')\n        val prefix = keyword.dropLast(tag.length)\n        \"$prefix\\\"$tag$\\\" \"\n    } else {\n        \"$keyword$ \"\n    }\n\n    interface Helper {\n        fun onClickTitle()\n        fun onClickLeftIcon()\n        fun onClickRightIcon()\n        fun onSearchEditTextClick()\n        fun onApplySearch(query: String)\n        fun onSearchEditTextBackPressed()\n        fun onReceiveContent(uri: Uri?)\n    }\n\n    interface OnStateChangeListener {\n        fun onStateChange(searchBar: SearchBar, newState: Int, oldState: Int, animation: Boolean)\n    }\n\n    interface SuggestionProvider {\n        fun providerSuggestions(text: String): List<Suggestion>?\n    }\n\n    abstract class Suggestion {\n        abstract fun getText(textView: TextView): CharSequence?\n        abstract fun onClick()\n        open fun onLongClick(): Boolean = false\n    }\n\n    private class SuggestionHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {\n        val text1: TextView = itemView.findViewById(android.R.id.text1)\n        val text2: TextView = itemView.findViewById(android.R.id.text2)\n    }\n\n    private inner class SuggestionAdapter(\n        private val mInflater: LayoutInflater,\n    ) : RecyclerView.Adapter<SuggestionHolder>() {\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionHolder = SuggestionHolder(mInflater.inflate(R.layout.item_simple_list_2, parent, false))\n\n        override fun onBindViewHolder(holder: SuggestionHolder, position: Int) {\n            val suggestion = mSuggestionList[position]\n            val text1 = suggestion.getText(holder.text1)\n            val text2 = suggestion.getText(holder.text2)\n            holder.text1.text = text1\n            if (text2 == null) {\n                holder.text2.visibility = GONE\n                holder.text2.text = \"\"\n            } else {\n                holder.text2.visibility = VISIBLE\n                holder.text2.text = text2\n            }\n\n            holder.itemView.setOnClickListener {\n                mSuggestionList.run {\n                    if (position < size) {\n                        this[position].onClick()\n                    }\n                }\n            }\n            holder.itemView.setOnLongClickListener {\n                mSuggestionList.run {\n                    if (position < size) {\n                        return@setOnLongClickListener this[position].onLongClick()\n                    }\n                }\n                return@setOnLongClickListener false\n            }\n        }\n\n        override fun getItemId(position: Int): Long = position.toLong()\n\n        override fun getItemCount(): Int = mSuggestionList.size\n    }\n\n    inner class TagSuggestion(\n        private var mHint: String?,\n        private var mKeyword: String,\n    ) : Suggestion() {\n        override fun getText(textView: TextView): CharSequence? = if (textView.id == android.R.id.text1) {\n            mKeyword\n        } else {\n            mHint\n        }\n\n        override fun onClick() {\n            val editable = mEditText.text as Editable\n            val keywords = editable.toString().substringBeforeLast(\" \", \"\")\n            val keyword = wrapTagKeyword(mKeyword)\n            val newKeywords = if (keywords.isNotEmpty()) \"$keywords $keyword\" else keyword\n            mEditText.setText(newKeywords)\n            mEditText.setSelection(newKeywords.length)\n        }\n    }\n\n    inner class KeywordSuggestion(\n        private val mKeyword: String,\n    ) : Suggestion() {\n        override fun getText(textView: TextView): CharSequence? = if (textView.id == android.R.id.text1) {\n            mKeyword\n        } else {\n            null\n        }\n\n        override fun onClick() {\n            mEditText.setText(mKeyword)\n            mEditText.setSelection(mEditText.length())\n        }\n\n        override fun onLongClick(): Boolean {\n            searchDatabase.deleteQuery(mKeyword)\n            updateSuggestions(false)\n            return true\n        }\n    }\n\n    companion object {\n        const val STATE_NORMAL = 0\n        const val STATE_SEARCH = 1\n        const val STATE_SEARCH_LIST = 2\n        private const val STATE_KEY_SUPER = \"super\"\n        private const val STATE_KEY_STATE = \"state\"\n        private const val ANIMATE_TIME = 300L\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/SearchDatabase.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\nimport android.database.sqlite.SQLiteOpenHelper;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport com.hippo.util.SqlUtils;\n\nimport java.util.LinkedList;\nimport java.util.List;\n\npublic final class SearchDatabase {\n    public static final String COLUMN_QUERY = \"query\";\n    public static final String COLUMN_DATE = \"date\";\n    private static final String TAG = SearchDatabase.class.getSimpleName();\n    private static final String DATABASE_NAME = \"search_database.db\";\n    private static final String TABLE_SUGGESTIONS = \"suggestions\";\n\n    private static final int MAX_HISTORY = 100;\n    private static SearchDatabase sInstance;\n    private final SQLiteDatabase mDatabase;\n\n    @SuppressWarnings(\"resource\")\n    private SearchDatabase(Context context) {\n        DatabaseHelper databaseHelper = new DatabaseHelper(context);\n        mDatabase = databaseHelper.getWritableDatabase();\n    }\n\n    public static SearchDatabase getInstance(Context context) {\n        if (sInstance == null) {\n            sInstance = new SearchDatabase(context.getApplicationContext());\n        }\n        return sInstance;\n    }\n\n    public String[] getSuggestions(String prefix, int limit) {\n        List<String> queryList = new LinkedList<>();\n        limit = Math.max(0, limit);\n\n        StringBuilder sb = new StringBuilder();\n        sb.append(\"SELECT * FROM \").append(TABLE_SUGGESTIONS);\n        if (!TextUtils.isEmpty(prefix)) {\n            sb.append(\" WHERE \").append(COLUMN_QUERY).append(\" LIKE '\")\n                    .append(SqlUtils.sqlEscapeString(prefix)).append(\"%'\");\n        }\n        sb.append(\" ORDER BY \").append(COLUMN_DATE).append(\" DESC\")\n                .append(\" LIMIT \").append(limit);\n\n        try {\n            Cursor cursor = mDatabase.rawQuery(sb.toString(), null);\n            int queryIndex = cursor.getColumnIndex(COLUMN_QUERY);\n            if (cursor.moveToFirst()) {\n                while (!cursor.isAfterLast()) {\n                    String suggestion = cursor.getString(queryIndex);\n                    if (!prefix.equals(suggestion)) {\n                        queryList.add(suggestion);\n                    }\n                    cursor.moveToNext();\n                }\n            }\n            cursor.close();\n            return queryList.toArray(new String[0]);\n        } catch (SQLException e) {\n            return new String[0];\n        }\n    }\n\n    public void addQuery(final String query) {\n        if (!TextUtils.isEmpty(query)) {\n            // Delete old first\n            deleteQuery(query);\n            // Add it to database\n            ContentValues values = new ContentValues();\n            values.put(COLUMN_QUERY, query);\n            values.put(COLUMN_DATE, System.currentTimeMillis());\n            mDatabase.insert(TABLE_SUGGESTIONS, null, values);\n            // Remove history if more than max\n            truncateHistory(MAX_HISTORY);\n        }\n    }\n\n    public void deleteQuery(final String query) {\n        mDatabase.delete(TABLE_SUGGESTIONS, COLUMN_QUERY + \"=?\", new String[]{query});\n    }\n\n    public void clearQuery() {\n        truncateHistory(0);\n    }\n\n    /**\n     * Reduces the length of the history table, to prevent it from growing too large.\n     *\n     * @param maxEntries Max entries to leave in the table. 0 means remove all entries.\n     */\n    private void truncateHistory(int maxEntries) {\n        if (maxEntries < 0) {\n            throw new IllegalArgumentException();\n        }\n\n        try {\n            // null means \"delete all\".  otherwise \"delete but leave n newest\"\n            String selection = null;\n            if (maxEntries > 0) {\n                selection = \"_id IN \" +\n                        \"(SELECT _id FROM \" + TABLE_SUGGESTIONS +\n                        \" ORDER BY \" + COLUMN_DATE + \" DESC\" +\n                        \" LIMIT -1 OFFSET \" + maxEntries + \")\";\n            }\n            mDatabase.delete(TABLE_SUGGESTIONS, selection, null);\n        } catch (RuntimeException e) {\n            Log.e(TAG, \"truncateHistory\", e);\n        }\n    }\n\n    /**\n     * Builds the database.  This version has extra support for using the version field\n     * as a mode flags field, and configures the database columns depending on the mode bits\n     * (features) requested by the extending class.\n     */\n    private static class DatabaseHelper extends SQLiteOpenHelper {\n        public DatabaseHelper(Context context) {\n            super(context, DATABASE_NAME, null, 1);\n        }\n\n        @Override\n        public void onCreate(SQLiteDatabase db) {\n            db.execSQL(\"CREATE TABLE \" + TABLE_SUGGESTIONS + \" (\" +\n                    \"_id INTEGER PRIMARY KEY\" +\n                    \", `\" + COLUMN_QUERY + \"` TEXT\" +\n                    \",\" + COLUMN_DATE + \" LONG\" +\n                    \");\");\n        }\n\n        @Override\n        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {\n            db.execSQL(\"DROP TABLE IF EXISTS \" + TABLE_SUGGESTIONS);\n            onCreate(db);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/SearchEditText.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.annotation.SuppressLint;\nimport android.content.ClipData;\nimport android.content.Context;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.ContentInfo;\nimport android.view.KeyEvent;\nimport android.view.MotionEvent;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.RequiresApi;\nimport androidx.appcompat.widget.AppCompatEditText;\n\nimport com.hippo.util.ExceptionUtils;\n\npublic class SearchEditText extends AppCompatEditText {\n    private SearchEditTextListener mListener;\n\n    public SearchEditText(Context context) {\n        super(context);\n    }\n\n    public SearchEditText(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public SearchEditText(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    public void setSearchEditTextListener(SearchEditTextListener listener) {\n        mListener = listener;\n    }\n\n    @Override\n    public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {\n        if (keyCode == KeyEvent.KEYCODE_BACK) {\n            // special case for the back key, we do not even try to send it\n            // to the drop down list but instead, consume it immediately\n            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {\n                KeyEvent.DispatcherState state = getKeyDispatcherState();\n                if (state != null) {\n                    state.startTracking(event, this);\n                }\n                return true;\n            } else if (event.getAction() == KeyEvent.ACTION_UP) {\n                KeyEvent.DispatcherState state = getKeyDispatcherState();\n                if (state != null) {\n                    state.handleUpEvent(event);\n                }\n                if (event.isTracking() && !event.isCanceled()) {\n                    // TODO stopSelectionActionMode\n                    if (mListener != null) {\n                        mListener.onBackPressed();\n                        return true;\n                    }\n                }\n            }\n        }\n        return super.onKeyPreIme(keyCode, event);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(@NonNull MotionEvent event) {\n        if (event.getAction() == MotionEvent.ACTION_UP && mListener != null) {\n            mListener.onClick();\n        }\n        try {\n            return super.onTouchEvent(event);\n        } catch (Throwable t) {\n            // Some devices crash here.\n            // I don't why.\n            ExceptionUtils.INSTANCE.throwIfFatal(t);\n            return false;\n        }\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.S)\n    @Nullable\n    @Override\n    public ContentInfo onReceiveContent(@NonNull ContentInfo payload) {\n        ClipData clipData = payload.getClip();\n        if (clipData.getItemCount() == 1) {\n            if (mListener != null) {\n                mListener.onReceiveContent(clipData.getItemAt(0).getUri());\n            }\n        }\n        return super.onReceiveContent(payload);\n    }\n\n    public interface SearchEditTextListener {\n        void onClick();\n\n        void onBackPressed();\n\n        void onReceiveContent(Uri uri);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/SearchLayout.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.util.SparseArray\nimport android.view.LayoutInflater\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.CompoundButton\nimport android.widget.FrameLayout\nimport android.widget.ImageView\nimport android.widget.Switch\nimport android.widget.TextView\nimport androidx.annotation.IntDef\nimport androidx.appcompat.app.AlertDialog\nimport androidx.recyclerview.widget.DefaultItemAnimator\nimport androidx.recyclerview.widget.LinearLayoutManager\nimport com.google.android.material.tabs.TabLayout\nimport com.google.android.material.tabs.TabLayout.OnTabSelectedListener\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.MarginItemDecoration\nimport com.hippo.easyrecyclerview.SimpleHolder\nimport com.hippo.ehviewer.GetText.getString\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.data.ListUrlBuilder\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.widget.RadioGridGroup\nimport com.hippo.yorozuya.ViewUtils\n\n@SuppressLint(\"InflateParams\")\nclass SearchLayout @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0,\n) : EasyRecyclerView(\n    context,\n    attrs,\n    defStyle,\n),\n    CompoundButton.OnCheckedChangeListener,\n    View.OnClickListener,\n    ImageSearchLayout.Helper,\n    OnTabSelectedListener {\n    private var mInflater: LayoutInflater? = null\n\n    @SearchMode\n    private var mSearchMode = SEARCH_MODE_NORMAL\n    private var mEnableAdvance = false\n    private var mNormalView: View? = null\n    private var mCategoryTable: CategoryTable? = null\n    private var mNormalSearchMode: RadioGridGroup? = null\n    private var mNormalSearchModeHelp: ImageView? = null\n\n    @SuppressLint(\"UseSwitchCompatOrMaterialCode\")\n    private var mEnableAdvanceSwitch: Switch? = null\n    private var mAdvanceView: View? = null\n    private var mTableAdvanceSearch: AdvanceSearchTable? = null\n    private var mImageView: ImageSearchLayout? = null\n    private var mActionView: View? = null\n    private var mAction: TabLayout? = null\n    private var mLayoutManager: LinearLayoutManager? = null\n    private var mAdapter: SearchAdapter? = null\n    private var mHelper: Helper? = null\n\n    init {\n        val resources = context.resources\n        mInflater = LayoutInflater.from(context)\n        mLayoutManager = SearchLayoutManager(context)\n        mAdapter = SearchAdapter()\n        mAdapter!!.setHasStableIds(true)\n        setLayoutManager(mLayoutManager)\n        setAdapter(mAdapter)\n        setHasFixedSize(true)\n        setClipToPadding(false)\n        (itemAnimator as DefaultItemAnimator?)!!.supportsChangeAnimations = false\n        val interval = resources.getDimensionPixelOffset(R.dimen.search_layout_interval)\n        val paddingH = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_h)\n        val paddingV = resources.getDimensionPixelOffset(R.dimen.search_layout_margin_v)\n        val decoration = MarginItemDecoration(\n            interval,\n            paddingH,\n            paddingV,\n            paddingH,\n            paddingV,\n        )\n        addItemDecoration(decoration)\n        decoration.applyPaddings(this)\n\n        // Create normal view\n        val normalView = mInflater!!.inflate(R.layout.search_normal, null)\n        mNormalView = normalView\n        mCategoryTable = normalView.findViewById(R.id.search_category_table)\n        mNormalSearchMode = normalView.findViewById(R.id.normal_search_mode)\n        mNormalSearchModeHelp = normalView.findViewById(R.id.normal_search_mode_help)\n        mEnableAdvanceSwitch = normalView.findViewById(R.id.search_enable_advance)\n        mNormalSearchModeHelp!!.setOnClickListener(this)\n        mEnableAdvanceSwitch!!.setOnCheckedChangeListener(this)\n        mEnableAdvanceSwitch!!.setSwitchPadding(resources.getDimensionPixelSize(R.dimen.switch_padding))\n\n        // Create advance view\n        mAdvanceView = mInflater!!.inflate(R.layout.search_advance, null)\n        mTableAdvanceSearch = mAdvanceView!!.findViewById(R.id.search_advance_search_table)\n\n        // Create image view\n        mImageView = mInflater!!.inflate(R.layout.search_image, null) as ImageSearchLayout\n        mImageView!!.setHelper(this)\n\n        // Create action view\n        mActionView = mInflater!!.inflate(R.layout.search_action, null)\n        mActionView!!.setLayoutParams(\n            LayoutParams(\n                LayoutParams.MATCH_PARENT,\n                LayoutParams.WRAP_CONTENT,\n            ),\n        )\n        mAction = mActionView!!.findViewById(R.id.action)\n        mAction!!.addOnTabSelectedListener(this)\n    }\n\n    fun setHelper(helper: Helper?) {\n        mHelper = helper\n    }\n\n    fun scrollSearchContainerToTop() {\n        mLayoutManager!!.scrollToPositionWithOffset(0, 0)\n    }\n\n    fun setImageUri(imageUri: Uri?) {\n        mImageView!!.setImageUri(imageUri)\n    }\n\n    fun setNormalSearchMode(id: Int) {\n        mNormalSearchMode!!.check(id)\n    }\n\n    override fun onSelectImage() {\n        if (mHelper != null) {\n            mHelper!!.onSelectImage()\n        }\n    }\n\n    override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {\n        super.dispatchSaveInstanceState(container)\n        mNormalView!!.saveHierarchyState(container)\n        mAdvanceView!!.saveHierarchyState(container)\n        mImageView!!.saveHierarchyState(container)\n        mActionView!!.saveHierarchyState(container)\n    }\n\n    override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {\n        super.dispatchRestoreInstanceState(container)\n        mNormalView!!.restoreHierarchyState(container)\n        mAdvanceView!!.restoreHierarchyState(container)\n        mImageView!!.restoreHierarchyState(container)\n        mActionView!!.restoreHierarchyState(container)\n    }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val state = Bundle()\n        state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState())\n        state.putInt(STATE_KEY_SEARCH_MODE, mSearchMode)\n        state.putBoolean(STATE_KEY_ENABLE_ADVANCE, mEnableAdvance)\n        return state\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        if (state is Bundle) {\n            super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER)!!)\n            mSearchMode = state.getInt(STATE_KEY_SEARCH_MODE)\n            mEnableAdvance = state.getBoolean(STATE_KEY_ENABLE_ADVANCE)\n        }\n    }\n\n    override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {\n        if (buttonView === mEnableAdvanceSwitch) {\n            post {\n                mEnableAdvance = isChecked\n                if (mSearchMode == SEARCH_MODE_NORMAL) {\n                    if (mEnableAdvance) {\n                        mAdapter!!.notifyItemInserted(1)\n                    } else {\n                        mAdapter!!.notifyItemRemoved(1)\n                    }\n                    if (mHelper != null) {\n                        mHelper!!.onChangeSearchMode()\n                    }\n                }\n            }\n        }\n    }\n\n    fun formatListUrlBuilder(urlBuilder: ListUrlBuilder, query: String?) {\n        urlBuilder.reset()\n        when (mSearchMode) {\n            SEARCH_MODE_NORMAL -> {\n                val nsMode = mNormalSearchMode!!.checkedRadioButtonId\n                when (nsMode) {\n                    R.id.search_subscription_search -> {\n                        urlBuilder.mode = ListUrlBuilder.MODE_SUBSCRIPTION\n                    }\n                    R.id.search_specify_uploader -> {\n                        urlBuilder.mode = ListUrlBuilder.MODE_UPLOADER\n                    }\n                    R.id.search_specify_tag -> {\n                        urlBuilder.mode = ListUrlBuilder.MODE_TAG\n                    }\n                    else -> {\n                        urlBuilder.mode = ListUrlBuilder.MODE_NORMAL\n                    }\n                }\n                urlBuilder.keyword = query\n                urlBuilder.category = mCategoryTable!!.category\n                if (mEnableAdvance) {\n                    urlBuilder.advanceSearch = mTableAdvanceSearch!!.advanceSearch\n                    urlBuilder.minRating = mTableAdvanceSearch!!.minRating\n                    val pageFrom = mTableAdvanceSearch!!.pageFrom\n                    val pageTo = mTableAdvanceSearch!!.pageTo\n                    if (pageFrom != -1 && pageFrom > 1000) {\n                        throw EhException(getString(R.string.search_sp_err0))\n                    } else if (pageTo != -1 && pageTo < 10) {\n                        throw EhException(getString(R.string.search_sp_err1))\n                    } else if (pageFrom != -1 && pageTo != -1 && pageTo - pageFrom < 20) {\n                        throw EhException(getString(R.string.search_sp_err2))\n                    } else if (pageFrom != -1 && pageTo != -1 && pageFrom.toFloat() / pageTo > 0.5) {\n                        throw EhException(getString(R.string.search_sp_err3))\n                    }\n                    urlBuilder.pageFrom = pageFrom\n                    urlBuilder.pageTo = pageTo\n                }\n            }\n            SEARCH_MODE_IMAGE -> {\n                urlBuilder.mode = ListUrlBuilder.MODE_IMAGE_SEARCH\n                mImageView!!.formatListUrlBuilder(urlBuilder)\n            }\n        }\n    }\n\n    override fun onClick(v: View) {\n        if (mNormalSearchModeHelp === v) {\n            AlertDialog.Builder(context)\n                .setMessage(R.string.search_tip)\n                .show()\n        }\n    }\n\n    override fun onTabSelected(tab: TabLayout.Tab) {\n        post { setSearchMode(tab.position) }\n    }\n\n    fun setSearchMode(@SearchMode mode: Int) {\n        val oldItemCount = mAdapter!!.getItemCount()\n        mSearchMode = mode\n        val newItemCount = mAdapter!!.getItemCount()\n        mAdapter!!.notifyItemRangeRemoved(0, oldItemCount - 1)\n        mAdapter!!.notifyItemRangeInserted(0, newItemCount - 1)\n        if (mHelper != null) {\n            mHelper!!.onChangeSearchMode()\n        }\n    }\n\n    override fun onTabUnselected(tab: TabLayout.Tab) {}\n    override fun onTabReselected(tab: TabLayout.Tab) {}\n\n    @IntDef(SEARCH_MODE_NORMAL, SEARCH_MODE_IMAGE)\n    @Retention(AnnotationRetention.SOURCE)\n    private annotation class SearchMode\n    interface Helper {\n        fun onChangeSearchMode()\n        fun onSelectImage()\n    }\n\n    internal class SearchLayoutManager(context: Context?) : LinearLayoutManager(context) {\n        override fun onLayoutChildren(recycler: Recycler, state: State) {\n            try {\n                super.onLayoutChildren(recycler, state)\n            } catch (e: IndexOutOfBoundsException) {\n                e.printStackTrace()\n            }\n        }\n    }\n\n    private inner class SearchAdapter : Adapter<SimpleHolder>() {\n        override fun getItemCount(): Int {\n            var count = SEARCH_ITEM_COUNT_ARRAY[mSearchMode]\n            if (mSearchMode == SEARCH_MODE_NORMAL && !mEnableAdvance) {\n                count--\n            }\n            return count\n        }\n\n        override fun getItemViewType(position: Int): Int {\n            var type = SEARCH_ITEM_TYPE[mSearchMode][position]\n            if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {\n                type = ITEM_TYPE_ACTION\n            }\n            return type\n        }\n\n        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SimpleHolder {\n            val view: View?\n            if (viewType == ITEM_TYPE_ACTION) {\n                ViewUtils.removeFromParent(mActionView)\n                view = mActionView\n            } else {\n                view = mInflater!!.inflate(R.layout.search_category, parent, false)\n                val title = view.findViewById<TextView>(R.id.category_title)\n                val content = view.findViewById<FrameLayout>(R.id.category_content)\n                when (viewType) {\n                    ITEM_TYPE_NORMAL -> {\n                        title.setText(R.string.search_normal)\n                        ViewUtils.removeFromParent(mNormalView)\n                        content.addView(mNormalView)\n                    }\n                    ITEM_TYPE_NORMAL_ADVANCE -> {\n                        title.setText(R.string.search_advance)\n                        ViewUtils.removeFromParent(mAdvanceView)\n                        content.addView(mAdvanceView)\n                    }\n                    ITEM_TYPE_IMAGE -> {\n                        title.setText(R.string.search_image)\n                        ViewUtils.removeFromParent(mImageView)\n                        content.addView(mImageView)\n                    }\n                }\n            }\n            return SimpleHolder(view!!)\n        }\n\n        override fun onBindViewHolder(holder: SimpleHolder, position: Int) {\n            if (holder.itemViewType == ITEM_TYPE_ACTION) {\n                mAction!!.selectTab(mAction!!.getTabAt(mSearchMode))\n            }\n        }\n\n        override fun getItemId(position: Int): Long {\n            var type = SEARCH_ITEM_TYPE[mSearchMode][position]\n            if (mSearchMode == SEARCH_MODE_NORMAL && position == 1 && !mEnableAdvance) {\n                type = ITEM_TYPE_ACTION\n            }\n            return type.toLong()\n        }\n    }\n\n    companion object {\n        const val SEARCH_MODE_NORMAL = 0\n        const val SEARCH_MODE_IMAGE = 1\n        private const val STATE_KEY_SUPER = \"super\"\n        private const val STATE_KEY_SEARCH_MODE = \"search_mode\"\n        private const val STATE_KEY_ENABLE_ADVANCE = \"enable_advance\"\n        private const val ITEM_TYPE_NORMAL = 0\n        private const val ITEM_TYPE_NORMAL_ADVANCE = 1\n        private const val ITEM_TYPE_IMAGE = 2\n        private const val ITEM_TYPE_ACTION = 3\n        private val SEARCH_ITEM_COUNT_ARRAY = intArrayOf(\n            3,\n            2,\n        )\n        private val SEARCH_ITEM_TYPE = arrayOf(\n            intArrayOf(ITEM_TYPE_NORMAL, ITEM_TYPE_NORMAL_ADVANCE, ITEM_TYPE_ACTION),\n            intArrayOf(\n                ITEM_TYPE_IMAGE,\n                ITEM_TYPE_ACTION,\n            ),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/SeekBarPanel.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.ehviewer.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.MotionEvent\nimport android.widget.LinearLayout\nimport android.widget.SeekBar\nimport com.hippo.ehviewer.R\nimport com.hippo.yorozuya.ViewUtils\n\nclass SeekBarPanel @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0,\n) : LinearLayout(\n    context,\n    attrs,\n    defStyle,\n) {\n    private val mLocation = IntArray(2)\n    private var mSeekBar: SeekBar? = null\n\n    init {\n        post {\n            val rootWindowInsets = getRootWindowInsets()\n            if (rootWindowInsets != null) {\n                @Suppress(\"DEPRECATION\")\n                setPadding(0, 0, 0, rootWindowInsets.systemWindowInsetBottom)\n            }\n        }\n    }\n\n    override fun onFinishInflate() {\n        super.onFinishInflate()\n        mSeekBar = ViewUtils.`$$`(this, R.id.seek_bar) as SeekBar\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onTouchEvent(event: MotionEvent): Boolean {\n        if (mSeekBar == null) {\n            return super.onTouchEvent(event)\n        } else {\n            ViewUtils.getLocationInAncestor(mSeekBar, mLocation, this)\n            val offsetX = -mLocation[0].toFloat()\n            val offsetY = -mLocation[1].toFloat()\n            event.offsetLocation(offsetX, offsetY)\n            mSeekBar!!.onTouchEvent(event)\n            event.offsetLocation(-offsetX, -offsetY)\n            return true\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/SimpleRatingView.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.graphics.Canvas;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport androidx.core.content.ContextCompat;\n\nimport com.hippo.ehviewer.R;\nimport com.hippo.yorozuya.MathUtils;\n\n/**\n * 5 stars, from 0 to 10\n */\npublic class SimpleRatingView extends View {\n    private Drawable mStarDrawable;\n    private Drawable mStarHalfDrawable;\n    private Drawable mStarOutlineDrawable;\n    private int mRatingSize;\n    private int mRatingInterval;\n\n    private float mRating;\n    private int mRatingInt;\n\n    public SimpleRatingView(Context context) {\n        super(context);\n        init(context);\n    }\n\n    public SimpleRatingView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context);\n    }\n\n    public SimpleRatingView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context);\n    }\n\n    private void init(Context context) {\n        Resources resources = context.getResources();\n        mStarDrawable = ContextCompat.getDrawable(context, R.drawable.v_star_x16);\n        mStarHalfDrawable = ContextCompat.getDrawable(context, R.drawable.v_star_half_x16);\n        mStarOutlineDrawable = ContextCompat.getDrawable(context, R.drawable.v_star_outline_x16);\n        mRatingSize = resources.getDimensionPixelOffset(R.dimen.rating_size);\n        mRatingInterval = resources.getDimensionPixelOffset(R.dimen.rating_interval);\n\n        mStarDrawable.setBounds(0, 0, mRatingSize, mRatingSize);\n        mStarHalfDrawable.setBounds(0, 0, mRatingSize, mRatingSize);\n        mStarOutlineDrawable.setBounds(0, 0, mRatingSize, mRatingSize);\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        setMeasuredDimension(mRatingSize * 5 + mRatingInterval * 4, mRatingSize);\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        int ratingInt = mRatingInt;\n        int step = mRatingSize + mRatingInterval;\n        int numStar = ratingInt / 2;\n        int numStarHalf = ratingInt % 2;\n        int saved = canvas.save();\n        while (numStar-- > 0) {\n            mStarDrawable.draw(canvas);\n            canvas.translate(step, 0);\n        }\n        if (numStarHalf == 1) {\n            mStarHalfDrawable.draw(canvas);\n            canvas.translate(step, 0);\n        }\n        int numOutline = 5 - numStar - numStarHalf;\n        while (numOutline-- > 0) {\n            mStarOutlineDrawable.draw(canvas);\n            canvas.translate(step, 0);\n        }\n        canvas.restoreToCount(saved);\n    }\n\n    public float getRating() {\n        return mRating;\n    }\n\n    public void setRating(float rating) {\n        if (mRating != rating) {\n            mRating = rating;\n            int ratingInt = MathUtils.clamp((int) Math.ceil(rating * 2), 0, 10);\n            if (mRatingInt != ratingInt) {\n                mRatingInt = ratingInt;\n                invalidate();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/ehviewer/widget/TileThumb.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.ehviewer.widget;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport com.hippo.widget.LoadImageView;\nimport com.hippo.yorozuya.MathUtils;\n\npublic class TileThumb extends LoadImageView {\n    private static final float MIN_ASPECT = 0.33f;\n    private static final float MAX_ASPECT = 1.5f;\n    private static final float DEFAULT_ASPECT = 0.67f;\n\n    public TileThumb(Context context) {\n        super(context);\n    }\n\n    public TileThumb(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public TileThumb(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    public void setThumbSize(int thumbWidth, int thumbHeight) {\n        float aspect;\n        if (thumbWidth > 0 && thumbHeight > 0) {\n            aspect = MathUtils.clamp(thumbWidth / (float) thumbHeight, MIN_ASPECT, MAX_ASPECT);\n        } else {\n            aspect = DEFAULT_ASPECT;\n        }\n        setAspect(aspect);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/DownUpDetector.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport android.view.MotionEvent;\n\nclass DownUpDetector {\n    private final DownUpListener mListener;\n    private boolean mStillDown;\n\n    public DownUpDetector(DownUpListener listener) {\n        mListener = listener;\n    }\n\n    private void setState(boolean down, MotionEvent e) {\n        if (down == mStillDown) return;\n        mStillDown = down;\n        if (down) {\n            mListener.onDown(e);\n        } else {\n            mListener.onUp(e);\n        }\n    }\n\n    public void onTouchEvent(MotionEvent ev) {\n        switch (ev.getActionMasked()) {\n            case MotionEvent.ACTION_DOWN -> setState(true, ev);\n            case MotionEvent.ACTION_POINTER_DOWN -> mListener.onPointerDown(ev);\n            case MotionEvent.ACTION_UP -> {\n                mListener.onPointerUp(ev);\n                setState(false, ev);\n            }\n            case MotionEvent.ACTION_CANCEL -> setState(false, ev);\n        }\n    }\n\n    public boolean isDown() {\n        return mStillDown;\n    }\n\n    public interface DownUpListener {\n        void onDown(MotionEvent e);\n\n        void onUp(MotionEvent e);\n\n        void onPointerDown(MotionEvent e);\n\n        void onPointerUp(MotionEvent e);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/Fling.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport android.content.Context;\nimport android.hardware.SensorManager;\nimport android.view.ViewConfiguration;\nimport android.view.animation.Interpolator;\n\nimport com.hippo.glview.anim.Animation;\n\nabstract class Fling extends Animation {\n    private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));\n    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)\n    private static final float START_TENSION = 0.5f;\n    private static final float END_TENSION = 1.0f;\n    private static final float P1 = START_TENSION * INFLEXION;\n    private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);\n    private static final float FLING_FRICTION = ViewConfiguration.getScrollFriction();\n\n    private static final int NB_SAMPLES = 100;\n    private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];\n    private static final Interpolator FLING_INTERPOLATOR = input -> {\n        final int index = (int) (NB_SAMPLES * input);\n        float distanceCoef = 1.f;\n        float velocityCoef;\n        if (index < NB_SAMPLES) {\n            final float t_inf = (float) index / NB_SAMPLES;\n            final float t_sup = (float) (index + 1) / NB_SAMPLES;\n            final float d_inf = SPLINE_POSITION[index];\n            final float d_sup = SPLINE_POSITION[index + 1];\n            velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);\n            distanceCoef = d_inf + (input - t_inf) * velocityCoef;\n        }\n        return distanceCoef;\n    };\n    private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];\n\n    static {\n        float x_min = 0.0f;\n        float y_min = 0.0f;\n        for (int i = 0; i < NB_SAMPLES; i++) {\n            final float alpha = (float) i / NB_SAMPLES;\n\n            float x_max = 1.0f;\n            float x, tx, coef;\n            while (true) {\n                x = x_min + (x_max - x_min) / 2.0f;\n                coef = 3.0f * x * (1.0f - x);\n                tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;\n                if (Math.abs(tx - alpha) < 1E-5) break;\n                if (tx > alpha) x_max = x;\n                else x_min = x;\n            }\n            SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;\n\n            float y_max = 1.0f;\n            float y, dy;\n            while (true) {\n                y = y_min + (y_max - y_min) / 2.0f;\n                coef = 3.0f * y * (1.0f - y);\n                dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;\n                if (Math.abs(dy - alpha) < 1E-5) break;\n                if (dy > alpha) y_max = y;\n                else y_min = y;\n            }\n            SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;\n        }\n        SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;\n    }\n\n    private final float mPhysicalCoeff;\n\n    public Fling(Context context) {\n        final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;\n        mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)\n                * 39.37f // inch/meter\n                * ppi\n                * 0.84f; // look and feel tuning\n        setInterpolator(FLING_INTERPOLATOR);\n    }\n\n    private double getSplineDeceleration(int velocity) {\n        return Math.log(INFLEXION * Math.abs(velocity) / (FLING_FRICTION * mPhysicalCoeff));\n    }\n\n    /* Returns the duration, expressed in milliseconds */\n    protected int getSplineFlingDuration(int velocity) {\n        final double l = getSplineDeceleration(velocity);\n        final double decelMinusOne = DECELERATION_RATE - 1.0;\n        return (int) (1000.0 * Math.exp(l / decelMinusOne));\n    }\n\n    protected double getSplineFlingDistance(int velocity) {\n        final double l = getSplineDeceleration(velocity);\n        final double decelMinusOne = DECELERATION_RATE - 1.0;\n        return FLING_FRICTION * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);\n    }\n\n    /**\n     * Modifies mDuration to the duration it takes to get from start to newFinal using the\n     * spline interpolation. The previous duration was needed to get to oldFinal.\n     **/\n    protected int adjustDuration(int oldFinal, int newFinal, int duration) {\n        final float x = Math.abs((float) newFinal / oldFinal);\n        final int index = (int) (NB_SAMPLES * x);\n        if (index < NB_SAMPLES) {\n            final float x_inf = (float) index / NB_SAMPLES;\n            final float x_sup = (float) (index + 1) / NB_SAMPLES;\n            final float t_inf = SPLINE_TIME[index];\n            final float t_sup = SPLINE_TIME[index + 1];\n            final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);\n            duration *= (int) timeCoef;\n        }\n        return duration;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/GalleryPageView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport com.hippo.glview.glrenderer.BasicTexture;\nimport com.hippo.glview.glrenderer.Texture;\nimport com.hippo.glview.image.GLImageMovableTextView;\nimport com.hippo.glview.image.ImageMovableTextTexture;\nimport com.hippo.glview.image.ImageTexture;\nimport com.hippo.glview.view.Gravity;\nimport com.hippo.glview.widget.GLFrameLayout;\nimport com.hippo.glview.widget.GLLinearLayout;\nimport com.hippo.glview.widget.GLProgressView;\nimport com.hippo.glview.widget.GLTextureView;\n\npublic class GalleryPageView extends GLFrameLayout {\n    public static final int INVALID_INDEX = -1;\n\n    public static final float PROGRESS_GONE = -1.0f;\n    public static final float PROGRESS_INDETERMINATE = -2.0f;\n\n    private final ImageView mImage;\n    private final GLLinearLayout mInfo;\n    private final GLImageMovableTextView mPage;\n    private final GLTextureView mError;\n    private final GLProgressView mProgress;\n\n    private final int mMinHeight;\n\n    private int mIndex = INVALID_INDEX;\n\n    public GalleryPageView(ImageMovableTextTexture pageTextTexture,\n                           int progressColor, int progressBgColor, int progressSize,\n                           int minHeight, int infoInterval) {\n        // Add image\n        mImage = new ImageView();\n        GravityLayoutParams glp = new GravityLayoutParams(LayoutParams.MATCH_PARENT,\n                LayoutParams.MATCH_PARENT);\n        addComponent(mImage, glp);\n\n        // Add other panel\n        mInfo = new GLLinearLayout();\n        mInfo.setOrientation(GLLinearLayout.VERTICAL);\n        mInfo.setInterval(infoInterval);\n        glp = new GravityLayoutParams(LayoutParams.WRAP_CONTENT,\n                LayoutParams.WRAP_CONTENT);\n        glp.gravity = Gravity.CENTER;\n        addComponent(mInfo, glp);\n\n        // Add page\n        mPage = new GLImageMovableTextView();\n        mPage.setTextTexture(pageTextTexture);\n        GLLinearLayout.LayoutParams lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                LayoutParams.WRAP_CONTENT);\n        lp.gravity = Gravity.CENTER_HORIZONTAL;\n        mInfo.addComponent(mPage, lp);\n\n        // Add error\n        mError = new GLTextureView();\n        lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                LayoutParams.WRAP_CONTENT);\n        lp.gravity = Gravity.CENTER_HORIZONTAL;\n        mInfo.addComponent(mError, lp);\n\n        // Add progress\n        mProgress = new GLProgressView();\n        mProgress.setBgColor(progressBgColor);\n        mProgress.setColor(progressColor);\n        mProgress.setMinimumWidth(progressSize);\n        mProgress.setMinimumHeight(progressSize);\n        lp = new GLLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,\n                LayoutParams.WRAP_CONTENT);\n        lp.gravity = Gravity.CENTER_HORIZONTAL;\n        mInfo.addComponent(mProgress, lp);\n\n        mMinHeight = minHeight;\n    }\n\n    @Override\n    protected int getSuggestedMinimumHeight() {\n        // The height of the actual image may be smaller than mPageMinHeight.\n        // Set min height as 0 when the image is visible.\n        // For PageLayoutManager, min height is useless.\n        if (mImage.getVisibility() == VISIBLE) {\n            return 0;\n        } else {\n            return mMinHeight;\n        }\n    }\n\n    int getIndex() {\n        return mIndex;\n    }\n\n    void setIndex(int index) {\n        mIndex = index;\n    }\n\n    public void showImage() {\n        mImage.setVisibility(VISIBLE);\n        mInfo.setVisibility(GONE);\n    }\n\n    public void showInfo() {\n        // For image valid rect\n        mImage.setVisibility(INVISIBLE);\n        mInfo.setVisibility(VISIBLE);\n    }\n\n    private void unbindImage() {\n        ImageTexture texture = mImage.getImageTexture();\n        if (texture != null) {\n            mImage.setImageTexture(null);\n            texture.recycle();\n        }\n    }\n\n    public void setImage(ImageTexture imageTexture) {\n        unbindImage();\n        if (imageTexture != null) {\n            mImage.setImageTexture(imageTexture);\n        }\n    }\n\n    public void setPage(int page) {\n        mPage.setVisibility(VISIBLE);\n        mPage.setText(Integer.toString(page));\n    }\n\n    public void setProgress(float progress) {\n        if (progress == PROGRESS_GONE) {\n            mProgress.setVisibility(GONE);\n        } else if (progress == PROGRESS_INDETERMINATE) {\n            mProgress.setVisibility(VISIBLE);\n            mProgress.setIndeterminate(true);\n        } else {\n            mProgress.setVisibility(VISIBLE);\n            mProgress.setIndeterminate(false);\n            mProgress.setProgress(progress);\n        }\n    }\n\n    private void unbindError() {\n        Texture texture = mError.getTexture();\n        if (texture != null) {\n            mError.setTexture(null);\n            if (texture instanceof BasicTexture) {\n                ((BasicTexture) texture).recycle();\n            }\n        }\n    }\n\n    public void setError(String error, GalleryView galleryView) {\n        unbindError();\n        if (error == null) {\n            mError.setVisibility(GONE);\n        } else {\n            mError.setVisibility(VISIBLE);\n            galleryView.bindErrorView(mError, error);\n        }\n    }\n\n    ImageView getImageView() {\n        return mImage;\n    }\n\n    boolean isLoaded() {\n        return mImage.getVisibility() == VISIBLE;\n    }\n\n    boolean isError() {\n        return mError.getVisibility() == VISIBLE;\n    }\n\n    boolean isUnderInfo(float x, float y) {\n        return mInfo.bounds().contains((int) x, (int) y);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/GalleryProvider.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.glgallery\n\nimport androidx.annotation.CallSuper\nimport androidx.annotation.IntDef\nimport androidx.collection.lruCache\nimport com.hippo.ehviewer.Settings\nimport com.hippo.glview.glrenderer.GLCanvas\nimport com.hippo.glview.image.ImageWrapper\nimport com.hippo.glview.view.GLRoot\nimport com.hippo.glview.view.GLRoot.OnGLIdleListener\nimport com.hippo.image.Image\nimport com.hippo.util.isAtLeastO\nimport com.hippo.yorozuya.ConcurrentPool\nimport com.hippo.yorozuya.MathUtils\nimport com.hippo.yorozuya.OSUtils\n\nabstract class GalleryProvider {\n    private val mNotifyTaskPool = ConcurrentPool<NotifyTask>(5)\n    private val mImageCache = lruCache<Int, ImageWrapper>(\n        maxSize = if (isAtLeastO) {\n            (OSUtils.getTotalMemory() / 12).toInt().coerceIn(MIN_CACHE_SIZE, MAX_CACHE_SIZE)\n        } else {\n            (OSUtils.getAppMaxMemory() / 3 * 2).toInt()\n        },\n        sizeOf = { _, v -> v.width * v.height * if (v.animated) 20 else 4 },\n        onEntryRemoved = { _, _, o, _ -> o.release() },\n    )\n    private val mPreloads = MathUtils.clamp(Settings.preloadImage, 0, 100)\n\n    @Volatile\n    private var mListener: Listener? = null\n\n    @Volatile\n    private var mGLRoot: GLRoot? = null\n\n    abstract suspend fun awaitReady(): Boolean\n\n    abstract val isReady: Boolean\n\n    abstract fun start()\n\n    @CallSuper\n    open fun stop() {\n        mImageCache.evictAll()\n    }\n\n    fun setGLRoot(glRoot: GLRoot) {\n        mGLRoot = glRoot\n    }\n\n    abstract val size: Int\n\n    private var lastRequestIndex = -1\n\n    fun request(index: Int) {\n        mImageCache[index]?.let {\n            notifyPageSucceed(index, it)\n        } ?: onRequest(index)\n\n        val preloadRange = if (index >= lastRequestIndex) {\n            index + 1..(index + mPreloads).coerceAtMost(size - 1)\n        } else {\n            index - 1 downTo (index - mPreloads).coerceAtLeast(0)\n        }\n        val start = if (preloadRange.step > 0) preloadRange.first else preloadRange.last\n        val end = if (preloadRange.step > 0) preloadRange.last else preloadRange.first\n        preloadPages(\n            preloadRange.filter { mImageCache[it] == null },\n            start - 8 to end + 8,\n        )\n\n        lastRequestIndex = index\n    }\n\n    fun forceRequest(index: Int) {\n        onForceRequest(index)\n    }\n\n    fun removeCache(index: Int) {\n        mImageCache.remove(index)\n    }\n\n    protected abstract fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>)\n\n    protected abstract fun onRequest(index: Int)\n\n    protected abstract fun onForceRequest(index: Int)\n\n    fun cancelRequest(index: Int) {\n        onCancelRequest(index)\n    }\n\n    protected abstract fun onCancelRequest(index: Int)\n\n    fun setListener(listener: Listener?) {\n        mListener = listener\n    }\n\n    fun notifyDataChanged(index: Int) {\n        notify(NotifyTask.TYPE_DATA_CHANGED, index, 0.0f, null, null)\n    }\n\n    fun notifyPageWait(index: Int) {\n        notify(NotifyTask.TYPE_WAIT, index, 0.0f, null, null)\n    }\n\n    fun notifyPagePercent(index: Int, percent: Float) {\n        notify(NotifyTask.TYPE_PERCENT, index, percent, null, null)\n    }\n\n    fun notifyPageSucceed(index: Int, image: Image) {\n        val imageWrapper = ImageWrapper(image)\n        if (imageWrapper.obtain()) mImageCache.put(index, imageWrapper)\n        notifyPageSucceed(index, imageWrapper)\n    }\n\n    private fun notifyPageSucceed(index: Int, image: ImageWrapper) {\n        notify(NotifyTask.TYPE_SUCCEED, index, 0.0f, image, null)\n    }\n\n    fun notifyPageFailed(index: Int, error: String?) {\n        notify(NotifyTask.TYPE_FAILED, index, 0.0f, null, error)\n    }\n\n    private fun notify(\n        @NotifyTask.Type type: Int,\n        index: Int,\n        percent: Float,\n        image: ImageWrapper?,\n        error: String?,\n    ) {\n        val listener = mListener ?: return\n        val glRoot = mGLRoot ?: return\n        val task = mNotifyTaskPool.pop() ?: NotifyTask(listener, mNotifyTaskPool)\n        task.setData(type, index, percent, image, error)\n        glRoot.addOnGLIdleListener(task)\n    }\n\n    interface Listener {\n        fun onDataChanged()\n        fun onPageWait(index: Int)\n        fun onPagePercent(index: Int, percent: Float)\n        fun onPageSucceed(index: Int, image: ImageWrapper?)\n        fun onPageFailed(index: Int, error: String?)\n        fun onDataChanged(index: Int)\n    }\n\n    private class NotifyTask(\n        private val mListener: Listener,\n        private val mPool: ConcurrentPool<NotifyTask>,\n    ) : OnGLIdleListener {\n        @Type\n        private var mType = 0\n        private var mIndex = 0\n        private var mPercent = 0f\n        private var mImage: ImageWrapper? = null\n        private var mError: String? = null\n        fun setData(\n            @Type type: Int,\n            index: Int,\n            percent: Float,\n            image: ImageWrapper?,\n            error: String?,\n        ) {\n            mType = type\n            mIndex = index\n            mPercent = percent\n            mImage = image\n            mError = error\n        }\n\n        override fun onGLIdle(canvas: GLCanvas, renderRequested: Boolean): Boolean {\n            when (mType) {\n                TYPE_DATA_CHANGED -> if (mIndex < 0) {\n                    mListener.onDataChanged()\n                } else {\n                    mListener.onDataChanged(mIndex)\n                }\n                TYPE_WAIT -> mListener.onPageWait(mIndex)\n                TYPE_PERCENT -> mListener.onPagePercent(mIndex, mPercent)\n                TYPE_SUCCEED -> mListener.onPageSucceed(mIndex, mImage)\n                TYPE_FAILED -> mListener.onPageFailed(mIndex, mError)\n            }\n\n            // Clean data\n            mImage = null\n            mError = null\n            // Push back\n            mPool.push(this)\n            return false\n        }\n\n        @IntDef(TYPE_DATA_CHANGED, TYPE_WAIT, TYPE_PERCENT, TYPE_SUCCEED, TYPE_FAILED)\n        @Retention(\n            AnnotationRetention.SOURCE,\n        )\n        annotation class Type\n        companion object {\n            const val TYPE_DATA_CHANGED = 0\n            const val TYPE_WAIT = 1\n            const val TYPE_PERCENT = 2\n            const val TYPE_SUCCEED = 3\n            const val TYPE_FAILED = 4\n        }\n    }\n\n    companion object {\n        private const val MAX_CACHE_SIZE = 512 * 1024 * 1024\n        private const val MIN_CACHE_SIZE = 256 * 1024 * 1024\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/GalleryView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.graphics.Rect;\nimport android.graphics.Typeface;\nimport android.view.MotionEvent;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.hippo.glview.glrenderer.BasicTexture;\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.glrenderer.StringTexture;\nimport com.hippo.glview.glrenderer.Texture;\nimport com.hippo.glview.image.ImageMovableTextTexture;\nimport com.hippo.glview.util.GalleryUtils;\nimport com.hippo.glview.view.AnimationTime;\nimport com.hippo.glview.view.GLRoot;\nimport com.hippo.glview.view.GLView;\nimport com.hippo.glview.widget.GLTextureView;\nimport com.hippo.yorozuya.MathUtils;\nimport com.hippo.yorozuya.Pool;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic final class GalleryView extends GLView implements GestureRecognizer.Listener {\n    public static final int LAYOUT_LEFT_TO_RIGHT = 0;\n    public static final int LAYOUT_RIGHT_TO_LEFT = 1;\n    public static final int LAYOUT_TOP_TO_BOTTOM = 2;\n    public static final int SCALE_ORIGIN = ImageView.SCALE_ORIGIN;\n    public static final int SCALE_FIT_WIDTH = ImageView.SCALE_FIT_WIDTH;\n    public static final int SCALE_FIT_HEIGHT = ImageView.SCALE_FIT_HEIGHT;\n    public static final int SCALE_FIT = ImageView.SCALE_FIT;\n    public static final int SCALE_FIXED = ImageView.SCALE_FIXED;\n    public static final int START_POSITION_TOP_LEFT = ImageView.START_POSITION_TOP_LEFT;\n    public static final int START_POSITION_TOP_RIGHT = ImageView.START_POSITION_TOP_RIGHT;\n    public static final int START_POSITION_BOTTOM_LEFT = ImageView.START_POSITION_BOTTOM_LEFT;\n    public static final int START_POSITION_BOTTOM_RIGHT = ImageView.START_POSITION_BOTTOM_RIGHT;\n    public static final int START_POSITION_CENTER = ImageView.START_POSITION_CENTER;\n    private static final float[] LEFT_AREA = {0.0f, 0.0f, 1.0f / 3.0f, 1f};\n    private static final float[] RIGHT_AREA = {2.0f / 3.0f, 0.0f, 1.0f, 1f};\n    private static final float[] MENU_AREA = {1.0f / 3.0f, 0.0f, 2.0f / 3.0f, 1.0f / 2.0f};\n    private static final float[] SLIDER_AREA = {1.0f / 3.0f, 1.0f / 2.0f, 2.0f / 3.0f, 1.0f};\n    private static final int METHOD_ON_SINGLE_TAP_UP = 0;\n    private static final int METHOD_ON_SINGLE_TAP_CONFIRMED = 1;\n    private static final int METHOD_ON_DOUBLE_TAP = 2;\n    private static final int METHOD_ON_DOUBLE_TAP_CONFIRMED = 3;\n    private static final int METHOD_ON_LONG_PRESS = 4;\n    private static final int METHOD_ON_SCROLL = 5;\n    private static final int METHOD_ON_FLING = 6;\n    private static final int METHOD_ON_SCALE_BEGIN = 7;\n    private static final int METHOD_ON_SCALE = 8;\n    private static final int METHOD_ON_SCALE_END = 9;\n    private static final int METHOD_ON_DOWN = 10;\n    private static final int METHOD_ON_UP = 11;\n    private static final int METHOD_ON_POINTER_DOWN = 12;\n    private static final int METHOD_ON_POINTER_UP = 13;\n    private static final int METHOD_SET_LAYOUT_MODE = 14;\n    private static final int METHOD_SET_CURRENT_PAGE = 15;\n    private static final int METHOD_PAGE_LEFT = 16;\n    private static final int METHOD_PAGE_RIGHT = 17;\n    private static final int METHOD_SET_SCALE_MODE = 18;\n    private static final int METHOD_SET_START_POSITION = 19;\n    private static final int METHOD_ON_ATTACH_TO_ROOT = 20;\n    private static final int METHOD_SET_PAGER_INTERVAL = 21;\n    private static final int METHOD_SET_SCROLL_INTERVAL = 22;\n    private final Context mContext;\n    private final GestureRecognizer mGestureRecognizer;\n    @Nullable\n    private final Listener mListener;\n    private final Pool<GalleryPageView> mGalleryPageViewPool = new Pool<>(5);\n    private final int mBackgroundColor;\n    private final int mPageMinHeight;\n    private final int mPageInfoInterval;\n    private final int mProgressColor;\n    private final int mProgressSize;\n    private final int mPageTextColor;\n    private final int mPageTextSize;\n    private final Typeface mPageTextTypeface;\n    private final int mErrorTextSize;\n    private final int mErrorTextColor;\n    private final String mEmptyString;\n    private final Rect mLeftArea = new Rect();\n    private final Rect mRightArea = new Rect();\n    private final Rect mMenuArea = new Rect();\n    private final Rect mSliderArea = new Rect();\n    private final List<Integer> mMethodList = new ArrayList<>(5);\n    private final List<Object[]> mArgsList = new ArrayList<>(5);\n    private final List<Integer> mMethodListTemp = new ArrayList<>(5);\n    private final List<Object[]> mArgsListTemp = new ArrayList<>(5);\n    private final AtomicInteger mCurrentIndex = new AtomicInteger(GalleryPageView.INVALID_INDEX);\n    private Adapter mAdapter;\n    private ImageMovableTextTexture mPageTextTexture;\n    private PagerLayoutManager mPagerLayoutManager;\n    private ScrollLayoutManager mScrollLayoutManager;\n    @Nullable\n    private LayoutManager mLayoutManager;\n    private GLTextureView mErrorViewCache;\n    private int mPagerInterval;\n    private int mScrollInterval;\n    private boolean mEnableRequestFill = true;\n    private boolean mRequestFill = false;\n    private boolean mWillFill = false;\n    private boolean mScale = false;\n    private boolean mScroll = false;\n    private boolean mFirstScroll = false;\n    private int mLayoutMode;\n    private int mScaleMode;\n    private int mStartPosition;\n    private int mIndex;\n\n    private GalleryView(Builder build) {\n        mContext = build.mContext;\n        mAdapter = build.mAdapter;\n        mAdapter.setGalleryView(this);\n        mListener = build.mListener;\n        mGestureRecognizer = new GestureRecognizer(mContext, this);\n\n        mLayoutMode = build.mLayoutMode;\n        mScaleMode = build.mScaleMode;\n        mStartPosition = build.mStartPosition;\n        mIndex = MathUtils.clamp(build.mStartPage, 0, Integer.MAX_VALUE);\n\n        mBackgroundColor = build.mBackgroundColor;\n        mPageMinHeight = build.mPageMinHeight;\n        mPagerInterval = build.mPagerInterval;\n        mScrollInterval = build.mScrollInterval;\n        mPageInfoInterval = build.mPageInfoInterval;\n        mProgressColor = build.mProgressColor;\n        mProgressSize = build.mProgressSize;\n        mPageTextColor = build.mPageTextColor;\n        mPageTextSize = build.mPageTextSize;\n        mPageTextTypeface = build.mPageTextTypeface;\n        mErrorTextColor = build.mErrorTextColor;\n        mErrorTextSize = build.mErrorTextSize;\n\n        mEmptyString = build.mEmptyString;\n\n        setBackgroundColor(mBackgroundColor);\n    }\n\n    @LayoutMode\n    public static int sanitizeLayoutMode(int layoutMode) {\n        if (layoutMode != GalleryView.LAYOUT_LEFT_TO_RIGHT &&\n                layoutMode != GalleryView.LAYOUT_RIGHT_TO_LEFT &&\n                layoutMode != GalleryView.LAYOUT_TOP_TO_BOTTOM) {\n            return GalleryView.LAYOUT_LEFT_TO_RIGHT;\n        } else {\n            return layoutMode;\n        }\n    }\n\n    @ScaleMode\n    public static int sanitizeScaleMode(int scaleMode) {\n        if (scaleMode != GalleryView.SCALE_ORIGIN &&\n                scaleMode != GalleryView.SCALE_FIT_WIDTH &&\n                scaleMode != GalleryView.SCALE_FIT_HEIGHT &&\n                scaleMode != GalleryView.SCALE_FIT &&\n                scaleMode != GalleryView.SCALE_FIXED) {\n            return GalleryView.SCALE_FIT;\n        } else {\n            return scaleMode;\n        }\n    }\n\n    @StartPosition\n    public static int sanitizeStartPosition(int startPosition) {\n        if (startPosition != GalleryView.START_POSITION_TOP_LEFT &&\n                startPosition != GalleryView.START_POSITION_TOP_RIGHT &&\n                startPosition != GalleryView.START_POSITION_BOTTOM_LEFT &&\n                startPosition != GalleryView.START_POSITION_BOTTOM_RIGHT &&\n                startPosition != GalleryView.START_POSITION_CENTER) {\n            return GalleryView.START_POSITION_TOP_LEFT;\n        } else {\n            return startPosition;\n        }\n    }\n\n    private void ensurePagerLayoutManager() {\n        if (mPagerLayoutManager == null) {\n            mPagerLayoutManager = new PagerLayoutManager(mContext, this,\n                    mScaleMode, mStartPosition, 1.0f, mPagerInterval);\n        }\n    }\n\n    private void ensureScrollLayoutManager() {\n        if (mScrollLayoutManager == null) {\n            mScrollLayoutManager = new ScrollLayoutManager(mContext, this, mScrollInterval);\n        }\n    }\n\n    private void attachLayoutManager() {\n        if (null != mLayoutManager) {\n            return;\n        }\n\n        switch (mLayoutMode) {\n            case LAYOUT_LEFT_TO_RIGHT -> {\n                ensurePagerLayoutManager();\n                mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT);\n                mPagerLayoutManager.onAttach(mAdapter);\n                mPagerLayoutManager.setCurrentIndex(mIndex);\n                mAdapter = null;\n                mLayoutManager = mPagerLayoutManager;\n            }\n            case LAYOUT_RIGHT_TO_LEFT -> {\n                ensurePagerLayoutManager();\n                mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT);\n                mPagerLayoutManager.onAttach(mAdapter);\n                mPagerLayoutManager.setCurrentIndex(mIndex);\n                mAdapter = null;\n                mLayoutManager = mPagerLayoutManager;\n            }\n            case LAYOUT_TOP_TO_BOTTOM -> {\n                ensureScrollLayoutManager();\n                mScrollLayoutManager.onAttach(mAdapter);\n                mScrollLayoutManager.setCurrentIndex(mIndex);\n                mAdapter = null;\n                mLayoutManager = mScrollLayoutManager;\n            }\n        }\n\n        requestFill();\n    }\n\n    private void detachLayoutManager() {\n        if (null == mLayoutManager) {\n            return;\n        }\n\n        mIndex = mLayoutManager.getInternalCurrentIndex();\n        mAdapter = mLayoutManager.onDetach();\n        mLayoutManager = null;\n    }\n\n    private void onAttachToRootInternal() {\n        if (null == mPageTextTexture) {\n            mPageTextTexture = ImageMovableTextTexture.create(mPageTextTypeface,\n                    mPageTextSize, mPageTextColor,\n                    new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'});\n        }\n        attachLayoutManager();\n    }\n\n    private void setPagerIntervalInternal(int interval) {\n        mPagerInterval = interval;\n        if (mPagerLayoutManager != null) {\n            mPagerLayoutManager.setInterval(interval);\n        }\n    }\n\n    private void setScrollIntervalInternal(int interval) {\n        mScrollInterval = interval;\n        if (mScrollLayoutManager != null) {\n            mScrollLayoutManager.setInterval(interval);\n        }\n    }\n\n    @Override\n    public void onAttachToRoot(GLRoot root) {\n        super.onAttachToRoot(root);\n        postMethod(METHOD_ON_ATTACH_TO_ROOT);\n    }\n\n    @Override\n    public void onDetachFromRoot() {\n        // When detached, render() will not be called. So do it here\n        detachLayoutManager();\n        if (null != mPageTextTexture) {\n            mPageTextTexture.recycle();\n            mPageTextTexture = null;\n        }\n\n        super.onDetachFromRoot();\n    }\n\n    public int getLayoutMode() {\n        return mLayoutMode;\n    }\n\n    public void setLayoutMode(@LayoutMode int layoutMode) {\n        postMethod(METHOD_SET_LAYOUT_MODE, layoutMode);\n    }\n\n    @Override\n    public void requestLayout() {\n        // Do not need requestLayout, because the size will not change\n        requestFill();\n    }\n\n    public void requestFill() {\n        if (mEnableRequestFill) {\n            mRequestFill = true;\n            if (!mWillFill) {\n                invalidate();\n            }\n        }\n    }\n\n    @Override\n    protected boolean dispatchTouchEvent(MotionEvent event) {\n        // Do not pass event to component, so handle event here\n        mGestureRecognizer.onTouchEvent(event);\n        return true;\n    }\n\n    String getEmptyStr() {\n        return mEmptyString;\n    }\n\n    boolean isFirstScroll() {\n        boolean firstScroll = mFirstScroll;\n        mFirstScroll = false;\n        return firstScroll;\n    }\n\n    // Make sure method run in render thread to ensure thread safe\n    private void postMethod(int method, Object... args) {\n        synchronized (this) {\n            mMethodList.add(method);\n            mArgsList.add(args);\n        }\n\n        invalidate();\n    }\n\n    public void setCurrentPage(int page) {\n        postMethod(METHOD_SET_CURRENT_PAGE, page);\n    }\n\n    public void pageLeft() {\n        postMethod(METHOD_PAGE_LEFT);\n    }\n\n    public void pageRight() {\n        postMethod(METHOD_PAGE_RIGHT);\n    }\n\n    public void setScaleMode(int scaleMode) {\n        postMethod(METHOD_SET_SCALE_MODE, scaleMode);\n    }\n\n    public void setStartPosition(int startPosition) {\n        postMethod(METHOD_SET_START_POSITION, startPosition);\n    }\n\n    public void setPagerInterval(int interval) {\n        postMethod(METHOD_SET_PAGER_INTERVAL, interval);\n    }\n\n    public void setScrollInterval(int interval) {\n        postMethod(METHOD_SET_SCROLL_INTERVAL, interval);\n    }\n\n    @Override\n    public boolean onSingleTapUp(float x, float y) {\n        postMethod(METHOD_ON_SINGLE_TAP_UP, x, y);\n        return true;\n    }\n\n    @Override\n    public boolean onSingleTapConfirmed(float x, float y) {\n        postMethod(METHOD_ON_SINGLE_TAP_CONFIRMED, x, y);\n        return true;\n    }\n\n    @Override\n    public boolean onDoubleTap(float x, float y) {\n        postMethod(METHOD_ON_DOUBLE_TAP, x, y);\n        return true;\n    }\n\n    @Override\n    public boolean onDoubleTapConfirmed(float x, float y) {\n        postMethod(METHOD_ON_DOUBLE_TAP_CONFIRMED, x, y);\n        return true;\n    }\n\n    @Override\n    public void onLongPress(float x, float y) {\n        if (mLayoutManager != null && mLayoutManager.isTapOrPressDisable()) {\n            return;\n        }\n\n        postMethod(METHOD_ON_LONG_PRESS, x, y);\n    }\n\n    @Override\n    public boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y) {\n        postMethod(METHOD_ON_SCROLL, dx, dy, totalX, totalY, x, y);\n        return true;\n    }\n\n    @Override\n    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {\n        postMethod(METHOD_ON_FLING, velocityX, velocityY);\n        return true;\n    }\n\n    @Override\n    public boolean onScaleBegin(float focusX, float focusY) {\n        postMethod(METHOD_ON_SCALE_BEGIN, focusX, focusY);\n        return true;\n    }\n\n    @Override\n    public boolean onScale(float focusX, float focusY, float scale) {\n        postMethod(METHOD_ON_SCALE, focusX, focusY, scale);\n        return true;\n    }\n\n    @Override\n    public void onScaleEnd() {\n        postMethod(METHOD_ON_SCALE_END);\n    }\n\n    @Override\n    public void onDown(float x, float y) {\n        postMethod(METHOD_ON_DOWN, x, y);\n    }\n\n    @Override\n    public void onUp() {\n        postMethod(METHOD_ON_UP);\n    }\n\n    @Override\n    public void onPointerDown(float x, float y) {\n        postMethod(METHOD_ON_POINTER_DOWN, x, y);\n    }\n\n    @Override\n    public void onPointerUp() {\n        postMethod(METHOD_ON_POINTER_UP);\n    }\n\n    @Override\n    protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) {\n        fill();\n        if (changeSize) {\n            int width = right - left;\n            int height = bottom - top;\n            mLeftArea.set((int) (LEFT_AREA[0] * width), (int) (LEFT_AREA[1] * height),\n                    (int) (LEFT_AREA[2] * width), (int) (LEFT_AREA[3] * height));\n            mRightArea.set((int) (RIGHT_AREA[0] * width), (int) (RIGHT_AREA[1] * height),\n                    (int) (RIGHT_AREA[2] * width), (int) (RIGHT_AREA[3] * height));\n            mMenuArea.set((int) (MENU_AREA[0] * width), (int) (MENU_AREA[1] * height),\n                    (int) (MENU_AREA[2] * width), (int) (MENU_AREA[3] * height));\n            mSliderArea.set((int) (SLIDER_AREA[0] * width), (int) (SLIDER_AREA[1] * height),\n                    (int) (SLIDER_AREA[2] * width), (int) (SLIDER_AREA[3] * height));\n        }\n    }\n\n    public void onDataChanged() {\n        GalleryUtils.assertInRenderThread();\n\n        if (mLayoutManager != null) {\n            mLayoutManager.onDataChanged();\n        }\n    }\n\n    private void onSingleTapUpInternal() {\n    }\n\n    private GalleryPageView findPageUnder(float x, float y) {\n        for (int i = 0, n = getComponentCount(); i < n; i++) {\n            GLView view = getComponent(i);\n            if (view instanceof GalleryPageView && view.bounds().contains((int) x, (int) y)) {\n                return (GalleryPageView) view;\n            }\n        }\n        return null;\n    }\n\n    private void onSingleTapConfirmedInternal(float x, float y) {\n        if (mLayoutManager == null || mLayoutManager.isTapOrPressDisable()) {\n            return;\n        }\n\n        GalleryPageView page = findPageUnder(x, y);\n        if (page != null &&\n                page.getIndex() != GalleryPageView.INVALID_INDEX &&\n                page.isError() &&\n                page.isUnderInfo(x - page.bounds().left, y - page.bounds().top)) {\n            if (mListener != null) {\n                mListener.onTapErrorText(page.getIndex());\n            }\n        } else if (mSliderArea.contains((int) x, (int) y)) {\n            if (mListener != null) {\n                mListener.onTapSliderArea();\n            }\n        } else if (mMenuArea.contains((int) x, (int) y)) {\n            if (mListener != null) {\n                mListener.onTapMenuArea();\n            }\n        } else if (mLeftArea.contains((int) x, (int) y)) {\n            mLayoutManager.onPageLeft();\n        } else if (mRightArea.contains((int) x, (int) y)) {\n            mLayoutManager.onPageRight();\n        }\n    }\n\n    private void onDoubleTapInternal() {\n    }\n\n    private void onDoubleTapConfirmedInternal(float x, float y) {\n        if (mScale) {\n            return;\n        }\n\n        if (mLayoutManager != null) {\n            mLayoutManager.onDoubleTapConfirmed(x, y);\n        }\n    }\n\n    private void onLongPressInternal(float x, float y) {\n        if (mScale) {\n            return;\n        }\n\n        if (mLayoutManager == null) {\n            return;\n        }\n\n        int index = mLayoutManager.getIndexUnder(x, y);\n        if (index == GalleryPageView.INVALID_INDEX) {\n            return;\n        }\n\n        if (mListener != null) {\n            mListener.onLongPressPage(index);\n        }\n    }\n\n    private void onScrollInternal(float dx, float dy, float totalX, float totalY, float x, float y) {\n        if (mScale) {\n            return;\n        }\n        mScroll = true;\n\n        if (mLayoutManager != null) {\n            mLayoutManager.onScroll(dx, dy, totalX, totalY, x, y);\n        }\n    }\n\n    private void onFlingInternal(float velocityX, float velocityY) {\n        if (mLayoutManager != null) {\n            mLayoutManager.onFling(velocityX, velocityY);\n        }\n    }\n\n    private void onScaleBeginInternal(float focusX, float focusY) {\n        onScaleInternal(focusX, focusY, 1.0f);\n    }\n\n    private void onScaleInternal(float focusX, float focusY, float scale) {\n        if (mScroll || (mLayoutManager != null && !mLayoutManager.canScale())) {\n            return;\n        }\n        mScale = true;\n\n        if (mLayoutManager != null) {\n            mLayoutManager.onScale(focusX, focusY, scale);\n        }\n    }\n\n    private void onScaleEndInternal() {\n    }\n\n    private void onDownInternal() {\n        mScale = false;\n        mScroll = false;\n        mFirstScroll = true;\n        if (mLayoutManager != null) {\n            mLayoutManager.onDown();\n        }\n    }\n\n    private void onUpInternal() {\n        if (mLayoutManager != null) {\n            mLayoutManager.onUp();\n        }\n    }\n\n    private void onPointerDownInternal() {\n        if (!mScroll && (mLayoutManager != null && mLayoutManager.canScale())) {\n            mScale = true;\n        }\n    }\n\n    private void onPointerUpInternal() {\n    }\n\n    private void setLayoutModeInternal(int layoutMode) {\n        if (mLayoutMode == layoutMode) {\n            return;\n        }\n        mLayoutMode = layoutMode;\n\n        if (mLayoutManager == null) {\n            return;\n        }\n\n        switch (mLayoutMode) {\n            case LAYOUT_LEFT_TO_RIGHT -> {\n                if (mLayoutManager == mPagerLayoutManager) {\n                    // mPagerLayoutManager already attached, just change mode\n                    mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT);\n                } else {\n                    ensurePagerLayoutManager();\n                    mPagerLayoutManager.setMode(PagerLayoutManager.MODE_LEFT_TO_RIGHT);\n                    int index = mLayoutManager.getInternalCurrentIndex();\n                    mPagerLayoutManager.onAttach(mLayoutManager.onDetach());\n                    mPagerLayoutManager.setCurrentIndex(index);\n                    mLayoutManager = mPagerLayoutManager;\n                }\n            }\n            case LAYOUT_RIGHT_TO_LEFT -> {\n                if (mLayoutManager == mPagerLayoutManager) {\n                    // mPagerLayoutManager already attached, just change mode\n                    mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT);\n                } else {\n                    ensurePagerLayoutManager();\n                    mPagerLayoutManager.setMode(PagerLayoutManager.MODE_RIGHT_TO_LEFT);\n                    int index = mLayoutManager.getInternalCurrentIndex();\n                    mPagerLayoutManager.onAttach(mLayoutManager.onDetach());\n                    mPagerLayoutManager.setCurrentIndex(index);\n                    mLayoutManager = mPagerLayoutManager;\n                }\n            }\n            case LAYOUT_TOP_TO_BOTTOM -> {\n                ensureScrollLayoutManager();\n                int index = mLayoutManager.getInternalCurrentIndex();\n                mScrollLayoutManager.onAttach(mLayoutManager.onDetach());\n                mScrollLayoutManager.setCurrentIndex(index);\n                mLayoutManager = mScrollLayoutManager;\n            }\n        }\n\n        requestFill();\n    }\n\n    private void setCurrentPageInternal(int page) {\n        if (mLayoutManager != null) {\n            mLayoutManager.setCurrentIndex(page);\n        } else {\n            mIndex = page;\n        }\n    }\n\n    private void pageLeftInternal() {\n        if (mLayoutManager != null) {\n            mLayoutManager.onPageLeft();\n        }\n    }\n\n    private void pageRightInternal() {\n        if (mLayoutManager != null) {\n            mLayoutManager.onPageRight();\n        }\n    }\n\n    private void setScaleModeInternal(int scaleMode) {\n        mScaleMode = scaleMode;\n        if (mPagerLayoutManager != null) {\n            mPagerLayoutManager.setScaleMode(scaleMode);\n        }\n    }\n\n    private void setStartPositionInternal(int startPosition) {\n        mStartPosition = startPosition;\n        if (mPagerLayoutManager != null) {\n            mPagerLayoutManager.setStartPosition(startPosition);\n        }\n    }\n\n    void forceFill() {\n        mRequestFill = true;\n        fill();\n    }\n\n    private void fill() {\n        GalleryUtils.assertInRenderThread();\n\n        if (!mRequestFill) {\n            return;\n        }\n\n        // Disable request layout\n        mEnableRequestFill = false;\n        if (mLayoutManager != null) {\n            mLayoutManager.onFill();\n        }\n        mEnableRequestFill = true;\n        mRequestFill = false;\n    }\n\n    private void dispatchMethod() {\n        List<Integer> methodListTemp = mMethodListTemp;\n        List<Object[]> argsListTemp = mArgsListTemp;\n\n        synchronized (this) {\n            if (mMethodList.isEmpty()) {\n                return;\n            }\n\n            methodListTemp.addAll(mMethodList);\n            argsListTemp.addAll(mArgsList);\n            mMethodList.clear();\n            mArgsList.clear();\n        }\n\n        for (int i = 0, n = methodListTemp.size(); i < n; i++) {\n            int method = methodListTemp.get(i);\n            Object[] args = argsListTemp.get(i);\n\n            switch (method) {\n                case METHOD_ON_SINGLE_TAP_UP -> onSingleTapUpInternal();\n                case METHOD_ON_SINGLE_TAP_CONFIRMED ->\n                        onSingleTapConfirmedInternal((Float) args[0], (Float) args[1]);\n                case METHOD_ON_DOUBLE_TAP -> onDoubleTapInternal();\n                case METHOD_ON_DOUBLE_TAP_CONFIRMED ->\n                        onDoubleTapConfirmedInternal((Float) args[0], (Float) args[1]);\n                case METHOD_ON_LONG_PRESS -> onLongPressInternal((Float) args[0], (Float) args[1]);\n                case METHOD_ON_SCROLL ->\n                        onScrollInternal((Float) args[0], (Float) args[1], (Float) args[2],\n                                (Float) args[3], (Float) args[4], (Float) args[5]);\n                case METHOD_ON_FLING -> onFlingInternal((Float) args[0], (Float) args[1]);\n                case METHOD_ON_SCALE_BEGIN ->\n                        onScaleBeginInternal((Float) args[0], (Float) args[1]);\n                case METHOD_ON_SCALE ->\n                        onScaleInternal((Float) args[0], (Float) args[1], (Float) args[2]);\n                case METHOD_ON_SCALE_END -> onScaleEndInternal();\n                case METHOD_ON_DOWN -> onDownInternal();\n                case METHOD_ON_UP -> onUpInternal();\n                case METHOD_ON_POINTER_DOWN -> onPointerDownInternal();\n                case METHOD_ON_POINTER_UP -> onPointerUpInternal();\n                case METHOD_SET_LAYOUT_MODE -> setLayoutModeInternal((Integer) args[0]);\n                case METHOD_SET_CURRENT_PAGE -> setCurrentPageInternal((Integer) args[0]);\n                case METHOD_PAGE_LEFT -> pageLeftInternal();\n                case METHOD_PAGE_RIGHT -> pageRightInternal();\n                case METHOD_SET_SCALE_MODE -> setScaleModeInternal((Integer) args[0]);\n                case METHOD_SET_START_POSITION -> setStartPositionInternal((Integer) args[0]);\n                case METHOD_ON_ATTACH_TO_ROOT -> onAttachToRootInternal();\n                case METHOD_SET_PAGER_INTERVAL -> setPagerIntervalInternal((Integer) args[0]);\n                case METHOD_SET_SCROLL_INTERVAL -> setScrollIntervalInternal((Integer) args[0]);\n            }\n        }\n\n        methodListTemp.clear();\n        argsListTemp.clear();\n    }\n\n    @Override\n    public void render(GLCanvas canvas) {\n        mWillFill = true;\n        int oldCurrentIndex = mCurrentIndex.get();\n\n        // Dispatch method\n        dispatchMethod();\n\n        long time = AnimationTime.get();\n        if (mLayoutManager != null && mLayoutManager.onUpdateAnimation(time)) {\n            invalidate();\n        }\n\n        fill();\n        mWillFill = false;\n\n        super.render(canvas);\n\n        int newCurrentIndex;\n        if (mLayoutManager != null) {\n            newCurrentIndex = mLayoutManager.getCurrentIndex();\n        } else {\n            newCurrentIndex = GalleryPageView.INVALID_INDEX;\n        }\n        mCurrentIndex.lazySet(newCurrentIndex);\n\n        if (oldCurrentIndex != newCurrentIndex && mListener != null) {\n            mListener.onUpdateCurrentIndex(newCurrentIndex);\n        }\n    }\n\n    public GalleryPageView findPageByIndex(int id) {\n        if (mLayoutManager != null) {\n            return mLayoutManager.findPageByIndex(id);\n        } else {\n            return null;\n        }\n    }\n\n    GalleryPageView obtainPage() {\n        GalleryPageView page = mGalleryPageViewPool.pop();\n        if (page == null) {\n            page = new GalleryPageView(mPageTextTexture,\n                    mProgressColor, mBackgroundColor, mProgressSize,\n                    mPageMinHeight, mPageInfoInterval);\n        }\n        return page;\n    }\n\n    void releasePage(GalleryPageView page) {\n        mGalleryPageViewPool.push(page);\n    }\n\n    GLTextureView obtainErrorView() {\n        GLTextureView errorView;\n        if (mErrorViewCache != null) {\n            errorView = mErrorViewCache;\n            mErrorViewCache = null;\n        } else {\n            errorView = new GLTextureView();\n        }\n        return errorView;\n    }\n\n    void unbindErrorView(GLTextureView errorView) {\n        Texture texture = errorView.getTexture();\n        if (texture != null) {\n            errorView.setTexture(null);\n            if (texture instanceof BasicTexture) {\n                ((BasicTexture) texture).recycle();\n            }\n        }\n    }\n\n    void bindErrorView(GLTextureView errorView, String error) {\n        unbindErrorView(errorView);\n\n        Texture texture = StringTexture.newInstance(error, mErrorTextSize, mErrorTextColor);\n        errorView.setTexture(texture);\n    }\n\n    void releaseErrorView(GLTextureView errorView) {\n        unbindErrorView(errorView);\n        mErrorViewCache = errorView;\n    }\n\n    @IntDef({LAYOUT_LEFT_TO_RIGHT, LAYOUT_RIGHT_TO_LEFT, LAYOUT_TOP_TO_BOTTOM})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface LayoutMode {\n    }\n\n    @IntDef({SCALE_ORIGIN, SCALE_FIT_WIDTH, SCALE_FIT_HEIGHT, SCALE_FIT, SCALE_FIXED})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface ScaleMode {\n    }\n\n    @IntDef({START_POSITION_TOP_LEFT, START_POSITION_TOP_RIGHT, START_POSITION_BOTTOM_LEFT,\n            START_POSITION_BOTTOM_RIGHT, START_POSITION_CENTER})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface StartPosition {\n    }\n\n    public interface Listener {\n        void onUpdateCurrentIndex(int index);\n\n        void onTapSliderArea();\n\n        void onTapMenuArea();\n\n        void onTapErrorText(int index);\n\n        void onLongPressPage(int index);\n    }\n\n    public static class Builder {\n        private final Context mContext;\n        private final Adapter mAdapter;\n        private Listener mListener;\n        private int mLayoutMode = LAYOUT_LEFT_TO_RIGHT;\n        private int mScaleMode = SCALE_FIT;\n        private int mStartPosition = START_POSITION_TOP_LEFT;\n        private int mStartPage = 0;\n        private int mBackgroundColor = Color.BLACK;\n        private int mPagerInterval = 48;\n        private int mScrollInterval = 24;\n        private int mPageMinHeight = 256;\n        private int mPageInfoInterval = 24;\n        private int mProgressColor = Color.WHITE;\n        private int mProgressSize = 56;\n        private int mPageTextColor = Color.WHITE;\n        private int mPageTextSize = 56;\n        private Typeface mPageTextTypeface = Typeface.DEFAULT;\n        private int mErrorTextColor = Color.RED;\n        private int mErrorTextSize = 24;\n        private String mEmptyString = \"Empty\";\n\n        public Builder(@NonNull Context context, @NonNull Adapter adapter) {\n            mContext = context;\n            mAdapter = adapter;\n        }\n\n        public Builder setListener(Listener listener) {\n            mListener = listener;\n            return this;\n        }\n\n        public Builder setLayoutMode(@LayoutMode int layoutMode) {\n            mLayoutMode = layoutMode;\n            return this;\n        }\n\n        public Builder setScaleMode(@ScaleMode int scaleMode) {\n            mScaleMode = scaleMode;\n            return this;\n        }\n\n        public Builder setStartPosition(@StartPosition int startPosition) {\n            mStartPosition = startPosition;\n            return this;\n        }\n\n        public Builder setStartPage(int startPage) {\n            mStartPage = startPage;\n            return this;\n        }\n\n        public Builder setBackgroundColor(int backgroundColor) {\n            mBackgroundColor = backgroundColor;\n            return this;\n        }\n\n        public Builder setPagerInterval(int pagerInterval) {\n            mPagerInterval = pagerInterval;\n            return this;\n        }\n\n        public Builder setScrollInterval(int scrollInterval) {\n            mScrollInterval = scrollInterval;\n            return this;\n        }\n\n        public Builder setPageMinHeight(int pageMinHeight) {\n            mPageMinHeight = pageMinHeight;\n            return this;\n        }\n\n        public Builder setPageInfoInterval(int pageInfoInterval) {\n            mPageInfoInterval = pageInfoInterval;\n            return this;\n        }\n\n        public Builder setProgressColor(int progressColor) {\n            mProgressColor = progressColor;\n            return this;\n        }\n\n        public Builder setProgressSize(int progressSize) {\n            mProgressSize = progressSize;\n            return this;\n        }\n\n        public Builder setPageTextColor(int pageTextColor) {\n            mPageTextColor = pageTextColor;\n            return this;\n        }\n\n        public Builder setPageTextSize(int pageTextSize) {\n            mPageTextSize = pageTextSize;\n            return this;\n        }\n\n        public Builder setPageTextTypeface(Typeface pageTextTypeface) {\n            mPageTextTypeface = pageTextTypeface;\n            return this;\n        }\n\n        public Builder setErrorTextColor(int errorTextColor) {\n            mErrorTextColor = errorTextColor;\n            return this;\n        }\n\n        public Builder setErrorTextSize(int errorTextSize) {\n            mErrorTextSize = errorTextSize;\n            return this;\n        }\n\n        public Builder setEmptyString(String emptyString) {\n            mEmptyString = emptyString;\n            return this;\n        }\n\n        public GalleryView build() {\n            return new GalleryView(this);\n        }\n    }\n\n    public static abstract class Adapter {\n        protected GalleryView mGalleryView;\n\n        private void setGalleryView(@NonNull GalleryView galleryView) {\n            mGalleryView = galleryView;\n        }\n\n        public void bind(GalleryPageView view, int index) {\n            onBind(view, index);\n            view.setIndex(index);\n        }\n\n        public void unbind(GalleryPageView view) {\n            onUnbind(view, view.getIndex());\n            view.setIndex(GalleryPageView.INVALID_INDEX);\n        }\n\n        public abstract void onBind(GalleryPageView view, int index);\n\n        public abstract void onUnbind(GalleryPageView view, int index);\n\n        public abstract int size();\n    }\n\n    public static abstract class LayoutManager {\n        protected GalleryView mGalleryView;\n\n        public LayoutManager(@NonNull GalleryView galleryView) {\n            mGalleryView = galleryView;\n        }\n\n        public abstract void onAttach(Adapter iterator);\n\n        public abstract Adapter onDetach();\n\n        public abstract void onFill();\n\n        public abstract void onDown();\n\n        public abstract void onUp();\n\n        public abstract void onDoubleTapConfirmed(float x, float y);\n\n        public abstract void onScroll(float dx, float dy, float totalX, float totalY, float x, float y);\n\n        public abstract void onFling(float velocityX, float velocityY);\n\n        public abstract boolean canScale();\n\n        public abstract void onScale(float focusX, float focusY, float scale);\n\n        public abstract boolean onUpdateAnimation(long time);\n\n        public abstract void onDataChanged();\n\n        public abstract void onPageLeft();\n\n        public abstract void onPageRight();\n\n        public abstract boolean isTapOrPressDisable();\n\n        public abstract GalleryPageView findPageByIndex(int index);\n\n        /**\n         * @return {@link GalleryPageView#INVALID_INDEX} for error\n         */\n        public abstract int getCurrentIndex();\n\n        public abstract void setCurrentIndex(int index);\n\n        public abstract int getIndexUnder(float x, float y);\n\n        abstract int getInternalCurrentIndex();\n\n        protected void placeCenter(GLView view) {\n            int spec = GLView.MeasureSpec.makeMeasureSpec(GLView.LayoutParams.WRAP_CONTENT,\n                    GLView.LayoutParams.WRAP_CONTENT);\n            view.measure(spec, spec);\n            int viewWidth = view.getMeasuredWidth();\n            int viewHeight = view.getMeasuredHeight();\n            int viewLeft = mGalleryView.getWidth() / 2 - viewWidth / 2;\n            int viewTop = mGalleryView.getHeight() / 2 - viewHeight / 2;\n            view.layout(viewLeft, viewTop, viewLeft + viewWidth, viewTop + viewHeight);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/GestureRecognizer.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport android.content.Context;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.view.ScaleGestureDetector;\n\nimport androidx.annotation.NonNull;\n\n// This class aggregates three gesture detectors: GestureDetector,\n// ScaleGestureDetector, and DownUpDetector.\nclass GestureRecognizer {\n    @SuppressWarnings(\"unused\")\n    private static final String TAG = \"GestureRecognizer\";\n    private final GestureDetector mGestureDetector;\n    private final ScaleGestureDetector mScaleDetector;\n    private final DownUpDetector mDownUpDetector;\n    private final Listener mListener;\n\n    public GestureRecognizer(Context context, Listener listener) {\n        mListener = listener;\n        MyGestureListener gestureListener = new MyGestureListener();\n        mGestureDetector = new GestureDetector(context, gestureListener, null /* ignoreMultitouch */);\n        mGestureDetector.setOnDoubleTapListener(gestureListener);\n        mScaleDetector = new ScaleGestureDetector(context, new MyScaleListener());\n        mDownUpDetector = new DownUpDetector(new MyDownUpListener());\n    }\n\n    public void onTouchEvent(MotionEvent event) {\n        mScaleDetector.onTouchEvent(event);\n        mGestureDetector.onTouchEvent(event);\n        mDownUpDetector.onTouchEvent(event);\n    }\n\n    public boolean isDown() {\n        return mDownUpDetector.isDown();\n    }\n\n    public interface Listener {\n        boolean onSingleTapUp(float x, float y);\n\n        boolean onSingleTapConfirmed(float x, float y);\n\n        boolean onDoubleTap(float x, float y);\n\n        boolean onDoubleTapConfirmed(float x, float y);\n\n        void onLongPress(float x, float y);\n\n        boolean onScroll(float dx, float dy, float totalX, float totalY, float x, float y);\n\n        /**\n         * @param velocityX Finger from top to bottom is positive\n         * @param velocityY Finger from left to right is positive\n         */\n        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);\n\n        boolean onScaleBegin(float focusX, float focusY);\n\n        boolean onScale(float focusX, float focusY, float scale);\n\n        void onScaleEnd();\n\n        void onDown(float x, float y);\n\n        void onUp();\n\n        void onPointerDown(float x, float y);\n\n        void onPointerUp();\n    }\n\n    private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {\n        @Override\n        public boolean onSingleTapUp(MotionEvent e) {\n            return mListener.onSingleTapUp(e.getX(), e.getY());\n        }\n\n        @Override\n        public boolean onSingleTapConfirmed(MotionEvent e) {\n            return mListener.onSingleTapConfirmed(e.getX(), e.getY());\n        }\n\n        @Override\n        public boolean onDoubleTapEvent(MotionEvent e) {\n            if (e.getAction() == MotionEvent.ACTION_UP) {\n                return mListener.onDoubleTapConfirmed(e.getX(), e.getY());\n            } else {\n                return true;\n            }\n        }\n\n        @Override\n        public boolean onDoubleTap(MotionEvent e) {\n            return mListener.onDoubleTap(e.getX(), e.getY());\n        }\n\n        @Override\n        public void onLongPress(MotionEvent e) { mListener.onLongPress(e.getX(), e.getY()); }\n\n        @Override\n        public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, float dx, float dy) {\n            if (e1 == null) return false;\n            return mListener.onScroll(dx, dy, e2.getX() - e1.getX(), e2.getY() - e1.getY(), e2.getX(), e2.getY());\n        }\n\n        @Override\n        public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {\n            return mListener.onFling(e1, e2, velocityX, velocityY);\n        }\n    }\n\n    private class MyScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {\n        @Override\n        public boolean onScaleBegin(ScaleGestureDetector detector) {\n            return mListener.onScaleBegin(detector.getFocusX(), detector.getFocusY());\n        }\n\n        @Override\n        public boolean onScale(ScaleGestureDetector detector) {\n            return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());\n        }\n\n        @Override\n        public void onScaleEnd(@NonNull ScaleGestureDetector detector) {\n            mListener.onScaleEnd();\n        }\n    }\n\n    private class MyDownUpListener implements DownUpDetector.DownUpListener {\n        @Override\n        public void onDown(MotionEvent e) {\n            mListener.onDown(e.getX(), e.getY());\n        }\n\n        @Override\n        public void onUp(MotionEvent e) {\n            mListener.onUp();\n        }\n\n        @Override\n        public void onPointerDown(MotionEvent e) {\n            mListener.onPointerDown(e.getX(), e.getY());\n        }\n\n        @Override\n        public void onPointerUp(MotionEvent e) {\n            mListener.onPointerUp();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/ImageView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport android.graphics.Rect;\nimport android.graphics.RectF;\n\nimport com.hippo.glview.anim.AlphaAnimation;\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.glrenderer.Texture;\nimport com.hippo.glview.image.ImageTexture;\nimport com.hippo.glview.view.GLView;\nimport com.hippo.yorozuya.AnimationUtils;\nimport com.hippo.yorozuya.MathUtils;\n\nimport java.util.Arrays;\n\nclass ImageView extends GLView implements ImageTexture.Callback {\n    public static final int SCALE_ORIGIN = 0;\n    public static final int SCALE_FIT_WIDTH = 1;\n    public static final int SCALE_FIT_HEIGHT = 2;\n    public static final int SCALE_FIT = 3;\n    public static final int SCALE_FIXED = 4;\n    public static final int START_POSITION_TOP_LEFT = 0;\n    public static final int START_POSITION_TOP_RIGHT = 1;\n    public static final int START_POSITION_BOTTOM_LEFT = 2;\n    public static final int START_POSITION_BOTTOM_RIGHT = 3;\n    public static final int START_POSITION_CENTER = 4;\n    // TODO adjust scale max and min according to image size and screen size\n    private static final float SCALE_MIN = 1 / 10.0f;\n    private static final float SCALE_MAX = 10.0f;\n    private static final long ALPHA_ANIMATION_DURING = 300L;\n    private final RectF mDst = new RectF();\n    private final RectF mSrcActual = new RectF();\n    private final RectF mDstActual = new RectF();\n    private final Rect mValidRect = new Rect();\n    private final AlphaAnimation mAlphaAnimation;\n    private ImageTexture mImageTexture;\n    private int mTextureWidth;\n    private int mTextureHeight;\n    private int mScaleMode = SCALE_FIT;\n    private int mStartPosition = START_POSITION_TOP_RIGHT;\n    private float mScaleValue = 1.0f;\n    private float mScale = 1.0f;\n    private boolean mScaleOffsetDirty = true;\n    private boolean mPositionInRootDirty = true;\n\n    public ImageView() {\n        mAlphaAnimation = new AlphaAnimation(0.0f, 1.0f);\n        mAlphaAnimation.setDuration(ALPHA_ANIMATION_DURING);\n        mAlphaAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);\n    }\n\n    @Override\n    protected int getSuggestedMinimumWidth() {\n        return Math.max(super.getSuggestedMinimumWidth(),\n                mImageTexture == null ? 0 : mTextureWidth);\n    }\n\n    @Override\n    protected int getSuggestedMinimumHeight() {\n        return Math.max(super.getSuggestedMinimumHeight(),\n                mImageTexture == null ? 0 : mTextureHeight);\n    }\n\n    @Override\n    protected void onMeasure(int widthSpec, int heightSpec) {\n        if (mImageTexture == null) {\n            super.onMeasure(widthSpec, heightSpec);\n        } else {\n            float ratio = (float) mTextureWidth / mTextureHeight;\n            int widthSize = MeasureSpec.getSize(widthSpec);\n            int heightSize = MeasureSpec.getSize(heightSpec);\n            int widthMode = MeasureSpec.getMode(widthSpec);\n            int heightMode = MeasureSpec.getMode(heightSpec);\n            int measureWidth = -1;\n            int measureHeight = -1;\n\n            if (widthMode == MeasureSpec.EXACTLY) {\n                measureWidth = widthSize;\n                if (heightMode == MeasureSpec.EXACTLY) {\n                    measureHeight = heightSize;\n                } else {\n                    measureHeight = (int) (widthSize / ratio);\n                    if (heightMode == MeasureSpec.AT_MOST) {\n                        measureHeight = Math.min(measureHeight, heightSize);\n                    }\n                }\n            } else if (heightMode == MeasureSpec.EXACTLY) {\n                measureHeight = heightSize;\n                measureWidth = (int) (heightSize * ratio);\n                if (widthMode == MeasureSpec.AT_MOST) {\n                    measureWidth = Math.min(measureWidth, widthSize);\n                }\n            }\n\n            if (measureWidth == -1 || measureHeight == -1) {\n                super.onMeasure(widthSpec, heightSpec);\n            } else {\n                setMeasuredSize(measureWidth, measureHeight);\n            }\n        }\n    }\n\n    @Override\n    protected void onSizeChanged(int newW, int newH, int oldW, int oldH) {\n        mScaleOffsetDirty = true;\n        mPositionInRootDirty = true;\n    }\n\n    @Override\n    protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) {\n        mPositionInRootDirty = true;\n\n        if (mImageTexture != null) {\n            getValidRect(mValidRect);\n            if (!mValidRect.isEmpty()) {\n                mImageTexture.start();\n            } else {\n                mImageTexture.stop();\n            }\n        }\n    }\n\n    public void getScaleDefault(float[] scaleDefault) {\n        if (mImageTexture == null) {\n            return;\n        }\n\n        scaleDefault[0] = 1.0f;\n        scaleDefault[1] = (float) getWidth() / mTextureWidth;\n        scaleDefault[2] = (float) getHeight() / mTextureHeight;\n        scaleDefault[3] = Math.max(scaleDefault[1], scaleDefault[2]) * 2;\n\n        scaleDefault[0] = MathUtils.clamp(scaleDefault[0], SCALE_MIN, SCALE_MAX);\n        scaleDefault[1] = MathUtils.clamp(scaleDefault[1], SCALE_MIN, SCALE_MAX);\n        scaleDefault[2] = MathUtils.clamp(scaleDefault[2], SCALE_MIN, SCALE_MAX);\n        scaleDefault[3] = MathUtils.clamp(scaleDefault[3], SCALE_MIN, SCALE_MAX);\n\n        Arrays.sort(scaleDefault);\n    }\n\n    public ImageTexture getImageTexture() {\n        return mImageTexture;\n    }\n\n    public void setImageTexture(ImageTexture imageTexture) {\n        // Remove callback\n        if (mImageTexture != null) {\n            mImageTexture.setCallback(null);\n            mImageTexture.stop();\n        }\n\n        int oldTextureWidth = mTextureWidth;\n        int oldTextureHeight = mTextureHeight;\n\n        mImageTexture = imageTexture;\n\n        if (imageTexture != null) {\n            imageTexture.setCallback(this);\n            mTextureWidth = imageTexture.getWidth();\n            mTextureHeight = imageTexture.getHeight();\n            // Avoid zero and negative\n            if (mTextureWidth <= 0) {\n                mTextureWidth = 1;\n            }\n            if (mTextureHeight <= 0) {\n                mTextureHeight = 1;\n            }\n\n            // Start alpha animation, do not show animation for image has no valid rect\n            getValidRect(mValidRect);\n            if (!mValidRect.isEmpty()) {\n                startAnimation(mAlphaAnimation, true);\n                mImageTexture.start();\n            }\n        } else {\n            mTextureWidth = 1;\n            mTextureHeight = 1;\n        }\n\n        mScaleOffsetDirty = true;\n        mPositionInRootDirty = true;\n\n        if (oldTextureWidth != mTextureWidth || oldTextureHeight != mTextureHeight) {\n            requestLayout();\n        }\n    }\n\n    public boolean isLoaded() {\n        return mImageTexture != null;\n    }\n\n    public boolean canFling() {\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n            if (mScaleOffsetDirty) {\n                return false;\n            }\n        }\n\n        return mDst.left < 0.0f || mDst.top < 0.0f || mDst.right > getWidth() || mDst.bottom > getHeight();\n    }\n\n    public int getMaxDx() {\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n            if (mScaleOffsetDirty) {\n                return 0;\n            }\n        }\n        return Math.max(0, -(int) mDst.left);\n    }\n\n    public int getMinDx() {\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n            if (mScaleOffsetDirty) {\n                return 0;\n            }\n        }\n        return Math.min(0, getWidth() - (int) mDst.right);\n    }\n\n    public int getMaxDy() {\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n            if (mScaleOffsetDirty) {\n                return 0;\n            }\n        }\n        return Math.max(0, -(int) mDst.top);\n    }\n\n    public int getMinDy() {\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n            if (mScaleOffsetDirty) {\n                return 0;\n            }\n        }\n        return Math.min(0, getHeight() - (int) mDst.bottom);\n    }\n\n    public float getScale() {\n        return mScale;\n    }\n\n    /**\n     * If target is shorter then screen, make it in screen center. If target is\n     * longer then parent, make sure target fill parent over\n     */\n    private void adjustPosition() {\n        RectF dst = mDst;\n        int screenWidth = getWidth();\n        int screenHeight = getHeight();\n        float targetWidth = dst.width();\n        float targetHeight = dst.height();\n\n        if (targetWidth > screenWidth) {\n            float fixXOffset = dst.left;\n            if (fixXOffset > 0) {\n                dst.left -= fixXOffset;\n                dst.right -= fixXOffset;\n            } else if ((fixXOffset = screenWidth - dst.right) > 0) {\n                dst.left += fixXOffset;\n                dst.right += fixXOffset;\n            }\n        } else {\n            float left = (screenWidth - targetWidth) / 2;\n            dst.offsetTo(left, dst.top);\n        }\n        if (targetHeight > screenHeight) {\n            float fixYOffset = dst.top;\n            if (fixYOffset > 0) {\n                dst.top -= fixYOffset;\n                dst.bottom -= fixYOffset;\n            } else if ((fixYOffset = screenHeight - dst.bottom) > 0) {\n                dst.top += fixYOffset;\n                dst.bottom += fixYOffset;\n            }\n        } else {\n            float top = (screenHeight - targetHeight) / 2;\n            dst.offsetTo(dst.left, top);\n        }\n    }\n\n    public void setScaleOffset(int scaleMode, int startPosition, float scaleValue) {\n        mScaleMode = scaleMode;\n        mStartPosition = startPosition;\n        mScaleValue = scaleValue;\n\n        int screenWidth = getWidth();\n        int screenHeight = getHeight();\n\n        if (mImageTexture == null || screenWidth == 0 || screenHeight == 0) {\n            mScaleOffsetDirty = true;\n            return;\n        }\n\n        int textureWidth = mTextureWidth;\n        int textureHeight = mTextureHeight;\n\n        // Set scale\n        float targetWidth;\n        float targetHeight;\n        switch (scaleMode) {\n            case SCALE_ORIGIN -> {\n                mScale = 1.0f;\n                targetWidth = textureWidth;\n                targetHeight = textureHeight;\n            }\n            case SCALE_FIT_WIDTH -> {\n                mScale = (float) screenWidth / textureWidth;\n                targetWidth = screenWidth;\n                targetHeight = textureHeight * mScale;\n            }\n            case SCALE_FIT_HEIGHT -> {\n                mScale = (float) screenHeight / textureHeight;\n                targetWidth = textureWidth * mScale;\n                targetHeight = screenHeight;\n            }\n            case SCALE_FIT -> {\n                float scaleX = (float) screenWidth / textureWidth;\n                float scaleY = (float) screenHeight / textureHeight;\n                if (scaleX < scaleY) {\n                    mScale = scaleX;\n                    targetWidth = screenWidth;\n                    targetHeight = textureHeight * scaleX;\n                } else {\n                    mScale = scaleY;\n                    targetWidth = textureWidth * scaleY;\n                    targetHeight = screenHeight;\n                }\n            }\n            default -> {\n                mScale = scaleValue;\n                targetWidth = textureWidth * scaleValue;\n                targetHeight = textureHeight * scaleValue;\n            }\n        }\n\n        // adjust scale, not too big, not too small\n        if (mScale < SCALE_MIN) {\n            mScale = SCALE_MIN;\n            targetWidth = textureWidth * SCALE_MIN;\n            targetHeight = textureHeight * SCALE_MIN;\n        } else if (mScale > SCALE_MAX) {\n            mScale = SCALE_MAX;\n            targetWidth = textureWidth * SCALE_MAX;\n            targetHeight = textureHeight * SCALE_MAX;\n        }\n\n        // Set mDst.left and mDst.right\n        RectF dst = mDst;\n        switch (startPosition) {\n            case START_POSITION_TOP_LEFT -> {\n                dst.left = 0;\n                dst.top = 0;\n            }\n            case START_POSITION_TOP_RIGHT -> {\n                dst.left = screenWidth - targetWidth;\n                dst.top = 0;\n            }\n            case START_POSITION_BOTTOM_LEFT -> {\n                dst.left = 0;\n                dst.top = screenHeight - targetHeight;\n            }\n            case START_POSITION_BOTTOM_RIGHT -> {\n                dst.left = screenWidth - targetWidth;\n                dst.top = screenHeight - targetHeight;\n            }\n            default -> {\n                dst.left = (screenWidth - targetWidth) / 2;\n                dst.top = (screenHeight - targetHeight) / 2;\n            }\n        }\n\n        // Set mDst.right and mDst.bottom\n        dst.right = dst.left + targetWidth;\n        dst.bottom = dst.top + targetHeight;\n\n        // adjust position\n        adjustPosition();\n\n        mScaleOffsetDirty = false;\n        mPositionInRootDirty = true;\n    }\n\n    public void scroll(int dx, int dy, int[] remain) {\n        // Only work after layout\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n        if (mScaleOffsetDirty) {\n            remain[0] = dx;\n            remain[1] = dy;\n            return;\n        }\n\n        RectF dst = mDst;\n        int screenWidth = getWidth();\n        int screenHeight = getHeight();\n        float targetWidth = dst.width();\n        float targetHeight = dst.height();\n\n        if (targetWidth > screenWidth) {\n            dst.left -= dx;\n            dst.right -= dx;\n\n            float fixXOffset = dst.left;\n            if (fixXOffset > 0) {\n                dst.left -= fixXOffset;\n                dst.right -= fixXOffset;\n                remain[0] = -(int) fixXOffset;\n            } else if ((fixXOffset = screenWidth - dst.right) > 0) {\n                dst.left += fixXOffset;\n                dst.right += fixXOffset;\n                remain[0] = (int) fixXOffset;\n            } else {\n                remain[0] = 0;\n            }\n        } else {\n            remain[0] = dx;\n        }\n        if (targetHeight > screenHeight) {\n            dst.top -= dy;\n            dst.bottom -= dy;\n\n            float fixYOffset = dst.top;\n            if (fixYOffset > 0) {\n                dst.top -= fixYOffset;\n                dst.bottom -= fixYOffset;\n                remain[1] = -(int) fixYOffset;\n            } else if ((fixYOffset = screenHeight - dst.bottom) > 0) {\n                dst.top += fixYOffset;\n                dst.bottom += fixYOffset;\n                remain[1] = (int) fixYOffset;\n            } else {\n                remain[1] = 0;\n            }\n        } else {\n            remain[1] = dy;\n        }\n\n        if (dx != remain[0] || dy != remain[1]) {\n            mPositionInRootDirty = true;\n            invalidate();\n        }\n    }\n\n    public void scale(float focusX, float focusY, float scale) {\n        // Only work after layout\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n        if (mScaleOffsetDirty) {\n            return;\n        }\n\n        if ((mScale == SCALE_MAX && scale >= 1.0f) || (mScale == SCALE_MIN && scale < 1.0f)) {\n            return;\n        }\n\n        float newScale = mScale * scale;\n        newScale = MathUtils.clamp(newScale, SCALE_MIN, SCALE_MAX);\n        mScale = newScale;\n        RectF dst = mDst;\n        float left = (focusX - ((focusX - dst.left) * scale));\n        float top = (focusY - ((focusY - dst.top) * scale));\n        dst.set(left, top,\n                (left + (mImageTexture.getWidth() * newScale)),\n                (top + (mImageTexture.getHeight() * newScale)));\n\n        // adjust position\n        adjustPosition();\n\n        mPositionInRootDirty = true;\n        invalidate();\n    }\n\n    private void applyPositionInRoot() {\n        int width = mImageTexture.getWidth();\n        int height = mImageTexture.getHeight();\n        RectF dst = mDst;\n        RectF dstActual = mDstActual;\n        RectF srcActual = mSrcActual;\n\n        dstActual.set(dst);\n        getValidRect(mValidRect);\n        if (dstActual.intersect(mValidRect.left, mValidRect.top, mValidRect.right, mValidRect.bottom)) {\n            srcActual.left = MathUtils.lerp(0, width,\n                    MathUtils.delerp(dst.left, dst.right, dstActual.left));\n            srcActual.right = MathUtils.lerp(0, width,\n                    MathUtils.delerp(dst.left, dst.right, dstActual.right));\n            srcActual.top = MathUtils.lerp(0, height,\n                    MathUtils.delerp(dst.top, dst.bottom, dstActual.top));\n            srcActual.bottom = MathUtils.lerp(0, height,\n                    MathUtils.delerp(dst.top, dst.bottom, dstActual.bottom));\n        } else {\n            // Can't be seen, set src and dst empty\n            srcActual.setEmpty();\n            dstActual.setEmpty();\n        }\n\n        mPositionInRootDirty = false;\n    }\n\n    @Override\n    public void onRender(GLCanvas canvas) {\n        Texture texture = mImageTexture;\n        if (texture == null) {\n            return;\n        }\n\n        if (mScaleOffsetDirty) {\n            setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n\n        if (mPositionInRootDirty) {\n            applyPositionInRoot();\n        }\n\n        if (!mSrcActual.isEmpty()) {\n            texture.draw(canvas, mSrcActual, mDstActual);\n        }\n    }\n\n    @Override\n    public void invalidateImageTexture(ImageTexture who) {\n        invalidate();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/PagerLayoutManager.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport android.content.Context;\nimport android.graphics.Rect;\nimport android.util.Log;\nimport android.view.animation.Interpolator;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\n\nimport com.hippo.glview.anim.Animation;\nimport com.hippo.glview.view.GLView;\nimport com.hippo.glview.widget.GLProgressView;\nimport com.hippo.glview.widget.GLTextureView;\nimport com.hippo.yorozuya.AnimationUtils;\nimport com.hippo.yorozuya.AssertUtils;\nimport com.hippo.yorozuya.MathUtils;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\nclass PagerLayoutManager extends GalleryView.LayoutManager {\n    public static final int MODE_LEFT_TO_RIGHT = 0;\n    public static final int MODE_RIGHT_TO_LEFT = 1;\n    private static final String TAG = PagerLayoutManager.class.getSimpleName();\n    private static final Interpolator SMOOTH_SCROLLER_INTERPOLATOR = t -> {\n        t -= 1.0f;\n        return t * t * t * t * t + 1.0f;\n    };\n    private final int[] mScrollRemain = new int[2];\n    private final float[] mScaleDefault = new float[4];\n    private final SmoothScroller mSmoothScroller;\n    private final PageFling mPageFling;\n    private final SmoothScaler mSmoothScaler;\n    private final Rect mTempRect = new Rect();\n    private GalleryView.Adapter mAdapter;\n    private GLProgressView mProgress;\n    private String mErrorStr;\n    private GLTextureView mErrorView;\n    private GalleryPageView mPrevious;\n    private GalleryPageView mCurrent;\n    private GalleryPageView mNext;\n    @Mode\n    private int mMode = MODE_RIGHT_TO_LEFT;\n    private int mScaleMode;\n    private int mStartPosition;\n    private float mScaleValue;\n    private int mOffset;\n    private boolean mCanScrollBetweenPages = false;\n    private boolean mStopAnimationFinger;\n    private int mInterval;\n    // Current index\n    private int mIndex;\n\n    public PagerLayoutManager(Context context, @NonNull GalleryView galleryView,\n                              int scaleMode, int startPoint, float scaleValue, int interval) {\n        super(galleryView);\n\n        mScaleMode = scaleMode;\n        mStartPosition = startPoint;\n        mScaleValue = scaleValue;\n\n        mInterval = interval;\n        mSmoothScroller = new SmoothScroller();\n        mPageFling = new PageFling(context);\n        mSmoothScaler = new SmoothScaler();\n    }\n\n    public void setInterval(int interval) {\n        if (mInterval == interval) {\n            return;\n        }\n\n        if (mAdapter != null) {\n            int index = getInternalCurrentIndex();\n            GalleryView.Adapter adapter = onDetach();\n            mInterval = interval;\n            onAttach(adapter);\n            setCurrentIndex(index);\n            mGalleryView.requestFill();\n        } else {\n            mInterval = interval;\n        }\n    }\n\n    private void resetParameters() {\n        mOffset = 0;\n        mCanScrollBetweenPages = false;\n        mStopAnimationFinger = false;\n    }\n\n    private boolean cancelAllAnimations() {\n        boolean running = mSmoothScroller.isRunning() ||\n                mPageFling.isRunning() ||\n                mSmoothScaler.isRunning();\n        mSmoothScroller.cancel();\n        mPageFling.cancel();\n        mSmoothScaler.cancel();\n        return running;\n    }\n\n    public void setMode(@Mode int mode) {\n        if (mMode == mode) {\n            return;\n        }\n\n        mMode = mode;\n        if (mAdapter != null) {\n            // It is attached, refill\n            // Cancel all animations\n            cancelAllAnimations();\n            // Remove all view\n            removeProgress();\n            removeErrorView();\n            removeAllPages();\n            // Reset parameters\n            resetParameters();\n            // Request fill\n            mGalleryView.requestFill();\n        }\n    }\n\n    public void setScaleMode(int scaleMode) {\n        if (mScaleMode == scaleMode) {\n            return;\n        }\n        mScaleMode = scaleMode;\n\n        if (mCurrent != null) {\n            mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n        if (mPrevious != null) {\n            mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n        if (mNext != null) {\n            mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n    }\n\n    public void setStartPosition(int startPosition) {\n        if (mStartPosition == startPosition) {\n            return;\n        }\n        mStartPosition = startPosition;\n\n        if (mCurrent != null) {\n            mCurrent.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n        if (mPrevious != null) {\n            mPrevious.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n        if (mNext != null) {\n            mNext.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n    }\n\n    @Override\n    public void onAttach(GalleryView.Adapter adapter) {\n        AssertUtils.assertNull(\"The PagerLayoutManager is attached\", mAdapter);\n        AssertUtils.assertNotNull(\"The adapter is null\", adapter);\n        mAdapter = adapter;\n        // Reset parameters\n        resetParameters();\n    }\n\n    private void removeProgress() {\n        if (mProgress != null) {\n            mGalleryView.removeComponent(mProgress);\n            mProgress = null;\n        }\n    }\n\n    private void removeErrorView() {\n        if (mErrorView != null) {\n            mGalleryView.removeComponent(mErrorView);\n            mGalleryView.releaseErrorView(mErrorView);\n            mErrorView = null;\n            mErrorStr = null;\n        }\n    }\n\n    private void removePage(@NonNull GalleryPageView page) {\n        mGalleryView.removeComponent(page);\n        mAdapter.unbind(page);\n        mGalleryView.releasePage(page);\n    }\n\n    private void removeAllPages() {\n        // Remove gallery view\n        if (mPrevious != null) {\n            removePage(mPrevious);\n            mPrevious = null;\n        }\n        if (mCurrent != null) {\n            removePage(mCurrent);\n            mCurrent = null;\n        }\n        if (mNext != null) {\n            removePage(mNext);\n            mNext = null;\n        }\n    }\n\n    @Override\n    public GalleryView.Adapter onDetach() {\n        AssertUtils.assertNotNull(\"The PagerLayoutManager is not attached\", mAdapter);\n\n        // Cancel all animations\n        cancelAllAnimations();\n\n        // Remove all view\n        removeProgress();\n        removeErrorView();\n        removeAllPages();\n\n        // Clear iterator\n        GalleryView.Adapter adapter = mAdapter;\n        mAdapter = null;\n\n        return adapter;\n    }\n\n    private GalleryPageView getLeftPage() {\n        return mMode == MODE_LEFT_TO_RIGHT ? mPrevious : mNext;\n    }\n\n    private GalleryPageView getRightPage() {\n        return mMode == MODE_LEFT_TO_RIGHT ? mNext : mPrevious;\n    }\n\n    private GalleryPageView obtainPage() {\n        GalleryPageView page = mGalleryView.obtainPage();\n        page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        return page;\n    }\n\n    private void layoutPage(GalleryPageView page, int widthSpec, int heightSpec,\n                            int left, int right, int bottom) {\n        Rect rect = mTempRect;\n        page.getValidRect(rect);\n        boolean oldValid = !rect.isEmpty();\n        page.measure(widthSpec, heightSpec);\n        page.layout(left, 0, right, bottom);\n        page.getValidRect(rect);\n        boolean newValid = !rect.isEmpty();\n        if (!oldValid && newValid) {\n            page.getImageView().setScaleOffset(mScaleMode, mStartPosition, mScaleValue);\n        }\n    }\n\n    @Override\n    public void onFill() {\n        GalleryView.Adapter adapter = mAdapter;\n        GalleryView galleryView = mGalleryView;\n        AssertUtils.assertNotNull(\"The PagerLayoutManager is not attached\", adapter);\n\n        int width = galleryView.getWidth();\n        int height = galleryView.getHeight();\n        int size = adapter.size();\n\n        if (size == 0) { // Get empty, show error text\n            String errorStr = galleryView.getEmptyStr();\n\n            // Remove progress and all pages\n            removeProgress();\n            removeAllPages();\n\n            // Ensure error view\n            if (mErrorView == null) {\n                mErrorView = galleryView.obtainErrorView();\n                galleryView.addComponent(mErrorView);\n            }\n\n            // Update error string\n            if (!errorStr.equals(mErrorStr)) {\n                mErrorStr = errorStr;\n                galleryView.bindErrorView(mErrorView, errorStr);\n            }\n\n            // Place error view center\n            placeCenter(mErrorView);\n        } else {\n            // Remove progress and error view\n            removeProgress();\n            removeErrorView();\n\n            // Ensure index in range\n            int index = mIndex;\n            if (index < 0) {\n                index = 0;\n                mIndex = index;\n                removeAllPages();\n                Log.e(TAG, \"index < 0, index = \" + index);\n            } else if (index >= size) {\n                index = size - 1;\n                mIndex = index;\n                removeAllPages();\n                Log.e(TAG, \"index >= size, index = \" + index + \", size = \" + size);\n            }\n\n            // Ensure pages\n            if (mCurrent == null) {\n                mCurrent = obtainPage();\n                galleryView.addComponent(mCurrent);\n                adapter.bind(mCurrent, index);\n            }\n            if (mPrevious == null && index > 0) {\n                mPrevious = obtainPage();\n                galleryView.addComponent(mPrevious);\n                adapter.bind(mPrevious, index - 1);\n            } else if (mPrevious != null && index == 0) {\n                removePage(mPrevious);\n            }\n            if (mNext == null && index < size - 1) {\n                mNext = obtainPage();\n                galleryView.addComponent(mNext);\n                adapter.bind(mNext, index + 1);\n            } else if (mNext != null && index == size - 1) {\n                removePage(mNext);\n            }\n\n            GalleryPageView leftPage = getLeftPage();\n            GalleryPageView rightPage = getRightPage();\n\n            // Fix offset\n            final int min = rightPage == null ? 0 : -width - mInterval + 1;\n            final int max = leftPage == null ? 0 : width + mInterval - 1;\n            mOffset = MathUtils.clamp(mOffset, min, max);\n\n            // Measure and layout pages\n            final int offset = mOffset;\n            final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(width, GLView.MeasureSpec.EXACTLY);\n            final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.EXACTLY);\n            if (mCurrent != null) {\n                layoutPage(mCurrent, widthSpec, heightSpec,\n                        offset, width + offset, height);\n            }\n            if (leftPage != null) {\n                layoutPage(leftPage, widthSpec, heightSpec,\n                        -mInterval - width + offset, -mInterval + offset, height);\n            }\n            if (rightPage != null) {\n                layoutPage(rightPage, widthSpec, heightSpec,\n                        width + mInterval + offset, width + mInterval + width + offset, height);\n            }\n        }\n    }\n\n    @Override\n    public void onDown() {\n        mStopAnimationFinger = cancelAllAnimations();\n    }\n\n    @Override\n    public void onUp() {\n        if (mCurrent == null) {\n            return;\n        }\n\n        // Scroll\n        if (mOffset != 0) {\n            int width = mGalleryView.getWidth();\n            int dx;\n            if (mOffset >= mInterval && getLeftPage() != null) {\n                dx = mOffset - width - mInterval;\n            } else if (mOffset <= -mInterval && getRightPage() != null) {\n                dx = mOffset + width + mInterval;\n            } else {\n                dx = mOffset;\n            }\n            final float pageDelta = 7 * (float) Math.abs(mOffset) / (width + mInterval);\n            int duration = (int) ((pageDelta + 1) * 100);\n            mSmoothScroller.startSmoothScroll(dx, duration);\n        }\n    }\n\n    @Override\n    public void onDoubleTapConfirmed(float x, float y) {\n        if (mCurrent == null || !mCurrent.getImageView().isLoaded()) {\n            return;\n        }\n\n        float[] scales = mScaleDefault;\n        ImageView image = mCurrent.getImageView();\n        image.getScaleDefault(scales);\n        float scale = image.getScale();\n        float endScale = scales[0];\n        for (float value : scales) {\n            if (scale < value - 0.01f) {\n                endScale = value;\n                break;\n            }\n        }\n\n        mSmoothScaler.startSmoothScaler(x, y, scale, endScale, 300);\n    }\n\n    private void pagePrevious() {\n        if (mIndex <= 0) {\n            return;\n        }\n        mIndex--;\n\n        if (mNext != null) {\n            removePage(mNext);\n        }\n        mNext = mCurrent;\n        mCurrent = mPrevious;\n        mPrevious = null;\n\n        if (mIndex > 0) {\n            mPrevious = obtainPage();\n            mGalleryView.addComponent(mPrevious);\n            mAdapter.bind(mPrevious, mIndex - 1);\n        }\n    }\n\n    private void pageNext() {\n        GalleryView.Adapter adapter = mAdapter;\n        int size = adapter.size();\n        if (mIndex >= size - 1) {\n            return;\n        }\n        mIndex++;\n\n        if (mPrevious != null) {\n            removePage(mPrevious);\n        }\n        mPrevious = mCurrent;\n        mCurrent = mNext;\n        mNext = null;\n\n        if (mIndex < size - 1) {\n            mNext = obtainPage();\n            mGalleryView.addComponent(mNext);\n            adapter.bind(mNext, mIndex + 1);\n        }\n    }\n\n    private void pageLeft() {\n        if (mMode == MODE_LEFT_TO_RIGHT) {\n            pagePrevious();\n        } else {\n            pageNext();\n        }\n    }\n\n    private void pageRight() {\n        if (mMode == MODE_LEFT_TO_RIGHT) {\n            pageNext();\n        } else {\n            pagePrevious();\n        }\n    }\n\n    private int scrollBetweenPages(int dx) {\n        GalleryPageView leftPage = getLeftPage();\n        GalleryPageView rightPage = getRightPage();\n        int width = mGalleryView.getWidth();\n\n        int remain;\n        if (dx < 0) { // Try to show left\n            int limit;\n            if (leftPage == null) {\n                limit = 0;\n            } else {\n                limit = width + mInterval;\n            }\n\n            if (dx > mOffset - limit) {\n                remain = 0;\n                mOffset -= dx;\n            } else {\n                // Go to left page if left page not null\n                if (leftPage != null) {\n                    pageLeft();\n                }\n                remain = dx + limit - mOffset;\n                mOffset = 0;\n            }\n        } else { // Try to show right\n            int limit;\n            if (rightPage == null) {\n                limit = 0;\n            } else {\n                limit = -width - mInterval;\n            }\n\n            if (dx < mOffset - limit) {\n                remain = 0;\n                mOffset -= dx;\n            } else {\n                // Go to right page if right page not null\n                if (rightPage != null) {\n                    pageRight();\n                }\n                remain = dx + limit - mOffset;\n                mOffset = 0;\n            }\n        }\n\n        return remain;\n    }\n\n    public void scrollInternal(float dx, float dy) {\n        if (mCurrent == null) {\n            return;\n        }\n\n        boolean needFill = false;\n        boolean canImageScroll = true;\n        int remainX = (int) dx;\n        int remainY = (int) dy;\n\n        if (mGalleryView.isFirstScroll()) {\n            mCanScrollBetweenPages = Math.abs(dx) > Math.abs(dy) * 1.5;\n        }\n\n        while (remainX != 0 || remainY != 0) {\n            if (mOffset == 0 && canImageScroll) {\n                ImageView image = mCurrent.getImageView();\n                image.scroll(remainX, remainY, mScrollRemain);\n                remainX = mScrollRemain[0];\n                remainY = mScrollRemain[1];\n                canImageScroll = false;\n            } else if (remainX == 0 ||\n                    (getLeftPage() == null && mOffset == 0 && remainX < 0) ||\n                    (getRightPage() == null && mOffset == 0 && remainX > 0)) {\n                // On edge\n                remainX = 0;\n                remainY = 0;\n            } else if (mCanScrollBetweenPages) {\n                remainX = scrollBetweenPages(remainX);\n                canImageScroll = true;\n                needFill = true;\n            } else {\n                remainX = 0;\n                remainY = 0;\n            }\n        }\n\n        if (needFill) {\n            mGalleryView.requestFill();\n        }\n    }\n\n    @Override\n    public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) {\n        scrollInternal(dx, dy);\n    }\n\n    @Override\n    public void onFling(float velocityX, float velocityY) {\n        if (mCurrent == null || mOffset != 0 || !mCurrent.getImageView().isLoaded() ||\n                !mCurrent.getImageView().canFling()) {\n            return;\n        }\n\n        ImageView image = mCurrent.getImageView();\n        mPageFling.startFling((int) velocityX, image.getMinDx(), image.getMaxDx(),\n                (int) velocityY, image.getMinDy(), image.getMaxDy());\n    }\n\n    @Override\n    public boolean canScale() {\n        return mCurrent != null && mOffset == 0 && mCurrent.getImageView().isLoaded();\n    }\n\n    @Override\n    public void onScale(float focusX, float focusY, float scale) {\n        if (mCurrent == null || !mCurrent.getImageView().isLoaded()) {\n            return;\n        }\n\n        mCurrent.getImageView().scale(focusX, focusY, scale);\n        mScaleValue = mCurrent.getImageView().getScale();\n    }\n\n    @Override\n    public boolean onUpdateAnimation(long time) {\n        boolean invalidate = mSmoothScroller.calculate(time);\n        invalidate |= mPageFling.calculate(time);\n        invalidate |= mSmoothScaler.calculate(time);\n        return invalidate;\n    }\n\n    @Override\n    public void onDataChanged() {\n        AssertUtils.assertNotNull(\"The PagerLayoutManager is not attached\", mAdapter);\n\n        // Cancel all animations\n        cancelAllAnimations();\n        // Remove all views\n        removeProgress();\n        removeErrorView();\n        removeAllPages();\n        // Reset parameters\n        resetParameters();\n        mGalleryView.requestFill();\n    }\n\n    @Override\n    public void onPageLeft() {\n        int size = mAdapter.size();\n        if (size <= 0 || mCurrent == null) {\n            return;\n        }\n\n        if (mMode == MODE_LEFT_TO_RIGHT) {\n            if (mIndex != 0) {\n                setCurrentIndex(mIndex - 1);\n            }\n        } else {\n            if (mIndex < size - 1) {\n                setCurrentIndex(mIndex + 1);\n            }\n        }\n    }\n\n    @Override\n    public void onPageRight() {\n        int size = mAdapter.size();\n        if (size <= 0 || mCurrent == null) {\n            return;\n        }\n\n        if (mMode == MODE_LEFT_TO_RIGHT) {\n            if (mIndex < size - 1) {\n                setCurrentIndex(mIndex + 1);\n            }\n        } else {\n            if (mIndex != 0) {\n                setCurrentIndex(mIndex - 1);\n            }\n        }\n    }\n\n    @Override\n    public boolean isTapOrPressDisable() {\n        return mStopAnimationFinger;\n    }\n\n    @Override\n    public GalleryPageView findPageByIndex(int index) {\n        if (mCurrent != null && mCurrent.getIndex() == index) {\n            return mCurrent;\n        }\n        if (mPrevious != null && mPrevious.getIndex() == index) {\n            return mPrevious;\n        }\n        if (mNext != null && mNext.getIndex() == index) {\n            return mNext;\n        }\n        return null;\n    }\n\n    @Override\n    public int getCurrentIndex() {\n        if (mCurrent != null) {\n            return mCurrent.getIndex();\n        } else {\n            return GalleryPageView.INVALID_INDEX;\n        }\n    }\n\n    @Override\n    public void setCurrentIndex(int index) {\n        int size = mAdapter.size();\n        if (size <= 0) {\n            // Can't get size now, assume size is MAX\n            size = Integer.MAX_VALUE;\n        }\n        if (index == mIndex || index < 0 || index >= size) {\n            return;\n        }\n        if (mCurrent == null) {\n            mIndex = index;\n        } else if (index == mIndex - 1) {\n            // Cancel all animations\n            cancelAllAnimations();\n            // Reset parameters\n            resetParameters();\n            // Go to previous\n            pagePrevious();\n            // Request fill\n            mGalleryView.requestFill();\n        } else if (index == mIndex + 1) {\n            // Cancel all animations\n            cancelAllAnimations();\n            // Reset parameters\n            resetParameters();\n            // Go to next\n            pageNext();\n            // Request fill\n            mGalleryView.requestFill();\n        } else {\n            mIndex = index;\n            // It is attached, refill\n            // Cancel all animations\n            cancelAllAnimations();\n            // Remove all view\n            removeProgress();\n            removeErrorView();\n            removeAllPages();\n            // Reset parameters\n            resetParameters();\n            // Request fill\n            mGalleryView.requestFill();\n        }\n    }\n\n    @Override\n    public int getIndexUnder(float x, float y) {\n        if (mCurrent == null) {\n            return GalleryPageView.INVALID_INDEX;\n        } else {\n            int intX = (int) x;\n            int intY = (int) y;\n            if (mCurrent.bounds().contains(intX, intY)) {\n                return mCurrent.getIndex();\n            } else if (mPrevious != null && mPrevious.bounds().contains(intX, intY)) {\n                return mPrevious.getIndex();\n            } else if (mNext != null && mNext.bounds().contains(intX, intY)) {\n                return mNext.getIndex();\n            } else {\n                return GalleryPageView.INVALID_INDEX;\n            }\n        }\n    }\n\n    @Override\n    int getInternalCurrentIndex() {\n        int currentIndex = getCurrentIndex();\n        if (currentIndex == GalleryPageView.INVALID_INDEX) {\n            currentIndex = mIndex;\n        }\n        return currentIndex;\n    }\n\n    @IntDef({MODE_LEFT_TO_RIGHT, MODE_RIGHT_TO_LEFT})\n    @Retention(RetentionPolicy.SOURCE)\n    private @interface Mode {\n    }\n\n    private class SmoothScroller extends Animation {\n        private int mDx;\n        private int mLastX;\n\n        public SmoothScroller() {\n            setInterpolator(SMOOTH_SCROLLER_INTERPOLATOR);\n        }\n\n        public void startSmoothScroll(int dx, int duration) {\n            mDx = dx;\n            mLastX = 0;\n            setDuration(duration);\n            start();\n            mGalleryView.invalidate();\n        }\n\n        @Override\n        protected void onCalculate(float progress) {\n            int x = (int) (mDx * progress);\n            int offsetX = x - mLastX;\n            while (offsetX != 0) {\n                int oldOffsetX = offsetX;\n                offsetX = scrollBetweenPages(offsetX);\n                // Avoid loop infinitely\n                if (offsetX == oldOffsetX) {\n                    break;\n                } else {\n                    mGalleryView.requestFill();\n                }\n            }\n            mLastX = x;\n        }\n    }\n\n    private class PageFling extends Fling {\n        private final int[] mTemp = new int[2];\n        private int mDx;\n        private int mDy;\n        private int mLastX;\n        private int mLastY;\n\n        public PageFling(Context context) {\n            super(context);\n        }\n\n        public void startFling(int velocityX, int minX, int maxX,\n                               int velocityY, int minY, int maxY) {\n            mDx = (int) (getSplineFlingDistance(velocityX) * Math.signum(velocityX));\n            mDy = (int) (getSplineFlingDistance(velocityY) * Math.signum(velocityY));\n            mLastX = 0;\n            mLastY = 0;\n            int durationX = getSplineFlingDuration(velocityX);\n            int durationY = getSplineFlingDuration(velocityY);\n\n            if (mDx < minX) {\n                durationX = adjustDuration(mDx, minX, durationX);\n                mDx = minX;\n            }\n            if (mDx > maxX) {\n                durationX = adjustDuration(mDx, maxX, durationX);\n                mDx = maxX;\n            }\n            if (mDy < minY) {\n                durationY = adjustDuration(mDy, minY, durationY);\n                mDy = minY;\n            }\n            if (mDy > maxY) {\n                durationY = adjustDuration(mDy, maxY, durationY);\n                mDy = maxY;\n            }\n\n            if (mDx == 0 && mDy == 0) {\n                return;\n            }\n\n            setDuration(Math.max(durationX, durationY));\n            start();\n            mGalleryView.invalidate();\n        }\n\n        @Override\n        protected void onCalculate(float progress) {\n            int x = (int) (mDx * progress);\n            int y = (int) (mDy * progress);\n            int offsetX = x - mLastX;\n            int offsetY = y - mLastY;\n            if (mCurrent != null && (offsetX != 0 || offsetY != 0)) {\n                mCurrent.getImageView().scroll(-offsetX, -offsetY, mTemp);\n            }\n            mLastX = x;\n            mLastY = y;\n        }\n    }\n\n    private class SmoothScaler extends Animation {\n        private float mFocusX;\n        private float mFocusY;\n        private float mStartScale;\n        private float mEndScale;\n        private float mLastScale;\n\n        public SmoothScaler() {\n            setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);\n        }\n\n        public void startSmoothScaler(float focusX, float focusY,\n                                      float startScale, float endScale, int duration) {\n            mFocusX = focusX;\n            mFocusY = focusY;\n            mStartScale = startScale;\n            mEndScale = endScale;\n            mLastScale = startScale;\n            setDuration(duration);\n            start();\n            mGalleryView.invalidate();\n        }\n\n        @Override\n        protected void onCalculate(float progress) {\n            if (mCurrent == null) {\n                return;\n            }\n\n            float scale = MathUtils.lerp(mStartScale, mEndScale, progress);\n            mCurrent.getImageView().scale(mFocusX, mFocusY, scale / mLastScale);\n            mLastScale = scale;\n            mScaleValue = scale;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/ScrollLayoutManager.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport android.content.Context;\nimport android.graphics.Rect;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.glview.anim.Animation;\nimport com.hippo.glview.view.GLView;\nimport com.hippo.glview.widget.GLProgressView;\nimport com.hippo.glview.widget.GLTextureView;\nimport com.hippo.yorozuya.AnimationUtils;\nimport com.hippo.yorozuya.AssertUtils;\nimport com.hippo.yorozuya.MathUtils;\n\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.List;\n\nclass ScrollLayoutManager extends GalleryView.LayoutManager {\n    private static final String TAG = ScrollLayoutManager.class.getSimpleName();\n\n    private static final float RESERVATION = 0.5f;\n    private static final float DEFAULT_SCALE = 1.0f;\n    private static final float MAX_SCALE = 2.0f;\n    private static final float MIN_SCALE = 0.3f;\n    private static final float SCALE_ERROR = 0.01f;\n\n    private static final int INVALID_TOP = Integer.MAX_VALUE;\n    private final LinkedList<GalleryPageView> mPages = new LinkedList<>();\n    private final LinkedList<GalleryPageView> mTempPages = new LinkedList<>();\n    private final PageFling mPageFling;\n    private final SmoothScaler mSmoothScaler;\n    private GalleryView.Adapter mAdapter;\n    private GLProgressView mProgress;\n    private String mErrorStr;\n    private GLTextureView mErrorView;\n    private float mScale = DEFAULT_SCALE;\n    private int mOffsetX;\n    private int mOffsetY;\n    private int mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;\n    private int mKeepTop = INVALID_TOP;\n    private int mFirstShownPageIndex = GalleryPageView.INVALID_INDEX;\n    private boolean mScrollUp;\n    private boolean mFlingUp;\n    private boolean mStopAnimationFinger;\n    private int mInterval;\n    // Current index\n    private int mIndex;\n\n    private int mBottomStateBottom;\n    private boolean mBottomStateHasNext;\n\n    public ScrollLayoutManager(Context context, @NonNull GalleryView galleryView, int interval) {\n        super(galleryView);\n\n        mInterval = interval;\n        mPageFling = new PageFling(context);\n        mSmoothScaler = new SmoothScaler();\n    }\n\n    public void setInterval(int interval) {\n        if (mInterval == interval) {\n            return;\n        }\n\n        if (mAdapter != null) {\n            int index = getInternalCurrentIndex();\n            GalleryView.Adapter adapter = onDetach();\n            mInterval = interval;\n            onAttach(adapter);\n            setCurrentIndex(index);\n            mGalleryView.requestFill();\n        } else {\n            mInterval = interval;\n        }\n    }\n\n    private void resetParameters() {\n        mScale = DEFAULT_SCALE;\n        mOffsetX = 0;\n        mOffsetY = 0;\n        mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;\n        mKeepTop = INVALID_TOP;\n        mFirstShownPageIndex = GalleryPageView.INVALID_INDEX;\n        mScrollUp = false;\n        mFlingUp = false;\n        mStopAnimationFinger = false;\n    }\n\n    // Return true for animations are running\n    private boolean cancelAllAnimations() {\n        boolean running = mPageFling.isRunning() ||\n                mSmoothScaler.isRunning();\n        mPageFling.cancel();\n        mSmoothScaler.cancel();\n        return running;\n    }\n\n    @Override\n    public void onAttach(GalleryView.Adapter adapter) {\n        AssertUtils.assertNull(\"The ScrollLayoutManager is attached\", mAdapter);\n        AssertUtils.assertNotNull(\"The iterator is null\", adapter);\n        mAdapter = adapter;\n        // Reset parameters\n        resetParameters();\n    }\n\n    private void removeProgress() {\n        if (mProgress != null) {\n            mGalleryView.removeComponent(mProgress);\n            mProgress = null;\n        }\n    }\n\n    private void removeErrorView() {\n        if (mErrorView != null) {\n            mGalleryView.removeComponent(mErrorView);\n            mGalleryView.releaseErrorView(mErrorView);\n            mErrorView = null;\n            mErrorStr = null;\n        }\n    }\n\n    private void removePage(@NonNull GalleryPageView page) {\n        mGalleryView.removeComponent(page);\n        mAdapter.unbind(page);\n        mGalleryView.releasePage(page);\n    }\n\n    private void removeAllPages() {\n        for (GalleryPageView page : mPages) {\n            removePage(page);\n        }\n        mPages.clear();\n    }\n\n    @Override\n    public GalleryView.Adapter onDetach() {\n        AssertUtils.assertNotNull(\"The PagerLayoutManager is not attached\", mAdapter);\n\n        // Cancel all animations\n        cancelAllAnimations();\n\n        // Remove all view\n        removeProgress();\n        removeErrorView();\n        removeAllPages();\n\n        // Clear iterator\n        GalleryView.Adapter iterator = mAdapter;\n        mAdapter = null;\n\n        return iterator;\n    }\n\n    private GalleryPageView obtainPage() {\n        GalleryPageView page = mGalleryView.obtainPage();\n        page.getImageView().setScaleOffset(ImageView.SCALE_FIT, ImageView.START_POSITION_TOP_RIGHT, 1.0f);\n        return page;\n    }\n\n    private GalleryPageView getPageForIndex(List<GalleryPageView> pages, int index, boolean remove) {\n        for (Iterator<GalleryPageView> iterator = pages.iterator(); iterator.hasNext(); ) {\n            GalleryPageView page = iterator.next();\n            if (index == page.getIndex()) {\n                if (remove) {\n                    iterator.remove();\n                }\n                return page;\n            }\n        }\n        return null;\n    }\n\n    private boolean isInScreen(GalleryPageView page, boolean includeFirst) {\n        int height = mGalleryView.getHeight();\n        Rect bound = page.bounds();\n        int pageTop = bound.top;\n        int pageBottom = bound.bottom;\n        return (includeFirst && pageTop >= 0 && pageTop < height) ||\n                (pageBottom > 0 && pageBottom <= height) ||\n                (pageTop < 0 && pageBottom > height);\n    }\n\n    private float getReservation() {\n        return Math.max(RESERVATION, (((1 + 2 * RESERVATION) * mScale) - 1) / 2);\n    }\n\n    private void fillPages(int startIndex, int startOffset) {\n        final GalleryView.Adapter adapter = mAdapter;\n        final GalleryView galleryView = mGalleryView;\n        final LinkedList<GalleryPageView> pages = mPages;\n        final LinkedList<GalleryPageView> tempPages = mTempPages;\n        final int width = galleryView.getWidth();\n        final int height = galleryView.getHeight();\n        final int pageWidth = (int) (width * mScale);\n        final int size = adapter.size();\n        final int interval = mInterval;\n        final float reservation = getReservation();\n        final int minY = (int) (-height * reservation);\n        final int maxY = (int) (height * (1 + reservation));\n        final int widthSpec = GLView.MeasureSpec.makeMeasureSpec(pageWidth, GLView.MeasureSpec.EXACTLY);\n        final int heightSpec = GLView.MeasureSpec.makeMeasureSpec(height, GLView.MeasureSpec.UNSPECIFIED);\n\n        // Fix start index and start offset\n        if (startIndex < 0) {\n            startIndex = 0;\n            startOffset = 0;\n        } else if (startIndex >= size) {\n            startIndex = size - 1;\n            startOffset = 0;\n        } else if (startOffset < minY) {\n            while (true) {\n                GalleryPageView page = getPageForIndex(pages, startIndex, false);\n                if (null == page) {\n                    startOffset = minY;\n                    break;\n                } else {\n                    page.measure(widthSpec, heightSpec);\n                    if (startOffset + page.getHeight() > minY) {\n                        break;\n                    } else if (size - 1 == startIndex) {\n                        startOffset = 0;\n                        break;\n                    } else {\n                        ++startIndex;\n                        startOffset += page.getHeight() + interval;\n                        if (startOffset >= minY) {\n                            break;\n                        }\n                    }\n                }\n            }\n        } else if (startOffset >= maxY) {\n            if (0 == startIndex) {\n                startOffset = 0;\n            } else {\n                --startIndex;\n                int startBottomOffset = startOffset - interval;\n                while (true) {\n                    GalleryPageView page = getPageForIndex(pages, startIndex, false);\n                    if (null == page) {\n                        startOffset = maxY - 1;\n                        break;\n                    } else {\n                        page.measure(widthSpec, heightSpec);\n                        startOffset = startBottomOffset - page.getHeight();\n                        if (startOffset < maxY) {\n                            break;\n                        } else if (0 == startIndex) {\n                            startOffset = 0;\n                            break;\n                        } else {\n                            --startIndex;\n                            startBottomOffset = startOffset - interval;\n                        }\n                    }\n                }\n            }\n        }\n\n        // Put page to temp list\n        tempPages.addAll(pages);\n        pages.clear();\n\n        // Sanitize offsetX\n        int margin = pageWidth - width;\n        if (margin >= 0) {\n            mOffsetX = MathUtils.clamp(mOffsetX, -margin, 0);\n        } else {\n            mOffsetX = -margin / 2;\n        }\n\n        // Layout start page\n        GalleryPageView page = getPageForIndex(tempPages, startIndex, true);\n        if (null == page) {\n            page = obtainPage();\n            galleryView.addComponent(page);\n            adapter.bind(page, startIndex);\n        }\n        pages.add(page);\n        page.measure(widthSpec, heightSpec);\n        page.layout(mOffsetX, startOffset, mOffsetX + pageWidth, startOffset + page.getMeasuredHeight());\n\n        // Prepare for check up and down\n        int bottomOffset = startOffset - interval;\n        int topOffset = startOffset + page.getMeasuredHeight() + interval;\n\n        // Check up\n        int index = startIndex - 1;\n        while (bottomOffset > minY && index >= 0) {\n            page = getPageForIndex(tempPages, index, true);\n            if (null == page) {\n                page = obtainPage();\n                galleryView.addComponent(page);\n                adapter.bind(page, index);\n            }\n            pages.addFirst(page);\n            page.measure(widthSpec, heightSpec);\n            page.layout(mOffsetX, bottomOffset - page.getMeasuredHeight(), mOffsetX + pageWidth, bottomOffset);\n            // Update\n            bottomOffset -= page.getMeasuredHeight() + interval;\n            index--;\n        }\n\n        // Avoid space in top\n        page = pages.getFirst();\n        if (0 == page.getIndex() && page.bounds().top > 0) {\n            int offset = -page.bounds().top;\n            for (GalleryPageView p : pages) {\n                p.offsetTopAndBottom(offset);\n            }\n            topOffset += offset;\n        }\n\n        // Check down\n        index = startIndex + 1;\n        while (topOffset < maxY && index < size) {\n            page = getPageForIndex(tempPages, index, true);\n            if (null == page) {\n                page = obtainPage();\n                galleryView.addComponent(page);\n                adapter.bind(page, index);\n            }\n            pages.addLast(page);\n            page.measure(widthSpec, heightSpec);\n            page.layout(mOffsetX, topOffset, mOffsetX + pageWidth, topOffset + page.getMeasuredHeight());\n            // Update\n            topOffset += page.getMeasuredHeight() + interval;\n            index++;\n        }\n\n        // Avoid space in bottom\n        if (size - 1 == pages.getLast().getIndex()) {\n            while (true) {\n                page = pages.getLast();\n                int pagesBottom = page.bounds().bottom;\n                if (pagesBottom >= height) {\n                    break;\n                }\n                page = pages.getFirst();\n                index = page.getIndex();\n                if (0 == index) {\n                    break;\n                }\n                --index;\n                int pagesTop = page.bounds().top;\n\n                page = getPageForIndex(tempPages, index, true);\n                if (null == page) {\n                    page = obtainPage();\n                    galleryView.addComponent(page);\n                    adapter.bind(page, index);\n                }\n                pages.addFirst(page);\n                page.measure(widthSpec, heightSpec);\n\n                int offset = Math.min(height - pagesBottom, page.getMeasuredHeight());\n                for (GalleryPageView p : pages) {\n                    p.offsetTopAndBottom(offset);\n                }\n                int bottom = pagesTop - interval + offset;\n                page.layout(mOffsetX, bottom - page.getMeasuredHeight(), mOffsetX + pageWidth, bottom);\n            }\n        }\n\n        // Remove remain page\n        for (GalleryPageView p : tempPages) {\n            removePage(p);\n        }\n        tempPages.clear();\n\n        // Update state\n        if (!pages.isEmpty()) {\n            page = pages.getFirst();\n            mIndex = page.getIndex();\n            mOffsetY = page.bounds().top;\n        }\n    }\n\n    @Override\n    public void onFill() {\n        GalleryView.Adapter adapter = mAdapter;\n        GalleryView galleryView = mGalleryView;\n        AssertUtils.assertNotNull(\"The PagerLayoutManager is not attached\", adapter);\n\n        int size = adapter.size();\n\n        if (size == 0) { // Get empty, show error text\n            String errorStr = galleryView.getEmptyStr();\n\n            // Remove progress and all pages\n            removeProgress();\n            removeAllPages();\n\n            // Ensure error view\n            if (mErrorView == null) {\n                mErrorView = galleryView.obtainErrorView();\n                galleryView.addComponent(mErrorView);\n            }\n\n            // Update error string\n            if (!errorStr.equals(mErrorStr)) {\n                mErrorStr = errorStr;\n                galleryView.bindErrorView(mErrorView, errorStr);\n            }\n\n            // Place error view center\n            placeCenter(mErrorView);\n        } else {\n            // Remove progress and error view\n            removeProgress();\n            removeErrorView();\n\n            // Ensure index in range\n            int index = mIndex;\n            if (index < 0) {\n                Log.e(TAG, \"index < 0, index = \" + index);\n                index = 0;\n                mIndex = index;\n                removeAllPages();\n            } else if (index >= size) {\n                Log.e(TAG, \"index >= size, index = \" + index + \", size = \" + size);\n                index = size - 1;\n                mIndex = index;\n                removeAllPages();\n            }\n\n            // Find keep index and keep top\n            int keepTop = INVALID_TOP;\n            int keepTopIndex;\n            if (GalleryPageView.INVALID_INDEX != mKeepTopPageIndex) {\n                keepTopIndex = mKeepTopPageIndex;\n                keepTop = mKeepTop;\n                mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;\n            } else if (GalleryPageView.INVALID_INDEX != mFirstShownPageIndex) {\n                keepTopIndex = mFirstShownPageIndex;\n            } else {\n                keepTopIndex = GalleryPageView.INVALID_INDEX;\n            }\n            if (GalleryPageView.INVALID_INDEX != keepTopIndex && INVALID_TOP == keepTop) {\n                keepTop = mOffsetY;\n                for (GalleryPageView page : mPages) {\n                    // Check keep page\n                    if (keepTopIndex == page.getIndex()) {\n                        break;\n                    }\n                    keepTop += page.getHeight() + mInterval;\n                }\n            }\n\n            int startIndex;\n            int startOffset;\n            if (GalleryPageView.INVALID_INDEX != keepTopIndex) {\n                startIndex = keepTopIndex;\n                startOffset = keepTop;\n            } else {\n                startIndex = mIndex;\n                startOffset = mOffsetY;\n            }\n            fillPages(startIndex, startOffset);\n\n            // Get first shown image\n            mFirstShownPageIndex = GalleryPageView.INVALID_INDEX;\n            for (GalleryPageView page : mPages) {\n                // Check first shown loaded page\n                if ((mScrollUp || mFlingUp) && !page.isLoaded()) {\n                    continue;\n                }\n\n                if (isInScreen(page, true)) {\n                    mFirstShownPageIndex = page.getIndex();\n                    break;\n                }\n            }\n        }\n    }\n\n    @Override\n    public void onDown() {\n        mScrollUp = false;\n        mStopAnimationFinger = cancelAllAnimations();\n    }\n\n    @Override\n    public void onUp() {\n        mScrollUp = false;\n    }\n\n    @Override\n    public void onDoubleTapConfirmed(float x, float y) {\n        if (mPages.isEmpty()) {\n            return;\n        }\n\n        float startScale = mScale;\n        float endScale;\n        if (startScale < DEFAULT_SCALE - SCALE_ERROR) {\n            endScale = DEFAULT_SCALE;\n        } else if (startScale < MAX_SCALE - SCALE_ERROR) {\n            endScale = MAX_SCALE;\n        } else {\n            endScale = DEFAULT_SCALE;\n        }\n\n        mSmoothScaler.startSmoothScaler(x, y, startScale, endScale, 300);\n    }\n\n    private void getBottomState() {\n        List<GalleryPageView> pages = mPages;\n        int bottom = mOffsetY;\n        int i = 0;\n        for (GalleryPageView page : pages) {\n            if (i != 0) {\n                bottom += mInterval;\n            }\n            bottom += page.getHeight();\n            i++;\n        }\n        boolean hasNext = mIndex + pages.size() < mAdapter.size();\n\n        mBottomStateBottom = bottom;\n        mBottomStateHasNext = hasNext;\n    }\n\n    // True for get top or bottom\n    private boolean scrollInternal(float dx, float dy) {\n        if (mPages.isEmpty()) {\n            return false;\n        }\n\n        GalleryView galleryView = mGalleryView;\n        int width = galleryView.getWidth();\n        int height = galleryView.getHeight();\n        int pageWidth = (int) (width * mScale);\n        final float reservation = getReservation();\n        boolean requestFill = false;\n        boolean result = false;\n\n        int margin = pageWidth - width;\n        int dxInt = (int) dx;\n        if (margin > 0 && 0 != dxInt) {\n            int oldOffsetX = mOffsetX;\n            int exceptOffsetX = oldOffsetX - dxInt;\n            mOffsetX = MathUtils.clamp(exceptOffsetX, -margin, 0);\n            if (mOffsetX != oldOffsetX) {\n                requestFill = true;\n            }\n        }\n\n        int remainY = (int) dy;\n        while (remainY != 0) {\n            if (remainY < 0) { // Try to show top\n                int limit;\n                if (mIndex > 0) {\n                    limit = (int) (-height * reservation) + mInterval;\n                } else {\n                    limit = 0;\n                }\n\n                if (mOffsetY - remainY <= limit) {\n                    mOffsetY -= remainY;\n                    remainY = 0;\n                    requestFill = true;\n                } else {\n                    if (mIndex > 0) {\n                        mOffsetY = limit;\n                        // Offset one pixel to avoid infinite loop\n                        ++mOffsetY;\n                        ++remainY;\n                        galleryView.forceFill();\n                        requestFill = false;\n                    } else {\n                        if (mOffsetY != limit) {\n                            mOffsetY = limit;\n                            requestFill = true;\n                        }\n                        remainY = 0;\n                        result = true;\n                    }\n                }\n            } else { // Try to show bottom\n                getBottomState();\n                int bottom = mBottomStateBottom;\n                boolean hasNext = mBottomStateHasNext;\n\n                int limit;\n                if (hasNext) {\n                    limit = (int) (height * (1 + reservation)) - mInterval;\n                } else {\n                    limit = height;\n                }\n                // Fix limit for page not fill screen\n                limit = Math.min(bottom, limit);\n\n                if (bottom - remainY >= limit) {\n                    mOffsetY -= remainY;\n                    remainY = 0;\n                    requestFill = true;\n                } else {\n                    if (hasNext) {\n                        mOffsetY -= bottom - limit;\n                        remainY = remainY + limit - bottom;\n                        // Offset one pixel to avoid infinite loop\n                        --mOffsetY;\n                        --remainY;\n                        galleryView.forceFill();\n                        requestFill = false;\n                    } else {\n                        if (mOffsetY != limit) {\n                            mOffsetY -= bottom - limit;\n                            requestFill = true;\n                        }\n                        remainY = 0;\n                        result = true;\n                    }\n                }\n            }\n        }\n\n        if (requestFill) {\n            mGalleryView.requestFill();\n        }\n\n        return result;\n    }\n\n    @Override\n    public void onScroll(float dx, float dy, float totalX, float totalY, float x, float y) {\n        mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;\n        mKeepTop = INVALID_TOP;\n        mScrollUp = dy < 0;\n        scrollInternal(dx, dy);\n    }\n\n    @Override\n    public void onFling(float velocityX, float velocityY) {\n        if (mPages.isEmpty()) {\n            return;\n        }\n\n        mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;\n        mKeepTop = INVALID_TOP;\n        mFlingUp = velocityY > 0;\n\n        int maxX;\n        int minX;\n        int width = mGalleryView.getWidth();\n        int pageWidth = (int) (width * mScale);\n        int margin = pageWidth - width;\n        if (margin > 0) {\n            maxX = -mOffsetX;\n            minX = -margin + mOffsetX;\n        } else {\n            maxX = 0;\n            minX = 0;\n        }\n\n        int maxY;\n        if (mIndex > 0) {\n            maxY = Integer.MAX_VALUE;\n        } else {\n            maxY = -mOffsetY;\n        }\n\n        getBottomState();\n        int bottom = mBottomStateBottom;\n        boolean hasNext = mBottomStateHasNext;\n        int minY;\n        if (hasNext) {\n            minY = Integer.MIN_VALUE;\n        } else {\n            minY = mGalleryView.getHeight() - bottom;\n        }\n\n        mPageFling.startFling((int) velocityX, minX, maxX,\n                (int) velocityY, minY, maxY);\n    }\n\n    @Override\n    public boolean canScale() {\n        return !mPages.isEmpty();\n    }\n\n    @Override\n    public void onScale(float focusX, float focusY, float scale) {\n        if (mPages.isEmpty()) {\n            return;\n        }\n\n        float oldScale = mScale;\n        mScale = MathUtils.clamp(oldScale * scale, MIN_SCALE, MAX_SCALE);\n        scale = mScale / oldScale;\n\n        if (oldScale != mScale) {\n            GalleryPageView page = null;\n            // Keep scale page origin position\n            for (GalleryPageView p : mPages) {\n                if (p.bounds().top < focusY) {\n                    page = p;\n                } else {\n                    break;\n                }\n            }\n\n            if (null != page) {\n                mKeepTopPageIndex = page.getIndex();\n                mKeepTop = page.bounds().top;\n\n                mGalleryView.forceFill();\n                int oldKeepTop = mKeepTop;\n                mKeepTop = INVALID_TOP;\n\n                // Apply scroll\n                int newOffsetX = (int) (focusX - ((focusX - mOffsetX) * scale));\n                int newKeepTop;\n                if (page.isLoaded()) {\n                    newKeepTop = (int) (focusY - ((focusY - oldKeepTop) * scale));\n                } else {\n                    newKeepTop = oldKeepTop;\n                }\n                scrollInternal(mOffsetX - newOffsetX, oldKeepTop - newKeepTop);\n            } else {\n                Log.e(TAG, \"Can't find target page\");\n                mKeepTopPageIndex = GalleryPageView.INVALID_INDEX;\n                mKeepTop = INVALID_TOP;\n                mGalleryView.forceFill();\n            }\n        }\n    }\n\n    @Override\n    public boolean onUpdateAnimation(long time) {\n        boolean invalidate = mPageFling.calculate(time);\n        invalidate |= mSmoothScaler.calculate(time);\n        return invalidate;\n    }\n\n    @Override\n    public void onDataChanged() {\n        AssertUtils.assertNotNull(\"The PagerLayoutManager is not attached\", mAdapter);\n\n        // Cancel all animations\n        cancelAllAnimations();\n        // Remove all views\n        removeProgress();\n        removeErrorView();\n        removeAllPages();\n        // Reset parameters\n        resetParameters();\n        mGalleryView.requestFill();\n    }\n\n    @Override\n    public void onPageLeft() {\n        if (mAdapter.size() <= 0 || mPages.isEmpty()) {\n            return;\n        }\n\n        ///////\n        // UP\n        ///////\n        GalleryView galleryView = mGalleryView;\n        if (mIndex > 0 || mOffsetY < 0) {\n            // Cancel all animations\n            cancelAllAnimations();\n\n            // Get first shown page\n            GalleryPageView previousPage = null;\n            GalleryPageView firstShownPage = null;\n            for (GalleryPageView p : mPages) {\n                if (isInScreen(p, true)) {\n                    firstShownPage = p;\n                    break;\n                }\n                previousPage = p;\n            }\n\n            int height = galleryView.getHeight();\n            int maxOffset = height - mInterval;\n            if (null == firstShownPage) {\n                Log.e(TAG, \"Can't find first shown page when paging left\");\n                mOffsetY += height / 2;\n            } else {\n                int firstShownTop = firstShownPage.bounds().top;\n                if (firstShownTop >= 0) {\n                    if (null == previousPage) {\n                        Log.e(TAG, \"Can't find previous page when paging left and offsetY == 0\");\n                        mOffsetY += height / 2;\n                    } else {\n                        mOffsetY += Math.min(maxOffset, -previousPage.bounds().top);\n                    }\n                } else {\n                    mOffsetY += Math.min(maxOffset, -firstShownTop);\n                }\n            }\n\n            // Request fill\n            mGalleryView.requestFill();\n        }\n    }\n\n    @Override\n    public void onPageRight() {\n        if (mAdapter.size() <= 0 || mPages.isEmpty()) {\n            return;\n        }\n\n        /////////\n        // DOWN\n        /////////\n        GalleryView galleryView = mGalleryView;\n        getBottomState();\n        int bottom = mBottomStateBottom;\n        boolean hasNext = mBottomStateHasNext;\n        if (hasNext || bottom > galleryView.getHeight()) {\n            // Cancel all animations\n            cancelAllAnimations();\n\n            // Get first shown page\n            GalleryPageView lastShownPage = null;\n            GalleryPageView nextPage = null;\n            for (GalleryPageView p : mPages) {\n                if (isInScreen(p, true)) {\n                    lastShownPage = p;\n                } else if (null != lastShownPage) {\n                    nextPage = p;\n                    break;\n                }\n            }\n\n            int height = galleryView.getHeight();\n            int maxOffset = height - mInterval;\n            if (null == lastShownPage) {\n                Log.e(TAG, \"Can't find last shown page when paging left\");\n                mOffsetY -= height / 2;\n            } else {\n                int lastShownBottom = lastShownPage.bounds().bottom;\n                if (lastShownBottom <= height) {\n                    if (null == nextPage) {\n                        Log.e(TAG, \"Can't find previous page when paging left and offsetY == 0\");\n                        mOffsetY -= height / 2;\n                    } else {\n                        mOffsetY -= Math.min(maxOffset, nextPage.bounds().bottom - height);\n                    }\n                } else {\n                    mOffsetY -= Math.min(maxOffset, lastShownBottom - height);\n                }\n            }\n\n            // Request fill\n            mGalleryView.requestFill();\n        }\n    }\n\n    @Override\n    public boolean isTapOrPressDisable() {\n        return mStopAnimationFinger;\n    }\n\n    @Override\n    public GalleryPageView findPageByIndex(int index) {\n        for (GalleryPageView page : mPages) {\n            if (page.getIndex() == index) {\n                return page;\n            }\n        }\n        return null;\n    }\n\n    @Override\n    public int getCurrentIndex() {\n        int index = GalleryPageView.INVALID_INDEX;\n        for (GalleryPageView page : mPages) {\n            if (isInScreen(page, false)) {\n                index = page.getIndex();\n            }\n        }\n        return index;\n    }\n\n    @Override\n    public void setCurrentIndex(int index) {\n        int size = mAdapter.size();\n        if (size <= 0) {\n            // Can't get size now, assume size is MAX\n            size = Integer.MAX_VALUE;\n        }\n        if (index < 0 || index >= size) {\n            return;\n        }\n\n        mKeepTopPageIndex = index;\n        mKeepTop = INVALID_TOP;\n\n        if (mPages.isEmpty()) {\n            mIndex = index;\n        } else {\n            // Fix the index page\n            GalleryPageView targetPage = null;\n            for (GalleryPageView page : mPages) {\n                if (page.getIndex() == index) {\n                    targetPage = page;\n                    break;\n                }\n            }\n\n            if (targetPage != null) {\n                // Cancel all animations\n                cancelAllAnimations();\n                mOffsetY -= targetPage.bounds().top;\n                // Request fill\n                mGalleryView.requestFill();\n            } else {\n                mIndex = index;\n                mOffsetY = 0;\n                // Cancel all animations\n                cancelAllAnimations();\n                // Remove all view\n                removeProgress();\n                removeErrorView();\n                removeAllPages();\n                // Request fill\n                mGalleryView.requestFill();\n            }\n        }\n    }\n\n    @Override\n    public int getIndexUnder(float x, float y) {\n        if (!mPages.isEmpty()) {\n            int intX = (int) x;\n            int intY = (int) y;\n            for (GalleryPageView page : mPages) {\n                if (page.bounds().contains(intX, intY)) {\n                    return page.getIndex();\n                }\n            }\n        }\n        return GalleryPageView.INVALID_INDEX;\n    }\n\n    @Override\n    int getInternalCurrentIndex() {\n        int currentIndex = getCurrentIndex();\n        if (currentIndex == GalleryPageView.INVALID_INDEX) {\n            currentIndex = mIndex;\n        }\n        return currentIndex;\n    }\n\n    private class PageFling extends Fling {\n        private int mDx;\n        private int mDy;\n        private int mLastX;\n        private int mLastY;\n\n        public PageFling(Context context) {\n            super(context);\n        }\n\n        public void startFling(int velocityX, int minX, int maxX,\n                               int velocityY, int minY, int maxY) {\n            mDx = (int) (getSplineFlingDistance(velocityX) * Math.signum(velocityX));\n            mDy = (int) (getSplineFlingDistance(velocityY) * Math.signum(velocityY));\n            mLastX = 0;\n            mLastY = 0;\n            int durationX = getSplineFlingDuration(velocityX);\n            int durationY = getSplineFlingDuration(velocityY);\n\n            if (mDx < minX) {\n                durationX = adjustDuration(mDx, minX, durationX);\n                mDx = minX;\n            }\n            if (mDx > maxX) {\n                durationX = adjustDuration(mDx, maxX, durationX);\n                mDx = maxX;\n            }\n            if (mDy < minY) {\n                durationY = adjustDuration(mDy, minY, durationY);\n                mDy = minY;\n            }\n            if (mDy > maxY) {\n                durationY = adjustDuration(mDy, maxY, durationY);\n                mDy = maxY;\n            }\n\n            if (mDx == 0 && mDy == 0) {\n                return;\n            }\n\n            setDuration(Math.max(durationX, durationY));\n            start();\n            mGalleryView.invalidate();\n        }\n\n        @Override\n        protected void onCalculate(float progress) {\n            int x = (int) (mDx * progress);\n            int y = (int) (mDy * progress);\n            int offsetX = x - mLastX;\n            int offsetY = y - mLastY;\n            if (scrollInternal(-offsetX, -offsetY)) {\n                cancel();\n                onFinish();\n            }\n            mLastX = x;\n            mLastY = y;\n        }\n\n        @Override\n        protected void onFinish() {\n            mFlingUp = false;\n            getBottomState();\n        }\n    }\n\n    private class SmoothScaler extends Animation {\n        private float mFocusX;\n        private float mFocusY;\n        private float mStartScale;\n        private float mEndScale;\n        private float mLastScale;\n\n        public SmoothScaler() {\n            setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);\n        }\n\n        public void startSmoothScaler(float focusX, float focusY,\n                                      float startScale, float endScale, int duration) {\n            mFocusX = focusX;\n            mFocusY = focusY;\n            mStartScale = startScale;\n            mEndScale = endScale;\n            mLastScale = startScale;\n            setDuration(duration);\n            start();\n            mGalleryView.invalidate();\n        }\n\n        @Override\n        protected void onCalculate(float progress) {\n            if (mPages.isEmpty()) {\n                return;\n            }\n\n            float scale = MathUtils.lerp(mStartScale, mEndScale, progress);\n            onScale(mFocusX, mFocusY, scale / mLastScale);\n            mLastScale = scale;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glgallery/SimpleAdapter.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glgallery;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.glview.image.ImageTexture;\nimport com.hippo.glview.image.ImageWrapper;\nimport com.hippo.glview.view.GLRootView;\n\npublic class SimpleAdapter extends GalleryView.Adapter implements GalleryProvider.Listener {\n    private final GalleryProvider mProvider;\n    private final ImageTexture.Uploader mUploader;\n\n    public SimpleAdapter(@NonNull GLRootView glRootView, @NonNull GalleryProvider provider) {\n        mProvider = provider;\n        mUploader = new ImageTexture.Uploader(glRootView);\n    }\n\n    public void clearUploader() {\n        mUploader.clear();\n    }\n\n    @Override\n    public void onBind(GalleryPageView view, int index) {\n        mProvider.request(index);\n        view.showInfo();\n        view.setImage(null);\n        view.setPage(index + 1);\n        view.setProgress(GalleryPageView.PROGRESS_INDETERMINATE);\n        view.setError(null, null);\n    }\n\n    @Override\n    public void onUnbind(GalleryPageView view, int index) {\n        mProvider.cancelRequest(index);\n        view.setImage(null);\n        view.setError(null, null);\n    }\n\n    @Override\n    public int size() {\n        return mProvider.getSize();\n    }\n\n    @Override\n    public void onDataChanged() {\n        mGalleryView.onDataChanged();\n    }\n\n    @Override\n    public void onPageWait(int index) {\n        GalleryPageView page = findPageByIndex(index);\n        if (page != null) {\n            page.showInfo();\n            page.setImage(null);\n            page.setPage(index + 1);\n            page.setProgress(GalleryPageView.PROGRESS_INDETERMINATE);\n            page.setError(null, null);\n        }\n    }\n\n    @Override\n    public void onPagePercent(int index, float percent) {\n        GalleryPageView page = findPageByIndex(index);\n        if (page != null) {\n            page.showInfo();\n            page.setImage(null);\n            page.setPage(index + 1);\n            page.setProgress(percent);\n            page.setError(null, null);\n        }\n    }\n\n    @Override\n    public void onPageSucceed(int index, ImageWrapper image) {\n        GalleryPageView page = findPageByIndex(index);\n        if (page != null) {\n            if (image.obtain()) {\n                ImageTexture imageTexture = new ImageTexture(image);\n                mUploader.addTexture(imageTexture);\n                page.showImage();\n                page.setImage(imageTexture);\n                page.setPage(index + 1);\n                page.setProgress(GalleryPageView.PROGRESS_GONE);\n                page.setError(null, null);\n            } else {\n                // The image is recycled, request again.\n                // TODO request loop ?\n                mProvider.request(index);\n            }\n        }\n    }\n\n    @Override\n    public void onPageFailed(int index, String error) {\n        GalleryPageView page = findPageByIndex(index);\n        if (page != null) {\n            page.showInfo();\n            page.setImage(null);\n            page.setPage(index + 1);\n            page.setProgress(GalleryPageView.PROGRESS_GONE);\n            page.setError(error, mGalleryView);\n        }\n    }\n\n    @Override\n    public void onDataChanged(int index) {\n        GalleryPageView page = findPageByIndex(index);\n        if (page != null) {\n            mProvider.request(index);\n        }\n    }\n\n    private GalleryPageView findPageByIndex(int index) {\n        return mGalleryView != null ? mGalleryView.findPageByIndex(index) : null;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/anim/AlphaAnimation.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.anim;\n\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.yorozuya.MathUtils;\n\npublic class AlphaAnimation extends CanvasAnimation {\n    private final float mStartAlpha;\n    private final float mEndAlpha;\n    private float mCurrentAlpha;\n\n    public AlphaAnimation(float from, float to) {\n        mStartAlpha = from;\n        mEndAlpha = to;\n        mCurrentAlpha = from;\n    }\n\n    @Override\n    public void apply(GLCanvas canvas) {\n        canvas.multiplyAlpha(mCurrentAlpha);\n    }\n\n    @Override\n    public int getCanvasSaveFlags() {\n        return GLCanvas.SAVE_FLAG_ALPHA;\n    }\n\n    @Override\n    protected void onCalculate(float progress) {\n        mCurrentAlpha = MathUtils.clamp(mStartAlpha +\n                (mEndAlpha - mStartAlpha) * progress, 0f, 1f);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/anim/Animation.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.anim;\n\nimport android.os.SystemClock;\nimport android.view.animation.Interpolator;\n\nimport com.hippo.yorozuya.MathUtils;\n\n// Animation calculates a value according to the current input time.\n//\n// 1. First we need to use setDuration(int) to set the duration of the\n//    animation. The duration is in milliseconds.\n// 2. Then we should call start(). The actual start time is the first value\n//    passed to calculate(long).\n// 3. Each time we want to get an animation value, we call\n//    calculate(long currentTimeMillis) to ask the Animation to calculate it.\n//    The parameter passed to calculate(long) should be nonnegative.\n// 4. Use get() to get that value.\n//\n// In step 3, onCalculate(float progress) is called so subclasses can calculate\n// the value according to progress (progress is a value in [0,1]).\n//\n// Before onCalculate(float) is called, There is an optional interpolator which\n// can change the progress value. The interpolator can be set by\n// setInterpolator(Interpolator). If the interpolator is used, the value passed\n// to onCalculate may be (for example, the overshoot effect).\n//\n// The isActive() method returns true after the animation start() is called and\n// before calculate is passed a value which reaches the duration of the\n// animation.\n//\n// The start() method can be called again to restart the Animation.\n//\nabstract public class Animation {\n    /**\n     * When the animation reaches the end and <code>repeatCount</code> is INFINITE\n     * or a positive value, the animation restarts from the beginning.\n     */\n    public static final int RESTART = 1;\n    /**\n     * When the animation reaches the end and <code>repeatCount</code> is INFINITE\n     * or a positive value, the animation reverses direction on every iteration.\n     */\n    public static final int REVERSE = 2;\n    /**\n     * This value used used with the {@link #setRepeatCount(int)} property to repeat\n     * the animation indefinitely.\n     */\n    public static final int INFINITE = -1;\n    private static final long ANIMATION_START = -1;\n    private static final long NO_ANIMATION = -2;\n    private long mStartTime = NO_ANIMATION;\n    private long mDuration;\n    private Interpolator mInterpolator;\n    private int mRepeatCount;\n\n    private int mRunnedCount;\n    private long mLastFrameTime;\n\n    public void setInterpolator(Interpolator interpolator) {\n        mInterpolator = interpolator;\n    }\n\n    public void setDuration(long duration) {\n        mDuration = duration;\n    }\n\n    public void setRepeatCount(int repeatCount) {\n        mRepeatCount = repeatCount;\n    }\n\n    public boolean isRunning() {\n        return mStartTime > 0 || mStartTime == ANIMATION_START;\n    }\n\n    public long getLastFrameTime() {\n        return mLastFrameTime;\n    }\n\n    public void start() {\n        if (mStartTime == NO_ANIMATION) {\n            mStartTime = ANIMATION_START;\n            mRunnedCount = 0;\n            mLastFrameTime = 0;\n        }\n    }\n\n    public void startAt(long time) {\n        start();\n        setStartTime(time);\n    }\n\n    public void startNow() {\n        start();\n        setStartTime(SystemClock.uptimeMillis());\n    }\n\n    public void setStartTime(long time) {\n        mStartTime = time;\n    }\n\n    public void cancel() {\n        mStartTime = NO_ANIMATION;\n    }\n\n    public void reset() {\n        mStartTime = ANIMATION_START;\n        mRunnedCount = 0;\n        mLastFrameTime = 0;\n    }\n\n    public boolean calculate(long currentTimeMillis) {\n        if (mStartTime == NO_ANIMATION) {\n            return false;\n        }\n        if (mStartTime == ANIMATION_START) {\n            mStartTime = currentTimeMillis;\n        }\n        mLastFrameTime = currentTimeMillis;\n\n        long elapse = currentTimeMillis - mStartTime;\n        float x = MathUtils.clamp(mDuration == 0.0f ? 1.0f : (float) elapse / mDuration, 0.0f, 1.0f); // Avoid NaN\n        Interpolator i = mInterpolator;\n        onCalculate(i != null ? i.getInterpolation(x) : x);\n\n        // It is ok to call cancel() in onCalculate()\n        if (mStartTime != NO_ANIMATION && elapse >= mDuration) {\n            mRunnedCount++;\n            if (mRunnedCount >= mRepeatCount && mRepeatCount != INFINITE) {\n                onFinish();\n                mStartTime = NO_ANIMATION;\n            } else {\n                mStartTime += elapse;\n            }\n        }\n\n        return mStartTime != NO_ANIMATION;\n    }\n\n    abstract protected void onCalculate(float progress);\n\n    protected void onFinish() {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/anim/CanvasAnimation.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.anim;\n\nimport com.hippo.glview.glrenderer.GLCanvas;\n\npublic abstract class CanvasAnimation extends Animation {\n    public abstract int getCanvasSaveFlags();\n\n    public abstract void apply(GLCanvas canvas);\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/anim/FloatAnimation.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.anim;\n\nimport com.hippo.yorozuya.MathUtils;\n\npublic class FloatAnimation extends Animation {\n    private float mFrom;\n    private float mTo;\n    private float mCurrent;\n\n    public void setRange(float from, float to) {\n        mFrom = from;\n        mTo = to;\n    }\n\n    @Override\n    protected void onCalculate(float progress) {\n        mCurrent = MathUtils.lerp(mFrom, mTo, progress);\n    }\n\n    public float get() {\n        return mCurrent;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/BasicTexture.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.RectF;\nimport android.util.Log;\n\nimport com.hippo.yorozuya.MathUtils;\n\nimport java.util.WeakHashMap;\n\n// BasicTexture is a Texture corresponds to a real GL texture.\n// The state of a BasicTexture indicates whether its data is loaded to GL memory.\n// If a BasicTexture is loaded into GL memory, it has a GL texture id.\npublic abstract class BasicTexture implements Texture {\n    protected static final int UNSPECIFIED = -1;\n    protected static final int STATE_UNLOADED = 0;\n    protected static final int STATE_LOADED = 1;\n    protected static final int STATE_ERROR = -1;\n    private static final String TAG = \"BasicTexture\";\n    // Log a warning if a texture is larger along a dimension\n    private static final int MAX_TEXTURE_SIZE = 4096;\n    private final static WeakHashMap<BasicTexture, Object> sAllTextures\n            = new WeakHashMap<>();\n    private static final ThreadLocal<Class<?>> sInFinalizer = new ThreadLocal<>();\n    protected int mId = -1;\n    protected int mState;\n    protected int mWidth = UNSPECIFIED;\n    protected int mHeight = UNSPECIFIED;\n    protected int mTextureWidth;\n    protected int mTextureHeight;\n    protected GLCanvas mCanvasRef = null;\n    private boolean mHasBorder;\n\n    protected BasicTexture(GLCanvas canvas, int id, int state) {\n        setAssociatedCanvas(canvas);\n        mId = id;\n        mState = state;\n        synchronized (sAllTextures) {\n            sAllTextures.put(this, null);\n        }\n    }\n\n    protected BasicTexture() {\n        this(null, 0, STATE_UNLOADED);\n    }\n\n    // This is for deciding if we can call Bitmap's recycle().\n    // We cannot call Bitmap's recycle() in finalizer because at that point\n    // the finalizer of Bitmap may already be called so recycle() will crash.\n    public static boolean inFinalizer() {\n        return sInFinalizer.get() != null;\n    }\n\n    public static void yieldAllTextures() {\n        synchronized (sAllTextures) {\n            for (BasicTexture t : sAllTextures.keySet()) {\n                t.yield();\n            }\n        }\n    }\n\n    public static void invalidateAllTextures() {\n        synchronized (sAllTextures) {\n            for (BasicTexture t : sAllTextures.keySet()) {\n                t.mState = STATE_UNLOADED;\n                t.setAssociatedCanvas(null);\n            }\n        }\n    }\n\n    protected void setAssociatedCanvas(GLCanvas canvas) {\n        mCanvasRef = canvas;\n    }\n\n    /**\n     * Sets the content size of this texture. In OpenGL, the actual texture\n     * size must be of power of 2, the size of the content may be smaller.\n     */\n    public void setSize(int width, int height) {\n        mWidth = width;\n        mHeight = height;\n        mTextureWidth = width > 0 ? MathUtils.nextPowerOf2(width) : 0;\n        mTextureHeight = height > 0 ? MathUtils.nextPowerOf2(height) : 0;\n        if (mTextureWidth > MAX_TEXTURE_SIZE || mTextureHeight > MAX_TEXTURE_SIZE) {\n            Log.w(TAG, String.format(\"texture is too large: %d x %d\",\n                    mTextureWidth, mTextureHeight), new Exception());\n        }\n    }\n\n    public boolean isFlippedVertically() {\n        return false;\n    }\n\n    public int getId() {\n        return mId;\n    }\n\n    @Override\n    public int getWidth() {\n        return mWidth;\n    }\n\n    @Override\n    public int getHeight() {\n        return mHeight;\n    }\n\n    // Returns the width rounded to the next power of 2.\n    public int getTextureWidth() {\n        return mTextureWidth;\n    }\n\n    // Returns the height rounded to the next power of 2.\n    public int getTextureHeight() {\n        return mTextureHeight;\n    }\n\n    // Returns true if the texture has one pixel transparent border around the\n    // actual content. This is used to avoid jigged edges.\n    //\n    // The jigged edges appear because we use GL_CLAMP_TO_EDGE for texture wrap\n    // mode (GL_CLAMP is not available in OpenGL ES), so a pixel partially\n    // covered by the texture will use the color of the edge texel. If we add\n    // the transparent border, the color of the edge texel will be mixed with\n    // appropriate amount of transparent.\n    //\n    // Currently our background is black, so we can draw the thumbnails without\n    // enabling blending.\n    public boolean hasBorder() {\n        return mHasBorder;\n    }\n\n    protected void setBorder(boolean hasBorder) {\n        mHasBorder = hasBorder;\n    }\n\n    @Override\n    public void draw(GLCanvas canvas, int x, int y) {\n        canvas.drawTexture(this, x, y, getWidth(), getHeight());\n    }\n\n    @Override\n    public void draw(GLCanvas canvas, int x, int y, int w, int h) {\n        canvas.drawTexture(this, x, y, w, h);\n    }\n\n    @Override\n    public void draw(GLCanvas canvas, RectF source, RectF target) {\n        canvas.drawTexture(this, source, target);\n    }\n\n    // onBind is called before GLCanvas binds this texture.\n    // It should make sure the data is uploaded to GL memory.\n    abstract protected boolean onBind(GLCanvas canvas);\n\n    // Returns the GL texture target for this texture (e.g. GL_TEXTURE_2D).\n    abstract protected int getTarget();\n\n    public boolean isLoaded() {\n        return mState == STATE_LOADED;\n    }\n\n    // recycle() is called when the texture will never be used again,\n    // so it can free all resources.\n    public void recycle() {\n        freeResource();\n    }\n\n    // yield() is called when the texture will not be used temporarily,\n    // so it can free some resources.\n    // The default implementation unloads the texture from GL memory, so\n    // the subclass should make sure it can reload the texture to GL memory\n    // later, or it will have to override this method.\n    public void yield() {\n        freeResource();\n    }\n\n    private void freeResource() {\n        GLCanvas canvas = mCanvasRef;\n        if (canvas != null && mId != -1) {\n            canvas.unloadTexture(this);\n            mId = -1; // Don't free it again.\n        }\n        mState = STATE_UNLOADED;\n        setAssociatedCanvas(null);\n    }\n\n    @Override\n    protected void finalize() throws Throwable {\n        try {\n            sInFinalizer.set(BasicTexture.class);\n            recycle();\n            sInFinalizer.remove();\n        } finally {\n            super.finalize();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/CanvasTexture.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Bitmap.Config;\nimport android.graphics.Canvas;\n\n// CanvasTexture is a texture whose content is the drawing on a Canvas.\n// The subclasses should override onDraw() to draw on the bitmap.\n// By default CanvasTexture is not opaque.\nabstract class CanvasTexture extends UploadedTexture {\n    private final Config mConfig;\n\n    public CanvasTexture(int width, int height) {\n        mConfig = Config.ARGB_8888;\n        setSize(width, height);\n        setOpaque(false);\n    }\n\n    @Override\n    protected Bitmap onGetBitmap() {\n        Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, mConfig);\n        Canvas canvas = new Canvas(bitmap);\n        onDraw(canvas, bitmap);\n        return bitmap;\n    }\n\n    @Override\n    protected void onFreeBitmap(Bitmap bitmap) {\n        if (!inFinalizer()) {\n            bitmap.recycle();\n        }\n    }\n\n    abstract protected void onDraw(Canvas canvas, Bitmap backing);\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/GLCanvas.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\n\n//\n// GLCanvas gives a convenient interface to draw using OpenGL.\n//\n// When a rectangle is specified in this interface, it means the region\n// [x, x+width) * [y, y+height)\n//\npublic interface GLCanvas {\n    int SAVE_FLAG_ALL = 0xFFFFFFFF;\n    int SAVE_FLAG_ALPHA = 0x01;\n    int SAVE_FLAG_MATRIX = 0x02;\n\n    GLId getGLId();\n\n    // Tells GLCanvas the size of the underlying GL surface. This should be\n    // called before first drawing and when the size of GL surface is changed.\n    // This is called by GLRoot and should not be called by the clients\n    // who only want to draw on the GLCanvas. Both width and height must be\n    // nonnegative.\n    void setSize(int width, int height);\n\n    // Clear the drawing buffers. This should only be used by GLRoot.\n    void clearBuffer();\n\n    void clearBuffer(float[] argb);\n\n    float getAlpha();\n\n    // Sets and gets the current alpha, alpha must be in [0, 1].\n    void setAlpha(float alpha);\n\n    // (current alpha) = (current alpha) * alpha\n    void multiplyAlpha(float alpha);\n\n    // Change the current transform matrix.\n    void translate(float x, float y, float z);\n\n    void translate(float x, float y);\n\n    void scale(float sx, float sy, float sz);\n\n    void rotate(float angle, float x, float y, float z);\n\n    void multiplyMatrix(float[] mMatrix, int offset);\n\n    // Pushes the configuration state (matrix, and alpha) onto\n    // a private stack.\n    void save();\n\n    // Same as save(), but only save those specified in saveFlags.\n    void save(int saveFlags);\n\n    // Pops from the top of the stack as current configuration state (matrix,\n    // alpha, and clip). This call balances a previous call to save(), and is\n    // used to remove all modifications to the configuration state since the\n    // last save call.\n    void restore();\n\n    // Draws a line using the specified paint from (x1, y1) to (x2, y2).\n    // (Both end points are included).\n    void drawLine(float x1, float y1, float x2, float y2, GLPaint paint);\n\n    // Draws a rectangle using the specified paint from (x1, y1) to (x2, y2).\n    // (Both end points are included).\n    void drawRect(float x1, float y1, float x2, float y2, GLPaint paint);\n\n    // Draws a oval using the specified paint for (cx, cy, radiusX, radiusY)\n    // (Both end points are included).\n    void drawOval(float cx, float cy, float radiusX, float radiusY,\n                  GLPaint paint);\n\n    // Draw a arc inside a rect\n    void drawArc(float cx, float cy, float radiusX, float radiusY,\n                 float sweepAngle, GLPaint paint);\n\n    // Fills the specified rectangle with the specified color.\n    void fillRect(float x, float y, float width, float height, int color);\n\n    // Fills the specified oval with the specified color.\n    void fillOval(float cx, float cy, float radiusX, float radiusY, int color);\n\n    // Fills the specified sector with the specified color.\n    void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color);\n\n    // Draws a texture to the specified rectangle.\n    void drawTexture(BasicTexture texture, int x, int y, int width, int height);\n\n    void drawMesh(BasicTexture tex, int x, int y, int xyBuffer, int uvBuffer, int indexBuffer, int indexCount);\n\n    // Draws the source rectangle part of the texture to the target rectangle.\n    void drawTexture(BasicTexture texture, RectF source, RectF target);\n\n    // Draw a texture with a specified texture transform.\n    void drawTexture(BasicTexture texture, float[] mTextureTransform, int x, int y, int w, int h);\n\n    // Draw two textures to the specified rectangle. The actual texture used is\n    // from * (1 - ratio) + to * ratio\n    // The two textures must have the same size.\n    void drawMixed(BasicTexture from, int toColor, float ratio, int x, int y, int w, int h);\n\n    // Draw a region of a texture and a specified color to the specified\n    // rectangle. The actual color used is from * (1 - ratio) + to * ratio.\n    // The region of the texture is defined by parameter \"src\". The target\n    // rectangle is specified by parameter \"target\".\n    void drawMixed(BasicTexture from, int toColor, float ratio, RectF src, RectF target);\n\n    // Unloads the specified texture from the canvas. The resource allocated\n    // to draw the texture will be released. The specified texture will return\n    // to the unloaded state. This function should be called only from\n    // BasicTexture or its descendant\n    boolean unloadTexture(BasicTexture texture);\n\n    // Delete the specified buffer object, similar to unloadTexture.\n    void deleteBuffer(int bufferId);\n\n    // Delete the textures and buffers in GL side. This function should only be\n    // called in the GL thread.\n    void deleteRecycledResources();\n\n    // Dump statistics information and clear the counters. For debug only.\n    void dumpStatisticsAndClear();\n\n    void beginRenderTarget(RawTexture texture);\n\n    void endRenderTarget();\n\n    /**\n     * Sets texture parameters to use GL_CLAMP_TO_EDGE for both\n     * GL_TEXTURE_WRAP_S and GL_TEXTURE_WRAP_T. Sets texture parameters to be\n     * GL_LINEAR for GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER.\n     * bindTexture() must be called prior to this.\n     *\n     * @param texture The texture to set parameters on.\n     */\n    void setTextureParameters(BasicTexture texture);\n\n    /**\n     * Initializes the texture to a size by calling texImage2D on it.\n     *\n     * @param texture The texture to initialize the size.\n     * @param format  The texture format (e.g. GL_RGBA)\n     * @param type    The texture type (e.g. GL_UNSIGNED_BYTE)\n     */\n    void initializeTextureSize(BasicTexture texture, int format, int type);\n\n    /**\n     * Initializes the texture to a size by calling texImage2D on it.\n     *\n     * @param texture The texture to initialize the size.\n     * @param bitmap  The bitmap to initialize the bitmap with.\n     */\n    void initializeTexture(BasicTexture texture, Bitmap bitmap);\n\n    /**\n     * Calls glTexSubImage2D to upload a bitmap to the texture.\n     *\n     * @param texture The target texture to write to.\n     * @param xOffset Specifies a texel offset in the x direction within the\n     *                texture array.\n     * @param yOffset Specifies a texel offset in the y direction within the\n     *                texture array.\n     * @param format  The texture format (e.g. GL_RGBA)\n     * @param type    The texture type (e.g. GL_UNSIGNED_BYTE)\n     */\n    void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, int format, int type);\n\n    /**\n     * Generates buffers and uploads the buffer data.\n     *\n     * @param buffer The buffer to upload\n     * @return The buffer ID that was generated.\n     */\n    int uploadBuffer(java.nio.FloatBuffer buffer);\n\n    /**\n     * Generates buffers and uploads the element array buffer data.\n     *\n     * @param buffer The buffer to upload\n     * @return The buffer ID that was generated.\n     */\n    int uploadBuffer(java.nio.ByteBuffer buffer);\n\n    /**\n     * After LightCycle makes GL calls, this method is called to restore the GL\n     * configuration to the one expected by GLCanvas.\n     */\n    void recoverFromLightCycle();\n\n    /**\n     * Gets the bounds given by x, y, width, and height as well as the internal\n     * matrix state. There is no special handling for non-90-degree rotations.\n     * It only considers the lower-left and upper-right corners as the bounds.\n     *\n     * @param bounds The output bounds to write to.\n     * @param x      The left side of the input rectangle.\n     * @param y      The bottom of the input rectangle.\n     * @param width  The width of the input rectangle.\n     * @param height The height of the input rectangle.\n     */\n    void getBounds(Rect bounds, int x, int y, int width, int height);\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/GLES11Canvas.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.opengl.GLU;\nimport android.opengl.GLUtils;\nimport android.opengl.Matrix;\nimport android.util.Log;\n\nimport com.hippo.yorozuya.MathUtils;\nimport com.hippo.yorozuya.collect.IntList;\n\nimport java.nio.Buffer;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.FloatBuffer;\nimport java.util.ArrayList;\nimport java.util.Locale;\n\nimport javax.microedition.khronos.opengles.GL10;\nimport javax.microedition.khronos.opengles.GL11;\nimport javax.microedition.khronos.opengles.GL11Ext;\nimport javax.microedition.khronos.opengles.GL11ExtensionPack;\n\npublic class GLES11Canvas implements GLCanvas {\n    @SuppressWarnings(\"unused\")\n    private static final String TAG = \"GLCanvasImp\";\n\n    private static final float OPAQUE_ALPHA = 0.95f;\n\n    private static final int COUNT_FILL_VERTEX = 4;\n    private static final int COUNT_LINE_VERTEX = 2;\n    private static final int COUNT_RECT_VERTEX = 4;\n    private static final int COUNT_CIRCLE_VERTEX = 120; // multiple of 4\n    private static final int OFFSET_FILL_RECT = 0;\n    private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX;\n    private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX;\n    private static final int OFFSET_FILL_CIRCLE = OFFSET_DRAW_RECT + COUNT_RECT_VERTEX;\n    private static final int OFFSET_DRAW_CIRCLE = OFFSET_FILL_CIRCLE + 1;\n    private static final int OFFSET_LAST = OFFSET_DRAW_CIRCLE + COUNT_CIRCLE_VERTEX + 1;\n\n    private static final float[] BOX_COORDINATES = new float[OFFSET_LAST * 2];\n    // TODO: the code only work for 2D should get fixed for 3D or removed\n    private static final int MSKEW_X = 4;\n    private static final int MSKEW_Y = 1;\n    private static final int MSCALE_X = 0;\n    private static final int MSCALE_Y = 5;\n    private static final float[] sCropRect = new float[4];\n    private static final GLId mGLId = new GLES11IdImpl();\n\n    static {\n        float[] temp = {\n                0, 0, // Fill rectangle\n                1, 0,\n                0, 1,\n                1, 1,\n                0, 0, // Draw line\n                1, 1,\n                0, 0, // Draw rectangle outline\n                0, 1,\n                1, 1,\n                1, 0,\n                0.5f, 0.5f // Fill circle\n        };\n        System.arraycopy(temp, 0, BOX_COORDINATES, 0, temp.length);\n\n        // Draw circle\n        int arrayOffset = OFFSET_DRAW_CIRCLE * 2;\n        for (int i = 0, n = COUNT_CIRCLE_VERTEX / 4; i <= n; i++) {\n            float value = (float) Math.sin(MathUtils.radians(90f / n * i)) / 2;\n            float positive = value + 0.5f;\n            float negative = -value + 0.5f;\n            BOX_COORDINATES[arrayOffset + i * 2 + 1] = positive;\n            BOX_COORDINATES[arrayOffset + n * 2 - i * 2] = positive;\n            BOX_COORDINATES[arrayOffset + n * 2 + i * 2] = negative;\n            BOX_COORDINATES[arrayOffset + n * 4 - i * 2 + 1] = positive;\n            BOX_COORDINATES[arrayOffset + n * 4 + i * 2 + 1] = negative;\n            BOX_COORDINATES[arrayOffset + n * 6 - i * 2] = negative;\n            BOX_COORDINATES[arrayOffset + n * 6 + i * 2] = positive;\n            BOX_COORDINATES[arrayOffset + n * 8 - i * 2 + 1] = negative;\n        }\n    }\n\n    private final float[] mMatrixValues = new float[16];\n    private final float[] mTextureMatrixValues = new float[16];\n    // The results of mapPoints are stored in this buffer, and the order is\n    // x1, y1, x2, y2.\n    private final float[] mMapPointsBuffer = new float[4];\n    private final float[] mTextureColor = new float[4];\n    private final ArrayList<RawTexture> mTargetStack = new ArrayList<>();\n    private final ArrayList<ConfigState> mRestoreStack = new ArrayList<>();\n    private final RectF mDrawTextureSourceRect = new RectF();\n    private final RectF mDrawTextureTargetRect = new RectF();\n    private final float[] mTempMatrix = new float[32];\n    private final IntList mUnboundTextures = new IntList();\n    private final IntList mDeleteBuffers = new IntList();\n    private final GL11 mGL;\n    private final int mBoxCoords;\n    private final GLState mGLState;\n    private final boolean mBlendEnabled = true;\n    private final int[] mFrameBuffer = new int[1];\n    // Drawing statistics\n    int mCountDrawLine;\n    int mCountFillRect;\n    int mCountDrawMesh;\n    int mCountTextureRect;\n    int mCountTextureOES;\n    private float mAlpha;\n    private ConfigState mRecycledRestoreAction;\n    private int mScreenWidth;\n    private int mScreenHeight;\n    private RawTexture mTargetTexture;\n\n    public GLES11Canvas(GL11 gl) {\n        mGL = gl;\n        mGLState = new GLState(gl);\n        // First create an nio buffer, then create a VBO from it.\n        int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE;\n        FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();\n        xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);\n\n        int[] name = new int[1];\n        mGLId.glGenBuffers(1, name, 0);\n        mBoxCoords = name[0];\n\n        gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);\n        gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE),\n                xyBuffer, GL11.GL_STATIC_DRAW);\n\n        gl.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);\n        gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);\n\n        // Enable the texture coordinate array for Texture 1\n        gl.glClientActiveTexture(GL11.GL_TEXTURE1);\n        gl.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);\n        gl.glClientActiveTexture(GL11.GL_TEXTURE0);\n        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);\n\n        // mMatrixValues and mAlpha will be initialized in setSize()\n    }\n\n    private static ByteBuffer allocateDirectNativeOrderBuffer(int size) {\n        return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());\n    }\n\n    // This function changes the source coordinate to the texture coordinates.\n    // It also clips the source and target coordinates if it is beyond the\n    // bound of the texture.\n    private static void convertCoordinate(RectF source, RectF target,\n                                          BasicTexture texture) {\n        int width = texture.getWidth();\n        int height = texture.getHeight();\n        int texWidth = texture.getTextureWidth();\n        int texHeight = texture.getTextureHeight();\n        // Convert to texture coordinates\n        source.left /= texWidth;\n        source.right /= texWidth;\n        source.top /= texHeight;\n        source.bottom /= texHeight;\n\n        // Clip if the rendering range is beyond the bound of the texture.\n        float xBound = (float) width / texWidth;\n        if (source.right > xBound) {\n            target.right = target.left + target.width() *\n                    (xBound - source.left) / source.width();\n            source.right = xBound;\n        }\n        float yBound = (float) height / texHeight;\n        if (source.bottom > yBound) {\n            target.bottom = target.top + target.height() *\n                    (yBound - source.top) / source.height();\n            source.bottom = yBound;\n        }\n    }\n\n    private static boolean isMatrixRotatedOrFlipped(float[] matrix) {\n        final float eps = 1e-5f;\n        return Math.abs(matrix[MSKEW_X]) > eps\n                || Math.abs(matrix[MSKEW_Y]) > eps\n                || matrix[MSCALE_X] < -eps\n                || matrix[MSCALE_Y] > eps;\n    }\n\n    private static void checkFramebufferStatus(GL11ExtensionPack gl11ep) {\n        int status = gl11ep.glCheckFramebufferStatusOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES);\n        if (status != GL11ExtensionPack.GL_FRAMEBUFFER_COMPLETE_OES) {\n            String msg = switch (status) {\n                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_OES ->\n                        \"FRAMEBUFFER_FORMATS\";\n                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_OES ->\n                        \"FRAMEBUFFER_ATTACHMENT\";\n                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_OES ->\n                        \"FRAMEBUFFER_MISSING_ATTACHMENT\";\n                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_OES ->\n                        \"FRAMEBUFFER_DRAW_BUFFER\";\n                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_OES ->\n                        \"FRAMEBUFFER_READ_BUFFER\";\n                case GL11ExtensionPack.GL_FRAMEBUFFER_UNSUPPORTED_OES -> \"FRAMEBUFFER_UNSUPPORTED\";\n                case GL11ExtensionPack.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_OES ->\n                        \"FRAMEBUFFER_INCOMPLETE_DIMENSIONS\";\n                default -> \"\";\n            };\n            throw new RuntimeException(msg + \":\" + Integer.toHexString(status));\n        }\n    }\n\n    @Override\n    public void setSize(int width, int height) {\n        if (mTargetTexture == null) {\n            mScreenWidth = width;\n            mScreenHeight = height;\n        }\n        mAlpha = 1.0f;\n\n        GL11 gl = mGL;\n        gl.glViewport(0, 0, width, height);\n        gl.glMatrixMode(GL11.GL_PROJECTION);\n        gl.glLoadIdentity();\n        GLU.gluOrtho2D(gl, 0, width, 0, height);\n\n        gl.glMatrixMode(GL11.GL_MODELVIEW);\n        gl.glLoadIdentity();\n\n        float[] matrix = mMatrixValues;\n        Matrix.setIdentityM(matrix, 0);\n        // to match the graphic coordinate system in android, we flip it vertically.\n        if (mTargetTexture == null) {\n            Matrix.translateM(matrix, 0, 0, height, 0);\n            Matrix.scaleM(matrix, 0, 1, -1, 1);\n        }\n    }\n\n    @Override\n    public float getAlpha() {\n        return mAlpha;\n    }\n\n    @Override\n    public void setAlpha(float alpha) {\n        mAlpha = alpha;\n    }\n\n    @Override\n    public void multiplyAlpha(float alpha) {\n        mAlpha *= alpha;\n    }\n\n    @Override\n    public void drawRect(float x, float y, float width, float height, GLPaint paint) {\n        GL11 gl = mGL;\n\n        mGLState.setColorMode(paint.getColor(), mAlpha);\n        mGLState.setLineWidth(paint.getLineWidth());\n\n        saveTransform();\n        translate(x, y);\n        scale(width, height, 1);\n\n        gl.glLoadMatrixf(mMatrixValues, 0);\n        gl.glDrawArrays(GL11.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX);\n\n        restoreTransform();\n        mCountDrawLine++;\n    }\n\n    @Override\n    public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {\n        GL11 gl = mGL;\n\n        mGLState.setColorMode(paint.getColor(), mAlpha);\n        mGLState.setLineWidth(paint.getLineWidth());\n\n        saveTransform();\n        translate(x1, y1);\n        scale(x2 - x1, y2 - y1, 1);\n\n        gl.glLoadMatrixf(mMatrixValues, 0);\n        gl.glDrawArrays(GL11.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX);\n\n        restoreTransform();\n        mCountDrawLine++;\n    }\n\n    @Override\n    public void drawOval(float cx, float cy, float radiusX, float radiusY, GLPaint paint) {\n        float halfLineWidth = paint.getLineWidth() / 2;\n        fillOval(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, paint.getColor());\n        fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor());\n    }\n\n    @Override\n    public void drawArc(float cx, float cy, float radiusX, float radiusY, float sweepAngle, GLPaint paint) {\n        float halfLineWidth = paint.getLineWidth() / 2;\n        fillSector(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, sweepAngle, paint.getColor());\n        fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor());\n    }\n\n    @Override\n    public void fillRect(float x, float y, float width, float height, int color) {\n        mGLState.setColorMode(color, mAlpha);\n        GL11 gl = mGL;\n\n        saveTransform();\n        translate(x, y);\n        scale(width, height, 1);\n\n        gl.glLoadMatrixf(mMatrixValues, 0);\n        gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX);\n\n        restoreTransform();\n        mCountFillRect++;\n    }\n\n    @Override\n    public void fillOval(float cx, float cy, float radiusX, float radiusY, int color) {\n        mGLState.setColorMode(color, mAlpha);\n        GL11 gl = mGL;\n\n        saveTransform();\n        translate(cx - radiusX, cy - radiusY);\n        scale(radiusX * 2, radiusY * 2, 1);\n\n        gl.glLoadMatrixf(mMatrixValues, 0);\n        gl.glDrawArrays(GL11.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE);\n\n        restoreTransform();\n        mCountFillRect++;\n    }\n\n    @Override\n    public void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color) {\n        float conjugateAngle = Math.abs(360 - MathUtils.positiveModulo(sweepAngle, 360));\n        int conjugateCount = Math.round(COUNT_CIRCLE_VERTEX * conjugateAngle / 360);\n        if (conjugateCount == 0) {\n            // It is a circle\n            fillOval(cx, cy, radiusX, radiusY, color);\n        } else if (conjugateCount < COUNT_CIRCLE_VERTEX) {\n            mGLState.setColorMode(color, mAlpha);\n            GL11 gl = mGL;\n\n            saveTransform();\n            translate(cx - radiusX, cy - radiusY);\n            scale(radiusX * 2, radiusY * 2, 1);\n\n            gl.glLoadMatrixf(mMatrixValues, 0);\n            gl.glDrawArrays(GL11.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE - conjugateCount);\n\n            restoreTransform();\n            mCountFillRect++;\n        }\n    }\n\n    @Override\n    public void translate(float x, float y, float z) {\n        Matrix.translateM(mMatrixValues, 0, x, y, z);\n    }\n\n    // This is a faster version of translate(x, y, z) because\n    // (1) we knows z = 0, (2) we inline the Matrix.translateM call,\n    // (3) we unroll the loop\n    @Override\n    public void translate(float x, float y) {\n        float[] m = mMatrixValues;\n        m[12] += m[0] * x + m[4] * y;\n        m[13] += m[1] * x + m[5] * y;\n        m[14] += m[2] * x + m[6] * y;\n        m[15] += m[3] * x + m[7] * y;\n    }\n\n    @Override\n    public void scale(float sx, float sy, float sz) {\n        Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);\n    }\n\n    @Override\n    public void rotate(float angle, float x, float y, float z) {\n        if (angle == 0) return;\n        float[] temp = mTempMatrix;\n        Matrix.setRotateM(temp, 0, angle, x, y, z);\n        Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);\n        System.arraycopy(temp, 16, mMatrixValues, 0, 16);\n    }\n\n    @Override\n    public void multiplyMatrix(float[] matrix, int offset) {\n        float[] temp = mTempMatrix;\n        Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);\n        System.arraycopy(temp, 0, mMatrixValues, 0, 16);\n    }\n\n    private void textureRect(float x, float y, float width, float height) {\n        GL11 gl = mGL;\n\n        saveTransform();\n        translate(x, y);\n        scale(width, height, 1);\n\n        gl.glLoadMatrixf(mMatrixValues, 0);\n        gl.glDrawArrays(GL11.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);\n\n        restoreTransform();\n        mCountTextureRect++;\n    }\n\n    @Override\n    public void drawMesh(BasicTexture tex, int x, int y, int xyBuffer,\n                         int uvBuffer, int indexBuffer, int indexCount) {\n        float alpha = mAlpha;\n        if (notBindTexture(tex)) return;\n\n        mGLState.setBlendEnabled(mBlendEnabled\n                && (!tex.isOpaque() || alpha < OPAQUE_ALPHA));\n        mGLState.setTextureAlpha(alpha);\n\n        // Reset the texture matrix. We will set our own texture coordinates\n        // below.\n        setTextureCoords(0, 0, 1, 1);\n\n        saveTransform();\n        translate(x, y);\n\n        mGL.glLoadMatrixf(mMatrixValues, 0);\n\n        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, xyBuffer);\n        mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);\n\n        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, uvBuffer);\n        mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);\n\n        mGL.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);\n        mGL.glDrawElements(GL11.GL_TRIANGLE_STRIP,\n                indexCount, GL11.GL_UNSIGNED_BYTE, 0);\n\n        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);\n        mGL.glVertexPointer(2, GL11.GL_FLOAT, 0, 0);\n        mGL.glTexCoordPointer(2, GL11.GL_FLOAT, 0, 0);\n\n        restoreTransform();\n        mCountDrawMesh++;\n    }\n\n    // Transforms two points by the given matrix m. The result\n    // {x1', y1', x2', y2'} are stored in mMapPointsBuffer and also returned.\n    private float[] mapPoints(float[] m, int x1, int y1, int x2, int y2) {\n        float[] r = mMapPointsBuffer;\n\n        // Multiply m and (x1 y1 0 1) to produce (x3 y3 z3 w3). z3 is unused.\n        float x3 = m[0] * x1 + m[4] * y1 + m[12];\n        float y3 = m[1] * x1 + m[5] * y1 + m[13];\n        float w3 = m[3] * x1 + m[7] * y1 + m[15];\n        r[0] = x3 / w3;\n        r[1] = y3 / w3;\n\n        // Same for x2 y2.\n        float x4 = m[0] * x2 + m[4] * y2 + m[12];\n        float y4 = m[1] * x2 + m[5] * y2 + m[13];\n        float w4 = m[3] * x2 + m[7] * y2 + m[15];\n        r[2] = x4 / w4;\n        r[3] = y4 / w4;\n\n        return r;\n    }\n\n    private void drawBoundTexture(\n            BasicTexture texture, int x, int y, int width, int height) {\n        // Test whether it has been rotated or flipped, if so, glDrawTexiOES\n        // won't work\n        if (isMatrixRotatedOrFlipped(mMatrixValues)) {\n            if (texture.hasBorder()) {\n                setTextureCoords(\n                        1.0f / texture.getTextureWidth(),\n                        1.0f / texture.getTextureHeight(),\n                        (texture.getWidth() - 1.0f) / texture.getTextureWidth(),\n                        (texture.getHeight() - 1.0f) / texture.getTextureHeight());\n            } else {\n                setTextureCoords(0, 0,\n                        (float) texture.getWidth() / texture.getTextureWidth(),\n                        (float) texture.getHeight() / texture.getTextureHeight());\n            }\n            textureRect(x, y, width, height);\n        } else {\n            // draw the rect from bottom-left to top-right\n            float[] points = mapPoints(\n                    mMatrixValues, x, y + height, x + width, y);\n            x = (int) (points[0] + 0.5f);\n            y = (int) (points[1] + 0.5f);\n            width = (int) (points[2] + 0.5f) - x;\n            height = (int) (points[3] + 0.5f) - y;\n            if (width > 0 && height > 0) {\n                ((GL11Ext) mGL).glDrawTexiOES(x, y, 0, width, height);\n                mCountTextureOES++;\n            }\n        }\n    }\n\n    @Override\n    public void drawTexture(\n            BasicTexture texture, int x, int y, int width, int height) {\n        drawTexture(texture, x, y, width, height, mAlpha);\n    }\n\n    private void drawTexture(BasicTexture texture,\n                             int x, int y, int width, int height, float alpha) {\n        if (width <= 0 || height <= 0) return;\n\n        mGLState.setBlendEnabled(mBlendEnabled\n                && (!texture.isOpaque() || alpha < OPAQUE_ALPHA));\n        if (notBindTexture(texture)) return;\n        mGLState.setTextureAlpha(alpha);\n        drawBoundTexture(texture, x, y, width, height);\n    }\n\n    @Override\n    public void drawTexture(BasicTexture texture, RectF source, RectF target) {\n        if (target.width() <= 0 || target.height() <= 0) return;\n\n        // Copy the input to avoid changing it.\n        mDrawTextureSourceRect.set(source);\n        mDrawTextureTargetRect.set(target);\n        source = mDrawTextureSourceRect;\n        target = mDrawTextureTargetRect;\n\n        mGLState.setBlendEnabled(mBlendEnabled\n                && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));\n        if (notBindTexture(texture)) return;\n        convertCoordinate(source, target, texture);\n        setTextureCoords(source);\n        mGLState.setTextureAlpha(mAlpha);\n        textureRect(target.left, target.top, target.width(), target.height());\n    }\n\n    @Override\n    public void drawTexture(BasicTexture texture, float[] mTextureTransform,\n                            int x, int y, int w, int h) {\n        mGLState.setBlendEnabled(mBlendEnabled\n                && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));\n        if (notBindTexture(texture)) return;\n        setTextureCoords(mTextureTransform);\n        mGLState.setTextureAlpha(mAlpha);\n        textureRect(x, y, w, h);\n    }\n\n    @Override\n    public void drawMixed(BasicTexture from,\n                          int toColor, float ratio, int x, int y, int w, int h) {\n        drawMixed(from, toColor, ratio, x, y, w, h, mAlpha);\n    }\n\n    private boolean notBindTexture(BasicTexture texture) {\n        if (!texture.onBind(this)) return true;\n        int target = texture.getTarget();\n        mGLState.setTextureTarget(target);\n        mGL.glBindTexture(target, texture.getId());\n        return false;\n    }\n\n    private void setTextureColor(float r, float g, float b, float alpha) {\n        float[] color = mTextureColor;\n        color[0] = r;\n        color[1] = g;\n        color[2] = b;\n        color[3] = alpha;\n    }\n\n    private void setMixedColor(int toColor, float ratio, float alpha) {\n        //\n        // The formula we want:\n        //     alpha * ((1 - ratio) * from + ratio * to)\n        //\n        // The formula that GL supports is in the form of:\n        //     combo * from + (1 - combo) * to * scale\n        //\n        // So, we have combo = alpha * (1 - ratio)\n        //     and     scale = alpha * ratio / (1 - combo)\n        //\n        float combo = alpha * (1 - ratio);\n        float scale = alpha * ratio / (1 - combo);\n\n        // Specify the interpolation factor via the alpha component of\n        // GL_TEXTURE_ENV_COLORs.\n        // RGB component are get from toColor and will used as SRC1\n        float colorScale = scale * (toColor >>> 24) / (0xff * 0xff);\n        setTextureColor(((toColor >>> 16) & 0xff) * colorScale,\n                ((toColor >>> 8) & 0xff) * colorScale,\n                (toColor & 0xff) * colorScale, combo);\n        GL11 gl = mGL;\n        gl.glTexEnvfv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, mTextureColor, 0);\n\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_RGB, GL11.GL_INTERPOLATE);\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_COMBINE_ALPHA, GL11.GL_INTERPOLATE);\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_RGB, GL11.GL_CONSTANT);\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_RGB, GL11.GL_SRC_COLOR);\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC1_ALPHA, GL11.GL_CONSTANT);\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND1_ALPHA, GL11.GL_SRC_ALPHA);\n\n        // Wire up the interpolation factor for RGB.\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_RGB, GL11.GL_CONSTANT);\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_RGB, GL11.GL_SRC_ALPHA);\n\n        // Wire up the interpolation factor for alpha.\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_SRC2_ALPHA, GL11.GL_CONSTANT);\n        gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_OPERAND2_ALPHA, GL11.GL_SRC_ALPHA);\n\n    }\n\n    @Override\n    public void drawMixed(BasicTexture from, int toColor, float ratio,\n                          RectF source, RectF target) {\n        if (target.width() <= 0 || target.height() <= 0) return;\n\n        if (ratio <= 0.01f) {\n            drawTexture(from, source, target);\n            return;\n        } else if (ratio >= 1) {\n            fillRect(target.left, target.top, target.width(), target.height(), toColor);\n            return;\n        }\n\n        float alpha = mAlpha;\n\n        // Copy the input to avoid changing it.\n        mDrawTextureSourceRect.set(source);\n        mDrawTextureTargetRect.set(target);\n        source = mDrawTextureSourceRect;\n        target = mDrawTextureTargetRect;\n\n        mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()\n                || Color.alpha(toColor) != 255 || alpha < OPAQUE_ALPHA));\n\n        if (notBindTexture(from)) return;\n\n        // Interpolate the RGB and alpha values between both textures.\n        mGLState.setTexEnvMode(GL11.GL_COMBINE);\n        setMixedColor(toColor, ratio, alpha);\n        convertCoordinate(source, target, from);\n        setTextureCoords(source);\n        textureRect(target.left, target.top, target.width(), target.height());\n        mGLState.setTexEnvMode(GL11.GL_REPLACE);\n    }\n\n    private void drawMixed(BasicTexture from, int toColor,\n                           float ratio, int x, int y, int width, int height, float alpha) {\n        // change from 0 to 0.01f to prevent getting divided by zero below\n        if (ratio <= 0.01f) {\n            drawTexture(from, x, y, width, height, alpha);\n            return;\n        } else if (ratio >= 1) {\n            fillRect(x, y, width, height, toColor);\n            return;\n        }\n\n        mGLState.setBlendEnabled(mBlendEnabled && (!from.isOpaque()\n                || Color.alpha(toColor) != 255 || alpha < OPAQUE_ALPHA));\n\n        if (notBindTexture(from)) return;\n\n        // Interpolate the RGB and alpha values between both textures.\n        mGLState.setTexEnvMode(GL11.GL_COMBINE);\n        setMixedColor(toColor, ratio, alpha);\n\n        drawBoundTexture(from, x, y, width, height);\n        mGLState.setTexEnvMode(GL11.GL_REPLACE);\n    }\n\n    @Override\n    public void clearBuffer(float[] argb) {\n        if (argb != null && argb.length == 4) {\n            mGL.glClearColor(argb[1], argb[2], argb[3], argb[0]);\n        } else {\n            mGL.glClearColor(0, 0, 0, 1);\n        }\n        mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);\n    }\n\n    @Override\n    public void clearBuffer() {\n        clearBuffer(null);\n    }\n\n    private void setTextureCoords(RectF source) {\n        setTextureCoords(source.left, source.top, source.right, source.bottom);\n    }\n\n    private void setTextureCoords(float left, float top,\n                                  float right, float bottom) {\n        mGL.glMatrixMode(GL11.GL_TEXTURE);\n        mTextureMatrixValues[0] = right - left;\n        mTextureMatrixValues[5] = bottom - top;\n        mTextureMatrixValues[10] = 1;\n        mTextureMatrixValues[12] = left;\n        mTextureMatrixValues[13] = top;\n        mTextureMatrixValues[15] = 1;\n        mGL.glLoadMatrixf(mTextureMatrixValues, 0);\n        mGL.glMatrixMode(GL11.GL_MODELVIEW);\n    }\n\n    private void setTextureCoords(float[] mTextureTransform) {\n        mGL.glMatrixMode(GL11.GL_TEXTURE);\n        mGL.glLoadMatrixf(mTextureTransform, 0);\n        mGL.glMatrixMode(GL11.GL_MODELVIEW);\n    }\n\n    // unloadTexture and deleteBuffer can be called from the finalizer thread,\n    // so we synchronized on the mUnboundTextures object.\n    @Override\n    public boolean unloadTexture(BasicTexture t) {\n        synchronized (mUnboundTextures) {\n            if (!t.isLoaded()) return false;\n            mUnboundTextures.add(t.mId);\n            return true;\n        }\n    }\n\n    @Override\n    public void deleteBuffer(int bufferId) {\n        synchronized (mUnboundTextures) {\n            mDeleteBuffers.add(bufferId);\n        }\n    }\n\n    @Override\n    public void deleteRecycledResources() {\n        synchronized (mUnboundTextures) {\n            IntList ids = mUnboundTextures;\n            if (!ids.isEmpty()) {\n                mGLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0);\n                ids.clear();\n            }\n\n            ids = mDeleteBuffers;\n            if (!ids.isEmpty()) {\n                mGLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0);\n                ids.clear();\n            }\n        }\n    }\n\n    @Override\n    public void save() {\n        save(SAVE_FLAG_ALL);\n    }\n\n    @Override\n    public void save(int saveFlags) {\n        ConfigState config = obtainRestoreConfig();\n\n        if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {\n            config.mAlpha = mAlpha;\n        } else {\n            config.mAlpha = -1;\n        }\n\n        if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {\n            System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);\n        } else {\n            config.mMatrix[0] = Float.NEGATIVE_INFINITY;\n        }\n\n        mRestoreStack.add(config);\n    }\n\n    @Override\n    public void restore() {\n        if (mRestoreStack.isEmpty()) throw new IllegalStateException();\n        //noinspection SequencedCollectionMethodCanBeUsed\n        ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1);\n        config.restore(this);\n        freeRestoreConfig(config);\n    }\n\n    private void freeRestoreConfig(ConfigState action) {\n        action.mNextFree = mRecycledRestoreAction;\n        mRecycledRestoreAction = action;\n    }\n\n    private ConfigState obtainRestoreConfig() {\n        if (mRecycledRestoreAction != null) {\n            ConfigState result = mRecycledRestoreAction;\n            mRecycledRestoreAction = result.mNextFree;\n            return result;\n        }\n        return new ConfigState();\n    }\n\n    @Override\n    public void dumpStatisticsAndClear() {\n        String line = String.format(\n                Locale.US,\n                \"MESH:%d, TEX_OES:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d\",\n                mCountDrawMesh, mCountTextureRect, mCountTextureOES,\n                mCountFillRect, mCountDrawLine);\n        mCountDrawMesh = 0;\n        mCountTextureRect = 0;\n        mCountTextureOES = 0;\n        mCountFillRect = 0;\n        mCountDrawLine = 0;\n        Log.d(TAG, line);\n    }\n\n    private void saveTransform() {\n        System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16);\n    }\n\n    private void restoreTransform() {\n        System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);\n    }\n\n    private void setRenderTarget(RawTexture texture) {\n        GL11ExtensionPack gl11ep = (GL11ExtensionPack) mGL;\n\n        if (mTargetTexture == null && texture != null) {\n            mGLId.glGenBuffers(1, mFrameBuffer, 0);\n            gl11ep.glBindFramebufferOES(\n                    GL11ExtensionPack.GL_FRAMEBUFFER_OES, mFrameBuffer[0]);\n        }\n        if (mTargetTexture != null && texture == null) {\n            gl11ep.glBindFramebufferOES(GL11ExtensionPack.GL_FRAMEBUFFER_OES, 0);\n            gl11ep.glDeleteFramebuffersOES(1, mFrameBuffer, 0);\n        }\n\n        mTargetTexture = texture;\n        if (texture == null) {\n            setSize(mScreenWidth, mScreenHeight);\n        } else {\n            setSize(texture.getWidth(), texture.getHeight());\n\n            if (!texture.isLoaded()) texture.prepare(this);\n\n            gl11ep.glFramebufferTexture2DOES(\n                    GL11ExtensionPack.GL_FRAMEBUFFER_OES,\n                    GL11ExtensionPack.GL_COLOR_ATTACHMENT0_OES,\n                    GL11.GL_TEXTURE_2D, texture.getId(), 0);\n\n            checkFramebufferStatus(gl11ep);\n        }\n    }\n\n    @Override\n    public void endRenderTarget() {\n        //noinspection SequencedCollectionMethodCanBeUsed\n        RawTexture texture = mTargetStack.remove(mTargetStack.size() - 1);\n        setRenderTarget(texture);\n        restore(); // restore matrix and alpha\n    }\n\n    @Override\n    public void beginRenderTarget(RawTexture texture) {\n        save(); // save matrix and alpha\n        mTargetStack.add(mTargetTexture);\n        setRenderTarget(texture);\n    }\n\n    @Override\n    public void setTextureParameters(BasicTexture texture) {\n        int width = texture.getWidth();\n        int height = texture.getHeight();\n        // Define a vertically flipped crop rectangle for OES_draw_texture.\n        // The four values in sCropRect are: left, bottom, width, and\n        // height. Negative value of width or height means flip.\n        sCropRect[0] = 0;\n        sCropRect[1] = height;\n        sCropRect[2] = width;\n        sCropRect[3] = -height;\n\n        // Set texture parameters.\n        int target = texture.getTarget();\n        mGL.glBindTexture(target, texture.getId());\n        mGL.glTexParameterfv(target, GL11Ext.GL_TEXTURE_CROP_RECT_OES, sCropRect, 0);\n        mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);\n        mGL.glTexParameteri(target, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);\n        mGL.glTexParameterf(target, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);\n        mGL.glTexParameterf(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);\n    }\n\n    @Override\n    public void initializeTextureSize(BasicTexture texture, int format, int type) {\n        int target = texture.getTarget();\n        mGL.glBindTexture(target, texture.getId());\n        int width = texture.getTextureWidth();\n        int height = texture.getTextureHeight();\n        mGL.glTexImage2D(target, 0, format, width, height, 0, format, type, null);\n    }\n\n    @Override\n    public void initializeTexture(BasicTexture texture, Bitmap bitmap) {\n        int target = texture.getTarget();\n        mGL.glBindTexture(target, texture.getId());\n        GLUtils.texImage2D(target, 0, bitmap, 0);\n    }\n\n    @Override\n    public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,\n                              int format, int type) {\n        int target = texture.getTarget();\n        mGL.glBindTexture(target, texture.getId());\n        GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);\n    }\n\n    @Override\n    public int uploadBuffer(FloatBuffer buf) {\n        return uploadBuffer(buf, Float.SIZE / Byte.SIZE);\n    }\n\n    @Override\n    public int uploadBuffer(ByteBuffer buf) {\n        return uploadBuffer(buf, 1);\n    }\n\n    private int uploadBuffer(Buffer buf, int elementSize) {\n        int[] bufferIds = new int[1];\n        mGLId.glGenBuffers(bufferIds.length, bufferIds, 0);\n        int bufferId = bufferIds[0];\n        mGL.glBindBuffer(GL11.GL_ARRAY_BUFFER, bufferId);\n        mGL.glBufferData(GL11.GL_ARRAY_BUFFER, buf.capacity() * elementSize, buf,\n                GL11.GL_STATIC_DRAW);\n        return bufferId;\n    }\n\n    @Override\n    public void recoverFromLightCycle() {\n        // This is only required for GLES20\n    }\n\n    @Override\n    public void getBounds(Rect bounds, int x, int y, int width, int height) {\n        // This is only required for GLES20\n    }\n\n    @Override\n    public GLId getGLId() {\n        return mGLId;\n    }\n\n    private static class GLState {\n        private final GL11 mGL;\n        private int mTexEnvMode = GL11.GL_REPLACE;\n        private float mTextureAlpha = 1.0f;\n        private int mTextureTarget = GL11.GL_TEXTURE_2D;\n        private boolean mBlendEnabled = true;\n        private float mLineWidth = 1.0f;\n\n        public GLState(GL11 gl) {\n            mGL = gl;\n\n            // Disable unused state\n            gl.glDisable(GL11.GL_LIGHTING);\n\n            // Enable used features\n            gl.glEnable(GL11.GL_DITHER);\n\n            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);\n            gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);\n            gl.glEnable(GL11.GL_TEXTURE_2D);\n\n            gl.glTexEnvf(GL11.GL_TEXTURE_ENV,\n                    GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);\n\n            // Set the background color\n            gl.glClearColor(0f, 0f, 0f, 0f);\n\n            gl.glEnable(GL11.GL_BLEND);\n            gl.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA);\n\n            // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel.\n            gl.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 2);\n        }\n\n        public void setTexEnvMode(int mode) {\n            if (mTexEnvMode == mode) return;\n            mTexEnvMode = mode;\n            mGL.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, mode);\n        }\n\n        public void setLineWidth(float width) {\n            if (mLineWidth == width) return;\n            mLineWidth = width;\n            mGL.glLineWidth(width);\n        }\n\n        public void setTextureAlpha(float alpha) {\n            if (mTextureAlpha == alpha) return;\n            mTextureAlpha = alpha;\n            if (alpha >= OPAQUE_ALPHA) {\n                // The alpha is need for those texture without alpha channel\n                mGL.glColor4f(1, 1, 1, 1);\n                setTexEnvMode(GL11.GL_REPLACE);\n            } else {\n                mGL.glColor4f(alpha, alpha, alpha, alpha);\n                setTexEnvMode(GL11.GL_MODULATE);\n            }\n        }\n\n        public void setColorMode(int color, float alpha) {\n            setBlendEnabled(Color.alpha(color) != 255 || alpha < OPAQUE_ALPHA);\n\n            // Set mTextureAlpha to an invalid value, so that it will reset\n            // again in setTextureAlpha(float) later.\n            mTextureAlpha = -1.0f;\n\n            setTextureTarget(0);\n\n            float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;\n            mGL.glColor4x(\n                    Math.round(((color >> 16) & 0xFF) * prealpha),\n                    Math.round(((color >> 8) & 0xFF) * prealpha),\n                    Math.round((color & 0xFF) * prealpha),\n                    Math.round(255 * prealpha));\n        }\n\n        // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled.\n        public void setTextureTarget(int target) {\n            if (mTextureTarget == target) return;\n            if (mTextureTarget != 0) {\n                mGL.glDisable(mTextureTarget);\n            }\n            mTextureTarget = target;\n            if (mTextureTarget != 0) {\n                mGL.glEnable(mTextureTarget);\n            }\n        }\n\n        public void setBlendEnabled(boolean enabled) {\n            if (mBlendEnabled == enabled) return;\n            mBlendEnabled = enabled;\n            if (enabled) {\n                mGL.glEnable(GL11.GL_BLEND);\n            } else {\n                mGL.glDisable(GL11.GL_BLEND);\n            }\n        }\n    }\n\n    private static class ConfigState {\n        float mAlpha;\n        float[] mMatrix = new float[16];\n        ConfigState mNextFree;\n\n        public void restore(GLES11Canvas canvas) {\n            if (mAlpha >= 0) canvas.setAlpha(mAlpha);\n            if (mMatrix[0] != Float.NEGATIVE_INFINITY) {\n                System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/GLES11IdImpl.java",
    "content": "/*\n * Copyright (C) 2012 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\npackage com.hippo.glview.glrenderer;\n\nimport javax.microedition.khronos.opengles.GL11;\nimport javax.microedition.khronos.opengles.GL11ExtensionPack;\n\n/**\n * Open GL ES 1.1 implementation for generating and destroying texture IDs and\n * buffer IDs\n */\npublic class GLES11IdImpl implements GLId {\n    // Mutex for sNextId\n    private final static Object sLock = new Object();\n    private static int sNextId = 1;\n\n    @Override\n    public int generateTexture() {\n        synchronized (sLock) {\n            return sNextId++;\n        }\n    }\n\n    @Override\n    public void glGenBuffers(int n, int[] buffers, int offset) {\n        synchronized (sLock) {\n            while (n-- > 0) {\n                buffers[offset + n] = sNextId++;\n            }\n        }\n    }\n\n    @Override\n    public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {\n        synchronized (sLock) {\n            gl.glDeleteTextures(n, textures, offset);\n        }\n    }\n\n    @Override\n    public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {\n        synchronized (sLock) {\n            gl.glDeleteBuffers(n, buffers, offset);\n        }\n    }\n\n    @Override\n    public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {\n        synchronized (sLock) {\n            gl11ep.glDeleteFramebuffersOES(n, buffers, offset);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/GLES20Canvas.java",
    "content": "/*\n * Copyright (C) 2012 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Color;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.opengl.GLES20;\nimport android.opengl.GLUtils;\nimport android.opengl.Matrix;\nimport android.util.Log;\n\nimport com.hippo.yorozuya.MathUtils;\nimport com.hippo.yorozuya.collect.IntList;\n\nimport java.nio.Buffer;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.nio.FloatBuffer;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Locale;\n\npublic class GLES20Canvas implements GLCanvas {\n    // ************** Constants **********************\n    private static final String TAG = GLES20Canvas.class.getSimpleName();\n    private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE;\n    private static final float OPAQUE_ALPHA = 0.95f;\n\n    private static final int COORDS_PER_VERTEX = 2;\n    private static final int VERTEX_STRIDE = COORDS_PER_VERTEX * FLOAT_SIZE;\n\n    private static final int COUNT_FILL_VERTEX = 4;\n    private static final int COUNT_LINE_VERTEX = 2;\n    private static final int COUNT_RECT_VERTEX = 4;\n    private static final int COUNT_CIRCLE_VERTEX = 120; // multiple of 4\n    private static final int OFFSET_FILL_RECT = 0;\n    private static final int OFFSET_DRAW_LINE = OFFSET_FILL_RECT + COUNT_FILL_VERTEX;\n    private static final int OFFSET_DRAW_RECT = OFFSET_DRAW_LINE + COUNT_LINE_VERTEX;\n    private static final int OFFSET_FILL_CIRCLE = OFFSET_DRAW_RECT + COUNT_RECT_VERTEX;\n    private static final int OFFSET_DRAW_CIRCLE = OFFSET_FILL_CIRCLE + 1;\n    private static final int OFFSET_LAST = OFFSET_DRAW_CIRCLE + COUNT_CIRCLE_VERTEX + 1;\n\n    private static final float[] BOX_COORDINATES = new float[OFFSET_LAST * 2];\n    private static final float[] BOUNDS_COORDINATES = {\n            0, 0, 0, 1,\n            1, 1, 0, 1,\n    };\n    private static final String POSITION_ATTRIBUTE = \"aPosition\";\n    private static final String COLOR_UNIFORM = \"uColor\";\n    private static final String MATRIX_UNIFORM = \"uMatrix\";\n    private static final String TEXTURE_MATRIX_UNIFORM = \"uTextureMatrix\";\n    private static final String TEXTURE_SAMPLER_UNIFORM = \"uTextureSampler\";\n    private static final String ALPHA_UNIFORM = \"uAlpha\";\n    private static final String TEXTURE_COORD_ATTRIBUTE = \"aTextureCoordinate\";\n    private static final String DRAW_VERTEX_SHADER = \"uniform mat4 \" + MATRIX_UNIFORM + \";\\n\"\n            + \"attribute vec2 \" + POSITION_ATTRIBUTE + \";\\n\"\n            + \"void main() {\\n\"\n            + \"  vec4 pos = vec4(\" + POSITION_ATTRIBUTE + \", 0.0, 1.0);\\n\"\n            + \"  gl_Position = \" + MATRIX_UNIFORM + \" * pos;\\n\"\n            + \"}\\n\";\n    private static final String DRAW_FRAGMENT_SHADER = \"precision mediump float;\\n\"\n            + \"uniform vec4 \" + COLOR_UNIFORM + \";\\n\"\n            + \"void main() {\\n\"\n            + \"  gl_FragColor = \" + COLOR_UNIFORM + \";\\n\"\n            + \"}\\n\";\n    private static final String TEXTURE_VERTEX_SHADER = \"uniform mat4 \" + MATRIX_UNIFORM + \";\\n\"\n            + \"uniform mat4 \" + TEXTURE_MATRIX_UNIFORM + \";\\n\"\n            + \"attribute vec2 \" + POSITION_ATTRIBUTE + \";\\n\"\n            + \"varying vec2 vTextureCoord;\\n\"\n            + \"void main() {\\n\"\n            + \"  vec4 pos = vec4(\" + POSITION_ATTRIBUTE + \", 0.0, 1.0);\\n\"\n            + \"  gl_Position = \" + MATRIX_UNIFORM + \" * pos;\\n\"\n            + \"  vTextureCoord = (\" + TEXTURE_MATRIX_UNIFORM + \" * pos).xy;\\n\"\n            + \"}\\n\";\n    private static final String MESH_VERTEX_SHADER = \"uniform mat4 \" + MATRIX_UNIFORM + \";\\n\"\n            + \"attribute vec2 \" + POSITION_ATTRIBUTE + \";\\n\"\n            + \"attribute vec2 \" + TEXTURE_COORD_ATTRIBUTE + \";\\n\"\n            + \"varying vec2 vTextureCoord;\\n\"\n            + \"void main() {\\n\"\n            + \"  vec4 pos = vec4(\" + POSITION_ATTRIBUTE + \", 0.0, 1.0);\\n\"\n            + \"  gl_Position = \" + MATRIX_UNIFORM + \" * pos;\\n\"\n            + \"  vTextureCoord = \" + TEXTURE_COORD_ATTRIBUTE + \";\\n\"\n            + \"}\\n\";\n    private static final String TEXTURE_FRAGMENT_SHADER = \"precision mediump float;\\n\"\n            + \"varying vec2 vTextureCoord;\\n\"\n            + \"uniform float \" + ALPHA_UNIFORM + \";\\n\"\n            + \"uniform sampler2D \" + TEXTURE_SAMPLER_UNIFORM + \";\\n\"\n            + \"void main() {\\n\"\n            + \"  gl_FragColor = texture2D(\" + TEXTURE_SAMPLER_UNIFORM + \", vTextureCoord);\\n\"\n            + \"  gl_FragColor *= \" + ALPHA_UNIFORM + \";\\n\"\n            + \"}\\n\";\n    private static final int INITIAL_RESTORE_STATE_SIZE = 8;\n    private static final int MATRIX_SIZE = 16;\n    // Handle indices -- common\n    private static final int INDEX_POSITION = 0;\n    private static final int INDEX_MATRIX = 1;\n    // Handle indices -- draw\n    private static final int INDEX_COLOR = 2;\n    // Handle indices -- texture\n    private static final int INDEX_TEXTURE_MATRIX = 2;\n    private static final int INDEX_TEXTURE_SAMPLER = 3;\n    private static final int INDEX_ALPHA = 4;\n    // Handle indices -- mesh\n    private static final int INDEX_TEXTURE_COORD = 2;\n    private static final GLId mGLId = new GLES20IdImpl();\n\n    static {\n        float[] temp = {\n                0, 0, // Fill rectangle\n                1, 0,\n                0, 1,\n                1, 1,\n                0, 0, // Draw line\n                1, 1,\n                0, 0, // Draw rectangle outline\n                0, 1,\n                1, 1,\n                1, 0,\n                0.5f, 0.5f // Fill circle\n        };\n        System.arraycopy(temp, 0, BOX_COORDINATES, 0, temp.length);\n\n        // Draw circle\n        int arrayOffset = OFFSET_DRAW_CIRCLE * 2;\n        for (int i = 0, n = COUNT_CIRCLE_VERTEX / 4; i <= n; i++) {\n            float value = (float) Math.sin(MathUtils.radians(90f / n * i)) / 2;\n            float positive = value + 0.5f;\n            float negative = -value + 0.5f;\n            BOX_COORDINATES[arrayOffset + i * 2 + 1] = positive;\n            BOX_COORDINATES[arrayOffset + n * 2 - i * 2] = positive;\n            BOX_COORDINATES[arrayOffset + n * 2 + i * 2] = negative;\n            BOX_COORDINATES[arrayOffset + n * 4 - i * 2 + 1] = positive;\n            BOX_COORDINATES[arrayOffset + n * 4 + i * 2 + 1] = negative;\n            BOX_COORDINATES[arrayOffset + n * 6 - i * 2] = negative;\n            BOX_COORDINATES[arrayOffset + n * 6 + i * 2] = positive;\n            BOX_COORDINATES[arrayOffset + n * 8 - i * 2 + 1] = negative;\n        }\n    }\n\n    private final IntList mSaveFlags = new IntList();\n    // Projection matrix\n    private final float[] mProjectionMatrix = new float[MATRIX_SIZE];\n    // GL programs\n    private final int mDrawProgram;\n    private final int mTextureProgram;\n    private final int mMeshProgram;\n\n    // GL buffer containing BOX_COORDINATES\n    private final int mBoxCoordinates;\n    private final IntList mUnboundTextures = new IntList();\n    private final IntList mDeleteBuffers = new IntList();\n    // Buffer for framebuffer IDs -- we keep track so we can switch the attached\n    // texture.\n    private final int[] mFrameBuffer = new int[1];\n    // Bound textures.\n    private final ArrayList<RawTexture> mTargetTextures = new ArrayList<>();\n    // Temporary variables used within calculations\n    private final float[] mTempMatrix = new float[32];\n    private final float[] mTempColor = new float[4];\n    private final RectF mTempSourceRect = new RectF();\n    private final RectF mTempTargetRect = new RectF();\n    private final float[] mTempTextureMatrix = new float[MATRIX_SIZE];\n    private final int[] mTempIntArray = new int[1];\n    ShaderParameter[] mDrawParameters = {\n            new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION\n            new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX\n            new UniformShaderParameter(COLOR_UNIFORM), // INDEX_COLOR\n    };\n    ShaderParameter[] mTextureParameters = {\n            new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION\n            new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX\n            new UniformShaderParameter(TEXTURE_MATRIX_UNIFORM), // INDEX_TEXTURE_MATRIX\n            new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER\n            new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA\n    };\n    ShaderParameter[] mMeshParameters = {\n            new AttributeShaderParameter(POSITION_ATTRIBUTE), // INDEX_POSITION\n            new UniformShaderParameter(MATRIX_UNIFORM), // INDEX_MATRIX\n            new AttributeShaderParameter(TEXTURE_COORD_ATTRIBUTE), // INDEX_TEXTURE_COORD\n            new UniformShaderParameter(TEXTURE_SAMPLER_UNIFORM), // INDEX_TEXTURE_SAMPLER\n            new UniformShaderParameter(ALPHA_UNIFORM), // INDEX_ALPHA\n    };\n    // Keep track of restore state\n    private float[] mMatrices = new float[INITIAL_RESTORE_STATE_SIZE * MATRIX_SIZE];\n    private float[] mAlphas = new float[INITIAL_RESTORE_STATE_SIZE];\n    private int mCurrentAlphaIndex = 0;\n    private int mCurrentMatrixIndex = 0;\n    // Viewport size\n    private int mWidth;\n    private int mHeight;\n    // Screen size for when we aren't bound to a texture\n    private int mScreenWidth;\n    private int mScreenHeight;\n    // Keep track of statistics for debugging\n    private int mCountDrawMesh = 0;\n    private int mCountTextureRect = 0;\n    private int mCountFillRect = 0;\n    private int mCountDrawLine = 0;\n\n    public GLES20Canvas() {\n        Matrix.setIdentityM(mTempTextureMatrix, 0);\n        Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);\n        mAlphas[mCurrentAlphaIndex] = 1f;\n        mTargetTextures.add(null);\n\n        FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES);\n        mBoxCoordinates = uploadBuffer(boxBuffer);\n\n        int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER);\n        int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER);\n        int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER);\n        int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER);\n        int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER);\n\n        mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters);\n        mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader,\n                mTextureParameters);\n        mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters);\n\n        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);\n        checkError();\n    }\n\n    private static FloatBuffer createBuffer(float[] values) {\n        // First create an nio buffer, then create a VBO from it.\n        int size = values.length * FLOAT_SIZE;\n        FloatBuffer buffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())\n                .asFloatBuffer();\n        buffer.put(values, 0, values.length).position(0);\n        return buffer;\n    }\n\n    private static int loadShader(int type, String shaderCode) {\n        // create a vertex shader type (GLES20.GL_VERTEX_SHADER)\n        // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)\n        int shader = GLES20.glCreateShader(type);\n\n        // add the source code to the shader and compile it\n        GLES20.glShaderSource(shader, shaderCode);\n        checkError();\n        GLES20.glCompileShader(shader);\n        checkError();\n\n        return shader;\n    }\n\n    private static void copyTextureCoordinates(BasicTexture texture, RectF outRect) {\n        int left = 0;\n        int top = 0;\n        int right = texture.getWidth();\n        int bottom = texture.getHeight();\n        if (texture.hasBorder()) {\n            left = 1;\n            top = 1;\n            right -= 1;\n            bottom -= 1;\n        }\n        outRect.set(left, top, right, bottom);\n    }\n\n    // This function changes the source coordinate to the texture coordinates.\n    // It also clips the source and target coordinates if it is beyond the\n    // bound of the texture.\n    private static void convertCoordinate(RectF source, RectF target, BasicTexture texture) {\n        int width = texture.getWidth();\n        int height = texture.getHeight();\n        int texWidth = texture.getTextureWidth();\n        int texHeight = texture.getTextureHeight();\n        // Convert to texture coordinates\n        source.left /= texWidth;\n        source.right /= texWidth;\n        source.top /= texHeight;\n        source.bottom /= texHeight;\n\n        // Clip if the rendering range is beyond the bound of the texture.\n        float xBound = (float) width / texWidth;\n        if (source.right > xBound) {\n            target.right = target.left + target.width() * (xBound - source.left) / source.width();\n            source.right = xBound;\n        }\n        float yBound = (float) height / texHeight;\n        if (source.bottom > yBound) {\n            target.bottom = target.top + target.height() * (yBound - source.top) / source.height();\n            source.bottom = yBound;\n        }\n    }\n\n    private static void checkFramebufferStatus() {\n        int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);\n        if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {\n            String msg = switch (status) {\n                case GLES20.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT ->\n                        \"GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT\";\n                case GLES20.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS ->\n                        \"GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS\";\n                case GLES20.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT ->\n                        \"GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT\";\n                case GLES20.GL_FRAMEBUFFER_UNSUPPORTED -> \"GL_FRAMEBUFFER_UNSUPPORTED\";\n                default -> \"\";\n            };\n            throw new RuntimeException(msg + \":\" + Integer.toHexString(status));\n        }\n    }\n\n    public static void checkError() {\n        int error = GLES20.glGetError();\n        if (error != 0) {\n            Throwable t = new Throwable();\n            Log.e(TAG, \"GL error: \" + error, t);\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    private static void printMatrix(String message, float[] m, int offset) {\n        StringBuilder b = new StringBuilder(message);\n        for (int i = 0; i < MATRIX_SIZE; i++) {\n            b.append(' ');\n            if (i % 4 == 0) {\n                b.append('\\n');\n            }\n            b.append(m[offset + i]);\n        }\n        Log.v(TAG, b.toString());\n    }\n\n    private int assembleProgram(int vertexShader, int fragmentShader, ShaderParameter[] params) {\n        int program = GLES20.glCreateProgram();\n        checkError();\n        if (program == 0) {\n            throw new RuntimeException(\"Cannot create GL program: \" + GLES20.glGetError());\n        }\n        GLES20.glAttachShader(program, vertexShader);\n        checkError();\n        GLES20.glAttachShader(program, fragmentShader);\n        checkError();\n        GLES20.glLinkProgram(program);\n        checkError();\n        int[] mLinkStatus = mTempIntArray;\n        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, mLinkStatus, 0);\n        if (mLinkStatus[0] != GLES20.GL_TRUE) {\n            Log.e(TAG, \"Could not link program: \");\n            Log.e(TAG, GLES20.glGetProgramInfoLog(program));\n            GLES20.glDeleteProgram(program);\n            program = 0;\n        }\n        for (ShaderParameter param : params) {\n            param.loadHandle(program);\n        }\n        return program;\n    }\n\n    @Override\n    public void setSize(int width, int height) {\n        mWidth = width;\n        mHeight = height;\n        GLES20.glViewport(0, 0, mWidth, mHeight);\n        checkError();\n        Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex);\n        Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1);\n        if (getTargetTexture() == null) {\n            mScreenWidth = width;\n            mScreenHeight = height;\n            Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0);\n            Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1);\n        }\n    }\n\n    @Override\n    public void clearBuffer() {\n        GLES20.glClearColor(0f, 0f, 0f, 1f);\n        checkError();\n        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);\n        checkError();\n    }\n\n    @Override\n    public void clearBuffer(float[] argb) {\n        GLES20.glClearColor(argb[1], argb[2], argb[3], argb[0]);\n        checkError();\n        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);\n        checkError();\n    }\n\n    @Override\n    public float getAlpha() {\n        return mAlphas[mCurrentAlphaIndex];\n    }\n\n    @Override\n    public void setAlpha(float alpha) {\n        mAlphas[mCurrentAlphaIndex] = alpha;\n    }\n\n    @Override\n    public void multiplyAlpha(float alpha) {\n        setAlpha(getAlpha() * alpha);\n    }\n\n    @Override\n    public void translate(float x, float y, float z) {\n        Matrix.translateM(mMatrices, mCurrentMatrixIndex, x, y, z);\n    }\n\n    // This is a faster version of translate(x, y, z) because\n    // (1) we knows z = 0, (2) we inline the Matrix.translateM call,\n    // (3) we unroll the loop\n    @Override\n    public void translate(float x, float y) {\n        int index = mCurrentMatrixIndex;\n        float[] m = mMatrices;\n        m[index + 12] += m[index] * x + m[index + 4] * y;\n        m[index + 13] += m[index + 1] * x + m[index + 5] * y;\n        m[index + 14] += m[index + 2] * x + m[index + 6] * y;\n        m[index + 15] += m[index + 3] * x + m[index + 7] * y;\n    }\n\n    @Override\n    public void scale(float sx, float sy, float sz) {\n        Matrix.scaleM(mMatrices, mCurrentMatrixIndex, sx, sy, sz);\n    }\n\n    @Override\n    public void rotate(float angle, float x, float y, float z) {\n        if (angle == 0f) {\n            return;\n        }\n        float[] temp = mTempMatrix;\n        Matrix.setRotateM(temp, 0, angle, x, y, z);\n        float[] matrix = mMatrices;\n        int index = mCurrentMatrixIndex;\n        Matrix.multiplyMM(temp, MATRIX_SIZE, matrix, index, temp, 0);\n        System.arraycopy(temp, MATRIX_SIZE, matrix, index, MATRIX_SIZE);\n    }\n\n    @Override\n    public void multiplyMatrix(float[] matrix, int offset) {\n        float[] temp = mTempMatrix;\n        float[] currentMatrix = mMatrices;\n        int index = mCurrentMatrixIndex;\n        Matrix.multiplyMM(temp, 0, currentMatrix, index, matrix, offset);\n        System.arraycopy(temp, 0, currentMatrix, index, 16);\n    }\n\n    @Override\n    public void save() {\n        save(SAVE_FLAG_ALL);\n    }\n\n    @Override\n    public void save(int saveFlags) {\n        boolean saveAlpha = (saveFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;\n        if (saveAlpha) {\n            float currentAlpha = getAlpha();\n            mCurrentAlphaIndex++;\n            if (mAlphas.length <= mCurrentAlphaIndex) {\n                mAlphas = Arrays.copyOf(mAlphas, mAlphas.length * 2);\n            }\n            mAlphas[mCurrentAlphaIndex] = currentAlpha;\n        }\n        boolean saveMatrix = (saveFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;\n        if (saveMatrix) {\n            int currentIndex = mCurrentMatrixIndex;\n            mCurrentMatrixIndex += MATRIX_SIZE;\n            if (mMatrices.length <= mCurrentMatrixIndex) {\n                mMatrices = Arrays.copyOf(mMatrices, mMatrices.length * 2);\n            }\n            System.arraycopy(mMatrices, currentIndex, mMatrices, mCurrentMatrixIndex, MATRIX_SIZE);\n        }\n        mSaveFlags.add(saveFlags);\n    }\n\n    @Override\n    public void restore() {\n        int restoreFlags = mSaveFlags.removeAt(mSaveFlags.size() - 1);\n        boolean restoreAlpha = (restoreFlags & SAVE_FLAG_ALPHA) == SAVE_FLAG_ALPHA;\n        if (restoreAlpha) {\n            mCurrentAlphaIndex--;\n        }\n        boolean restoreMatrix = (restoreFlags & SAVE_FLAG_MATRIX) == SAVE_FLAG_MATRIX;\n        if (restoreMatrix) {\n            mCurrentMatrixIndex -= MATRIX_SIZE;\n        }\n    }\n\n    @Override\n    public void drawLine(float x1, float y1, float x2, float y2, GLPaint paint) {\n        draw(GLES20.GL_LINE_STRIP, OFFSET_DRAW_LINE, COUNT_LINE_VERTEX, x1, y1, x2 - x1, y2 - y1,\n                paint);\n        mCountDrawLine++;\n    }\n\n    @Override\n    public void drawRect(float x, float y, float width, float height, GLPaint paint) {\n        draw(GLES20.GL_LINE_LOOP, OFFSET_DRAW_RECT, COUNT_RECT_VERTEX, x, y, width, height, paint);\n        mCountDrawLine++;\n    }\n\n    @Override\n    public void drawOval(float cx, float cy, float radiusX, float radiusY, GLPaint paint) {\n        float halfLineWidth = paint.getLineWidth() / 2;\n        fillOval(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, paint.getColor());\n        fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor());\n    }\n\n    @Override\n    public void drawArc(float cx, float cy, float radiusX, float radiusY,\n                        float sweepAngle, GLPaint paint) {\n        float halfLineWidth = paint.getLineWidth() / 2;\n        fillSector(cx, cy, radiusX + halfLineWidth, radiusY + halfLineWidth, sweepAngle, paint.getColor());\n        fillOval(cx, cy, radiusX - halfLineWidth, radiusY - halfLineWidth, paint.getBackgroundColor());\n    }\n\n    private void draw(int type, int offset, int count, float x, float y, float width, float height,\n                      GLPaint paint) {\n        draw(type, offset, count, x, y, width, height, paint.getColor(), paint.getLineWidth());\n    }\n\n    private void draw(int type, int offset, int count, float x, float y, float width, float height,\n                      int color, float lineWidth) {\n        prepareDraw(offset, color, lineWidth);\n        draw(mDrawParameters, type, count, x, y, width, height);\n    }\n\n    private void prepareDraw(int offset, int color, float lineWidth) {\n        GLES20.glUseProgram(mDrawProgram);\n        checkError();\n        if (lineWidth > 0) {\n            GLES20.glLineWidth(lineWidth);\n            checkError();\n        }\n        float[] colorArray = getColor(color);\n        boolean blendingEnabled = (colorArray[3] < 1f);\n        enableBlending(blendingEnabled);\n        if (blendingEnabled) {\n            GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]);\n            checkError();\n        }\n\n        GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0);\n        setPosition(mDrawParameters, offset);\n        checkError();\n    }\n\n    private float[] getColor(int color) {\n        float alpha = ((color >>> 24) & 0xFF) / 255f * getAlpha();\n        float red = ((color >>> 16) & 0xFF) / 255f * alpha;\n        float green = ((color >>> 8) & 0xFF) / 255f * alpha;\n        float blue = (color & 0xFF) / 255f * alpha;\n        mTempColor[0] = red;\n        mTempColor[1] = green;\n        mTempColor[2] = blue;\n        mTempColor[3] = alpha;\n        return mTempColor;\n    }\n\n    private void enableBlending(boolean enableBlending) {\n        if (enableBlending) {\n            GLES20.glEnable(GLES20.GL_BLEND);\n            checkError();\n        } else {\n            GLES20.glDisable(GLES20.GL_BLEND);\n            checkError();\n        }\n    }\n\n    private void setPosition(ShaderParameter[] params, int offset) {\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mBoxCoordinates);\n        checkError();\n        GLES20.glVertexAttribPointer(params[INDEX_POSITION].handle, COORDS_PER_VERTEX,\n                GLES20.GL_FLOAT, false, VERTEX_STRIDE, offset * VERTEX_STRIDE);\n        checkError();\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        checkError();\n    }\n\n    private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width,\n                      float height) {\n        setMatrix(params, x, y, width, height);\n        int positionHandle = params[INDEX_POSITION].handle;\n        GLES20.glEnableVertexAttribArray(positionHandle);\n        checkError();\n        GLES20.glDrawArrays(type, 0, count);\n        checkError();\n        GLES20.glDisableVertexAttribArray(positionHandle);\n        checkError();\n    }\n\n    private void setMatrix(ShaderParameter[] params, float x, float y, float width, float height) {\n        Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f);\n        Matrix.scaleM(mTempMatrix, 0, width, height, 1f);\n        Matrix.multiplyMM(mTempMatrix, MATRIX_SIZE, mProjectionMatrix, 0, mTempMatrix, 0);\n        GLES20.glUniformMatrix4fv(params[INDEX_MATRIX].handle, 1, false, mTempMatrix, MATRIX_SIZE);\n        checkError();\n    }\n\n    @Override\n    public void fillRect(float x, float y, float width, float height, int color) {\n        draw(GLES20.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, COUNT_FILL_VERTEX, x, y, width, height,\n                color, 0f);\n        mCountFillRect++;\n    }\n\n    @Override\n    public void fillOval(float cx, float cy, float radiusX, float radiusY, int color) {\n        draw(GLES20.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE,\n                cx - radiusX, cy - radiusY, radiusX * 2, radiusY * 2, color, 0f);\n        mCountFillRect++;\n    }\n\n    @Override\n    public void fillSector(float cx, float cy, float radiusX, float radiusY, float sweepAngle, int color) {\n        float conjugateAngle = Math.abs(360 - MathUtils.positiveModulo(sweepAngle, 360));\n        int conjugateCount = Math.round(COUNT_CIRCLE_VERTEX * conjugateAngle / 360);\n        if (conjugateCount == 0) {\n            // It is a circle\n            fillOval(cx, cy, radiusX, radiusY, color);\n        } else if (conjugateCount < COUNT_CIRCLE_VERTEX) {\n            draw(GLES20.GL_TRIANGLE_FAN, OFFSET_FILL_CIRCLE, OFFSET_LAST - OFFSET_FILL_CIRCLE - conjugateCount,\n                    cx - radiusX, cy - radiusY, radiusX * 2, radiusY * 2, color, 0f);\n            mCountFillRect++;\n        }\n    }\n\n    @Override\n    public void drawTexture(BasicTexture texture, int x, int y, int width, int height) {\n        if (width <= 0 || height <= 0) {\n            return;\n        }\n        copyTextureCoordinates(texture, mTempSourceRect);\n        mTempTargetRect.set(x, y, x + width, y + height);\n        convertCoordinate(mTempSourceRect, mTempTargetRect, texture);\n        drawTextureRect(texture, mTempSourceRect, mTempTargetRect);\n    }\n\n    @Override\n    public void drawTexture(BasicTexture texture, RectF source, RectF target) {\n        if (target.width() <= 0 || target.height() <= 0) {\n            return;\n        }\n        mTempSourceRect.set(source);\n        mTempTargetRect.set(target);\n\n        convertCoordinate(mTempSourceRect, mTempTargetRect, texture);\n        drawTextureRect(texture, mTempSourceRect, mTempTargetRect);\n    }\n\n    @Override\n    public void drawTexture(BasicTexture texture, float[] textureTransform, int x, int y, int w,\n                            int h) {\n        if (w <= 0 || h <= 0) {\n            return;\n        }\n        mTempTargetRect.set(x, y, x + w, y + h);\n        drawTextureRect(texture, textureTransform, mTempTargetRect);\n    }\n\n    private void drawTextureRect(BasicTexture texture, RectF source, RectF target) {\n        setTextureMatrix(source);\n        drawTextureRect(texture, mTempTextureMatrix, target);\n    }\n\n    private void setTextureMatrix(RectF source) {\n        mTempTextureMatrix[0] = source.width();\n        mTempTextureMatrix[5] = source.height();\n        mTempTextureMatrix[12] = source.left;\n        mTempTextureMatrix[13] = source.top;\n    }\n\n    private void drawTextureRect(BasicTexture texture, float[] textureMatrix, RectF target) {\n        ShaderParameter[] params = mTextureParameters;\n        prepareTexture(texture, mTextureProgram, params);\n        setPosition(params, OFFSET_FILL_RECT);\n        GLES20.glUniformMatrix4fv(params[INDEX_TEXTURE_MATRIX].handle, 1, false, textureMatrix, 0);\n        checkError();\n        if (texture.isFlippedVertically()) {\n            save(SAVE_FLAG_MATRIX);\n            translate(0, target.centerY());\n            scale(1, -1, 1);\n            translate(0, -target.centerY());\n        }\n        draw(params, GLES20.GL_TRIANGLE_STRIP, COUNT_FILL_VERTEX, target.left, target.top,\n                target.width(), target.height());\n        if (texture.isFlippedVertically()) {\n            restore();\n        }\n        mCountTextureRect++;\n    }\n\n    private void prepareTexture(BasicTexture texture, int program, ShaderParameter[] params) {\n        GLES20.glUseProgram(program);\n        checkError();\n        enableBlending(!texture.isOpaque() || getAlpha() < OPAQUE_ALPHA);\n        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);\n        checkError();\n        texture.onBind(this);\n        GLES20.glBindTexture(texture.getTarget(), texture.getId());\n        checkError();\n        GLES20.glUniform1i(params[INDEX_TEXTURE_SAMPLER].handle, 0);\n        checkError();\n        GLES20.glUniform1f(params[INDEX_ALPHA].handle, getAlpha());\n        checkError();\n    }\n\n    @Override\n    public void drawMesh(BasicTexture texture, int x, int y, int xyBuffer, int uvBuffer,\n                         int indexBuffer, int indexCount) {\n        prepareTexture(texture, mMeshProgram, mMeshParameters);\n\n        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);\n        checkError();\n\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, xyBuffer);\n        checkError();\n        int positionHandle = mMeshParameters[INDEX_POSITION].handle;\n        GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false,\n                VERTEX_STRIDE, 0);\n        checkError();\n\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, uvBuffer);\n        checkError();\n        int texCoordHandle = mMeshParameters[INDEX_TEXTURE_COORD].handle;\n        GLES20.glVertexAttribPointer(texCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT,\n                false, VERTEX_STRIDE, 0);\n        checkError();\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);\n        checkError();\n\n        GLES20.glEnableVertexAttribArray(positionHandle);\n        checkError();\n        GLES20.glEnableVertexAttribArray(texCoordHandle);\n        checkError();\n\n        setMatrix(mMeshParameters, x, y, 1, 1);\n        GLES20.glDrawElements(GLES20.GL_TRIANGLE_STRIP, indexCount, GLES20.GL_UNSIGNED_BYTE, 0);\n        checkError();\n\n        GLES20.glDisableVertexAttribArray(positionHandle);\n        checkError();\n        GLES20.glDisableVertexAttribArray(texCoordHandle);\n        checkError();\n        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);\n        checkError();\n        mCountDrawMesh++;\n    }\n\n    @Override\n    public void drawMixed(BasicTexture texture, int toColor, float ratio, int x, int y, int w, int h) {\n        copyTextureCoordinates(texture, mTempSourceRect);\n        mTempTargetRect.set(x, y, x + w, y + h);\n        drawMixed(texture, toColor, ratio, mTempSourceRect, mTempTargetRect);\n    }\n\n    @Override\n    public void drawMixed(BasicTexture texture, int toColor, float ratio, RectF source, RectF target) {\n        if (target.width() <= 0 || target.height() <= 0) {\n            return;\n        }\n        save(SAVE_FLAG_ALPHA);\n\n        float currentAlpha = getAlpha();\n        float cappedRatio = Math.min(1f, Math.max(0f, ratio));\n\n        float textureAlpha = (1f - cappedRatio) * currentAlpha;\n        setAlpha(textureAlpha);\n        drawTexture(texture, source, target);\n\n        if (0 != Color.alpha(toColor)) {\n            float colorAlpha = cappedRatio * currentAlpha;\n            setAlpha(colorAlpha);\n            fillRect(target.left, target.top, target.width(), target.height(), toColor);\n        }\n\n        restore();\n    }\n\n    @Override\n    public boolean unloadTexture(BasicTexture texture) {\n        boolean unload = texture.isLoaded();\n        if (unload) {\n            synchronized (mUnboundTextures) {\n                mUnboundTextures.add(texture.getId());\n            }\n        }\n        return unload;\n    }\n\n    @Override\n    public void deleteBuffer(int bufferId) {\n        synchronized (mUnboundTextures) {\n            mDeleteBuffers.add(bufferId);\n        }\n    }\n\n    @Override\n    public void deleteRecycledResources() {\n        synchronized (mUnboundTextures) {\n            IntList ids = mUnboundTextures;\n            if (!mUnboundTextures.isEmpty()) {\n                mGLId.glDeleteTextures(null, ids.size(), ids.getInternalArray(), 0);\n                ids.clear();\n            }\n\n            ids = mDeleteBuffers;\n            if (!ids.isEmpty()) {\n                mGLId.glDeleteBuffers(null, ids.size(), ids.getInternalArray(), 0);\n                ids.clear();\n            }\n        }\n    }\n\n    @Override\n    public void dumpStatisticsAndClear() {\n        String line = String.format(Locale.US, \"MESH:%d, TEX_RECT:%d, FILL_RECT:%d, LINE:%d\", mCountDrawMesh,\n                mCountTextureRect, mCountFillRect, mCountDrawLine);\n        mCountDrawMesh = 0;\n        mCountTextureRect = 0;\n        mCountFillRect = 0;\n        mCountDrawLine = 0;\n        Log.d(TAG, line);\n    }\n\n    @Override\n    public void endRenderTarget() {\n        //noinspection SequencedCollectionMethodCanBeUsed\n        RawTexture oldTexture = mTargetTextures.remove(mTargetTextures.size() - 1);\n        RawTexture texture = getTargetTexture();\n        setRenderTarget(oldTexture, texture);\n        restore(); // restore matrix and alpha\n    }\n\n    @Override\n    public void beginRenderTarget(RawTexture texture) {\n        save(); // save matrix and alpha and blending\n        RawTexture oldTexture = getTargetTexture();\n        mTargetTextures.add(texture);\n        setRenderTarget(oldTexture, texture);\n    }\n\n    private RawTexture getTargetTexture() {\n        //noinspection SequencedCollectionMethodCanBeUsed\n        return mTargetTextures.get(mTargetTextures.size() - 1);\n    }\n\n    private void setRenderTarget(BasicTexture oldTexture, RawTexture texture) {\n        if (oldTexture == null && texture != null) {\n            GLES20.glGenFramebuffers(1, mFrameBuffer, 0);\n            checkError();\n            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer[0]);\n            checkError();\n        } else if (oldTexture != null && texture == null) {\n            GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);\n            checkError();\n            GLES20.glDeleteFramebuffers(1, mFrameBuffer, 0);\n            checkError();\n        }\n\n        if (texture == null) {\n            setSize(mScreenWidth, mScreenHeight);\n        } else {\n            setSize(texture.getWidth(), texture.getHeight());\n\n            if (!texture.isLoaded()) {\n                texture.prepare(this);\n            }\n\n            GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,\n                    texture.getTarget(), texture.getId(), 0);\n            checkError();\n\n            checkFramebufferStatus();\n        }\n    }\n\n    @Override\n    public void setTextureParameters(BasicTexture texture) {\n        int target = texture.getTarget();\n        GLES20.glBindTexture(target, texture.getId());\n        checkError();\n        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);\n        GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);\n        GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);\n        GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);\n    }\n\n    @Override\n    public void initializeTextureSize(BasicTexture texture, int format, int type) {\n        int target = texture.getTarget();\n        GLES20.glBindTexture(target, texture.getId());\n        checkError();\n        int width = texture.getTextureWidth();\n        int height = texture.getTextureHeight();\n        GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null);\n    }\n\n    @Override\n    public void initializeTexture(BasicTexture texture, Bitmap bitmap) {\n        int target = texture.getTarget();\n        GLES20.glBindTexture(target, texture.getId());\n        checkError();\n        GLUtils.texImage2D(target, 0, bitmap, 0);\n    }\n\n    @Override\n    public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap,\n                              int format, int type) {\n        int target = texture.getTarget();\n        GLES20.glBindTexture(target, texture.getId());\n        checkError();\n        GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type);\n    }\n\n    @Override\n    public int uploadBuffer(FloatBuffer buf) {\n        return uploadBuffer(buf, FLOAT_SIZE);\n    }\n\n    @Override\n    public int uploadBuffer(ByteBuffer buf) {\n        return uploadBuffer(buf, 1);\n    }\n\n    private int uploadBuffer(Buffer buffer, int elementSize) {\n        mGLId.glGenBuffers(1, mTempIntArray, 0);\n        checkError();\n        int bufferId = mTempIntArray[0];\n        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, bufferId);\n        checkError();\n        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, buffer.capacity() * elementSize, buffer,\n                GLES20.GL_STATIC_DRAW);\n        checkError();\n        return bufferId;\n    }\n\n    @Override\n    public void recoverFromLightCycle() {\n        GLES20.glViewport(0, 0, mWidth, mHeight);\n        GLES20.glDisable(GLES20.GL_DEPTH_TEST);\n        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);\n        checkError();\n    }\n\n    @Override\n    public void getBounds(Rect bounds, int x, int y, int width, int height) {\n        Matrix.translateM(mTempMatrix, 0, mMatrices, mCurrentMatrixIndex, x, y, 0f);\n        Matrix.scaleM(mTempMatrix, 0, width, height, 1f);\n        Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE, mTempMatrix, 0, BOUNDS_COORDINATES, 0);\n        Matrix.multiplyMV(mTempMatrix, MATRIX_SIZE + 4, mTempMatrix, 0, BOUNDS_COORDINATES, 4);\n        bounds.left = Math.round(mTempMatrix[MATRIX_SIZE]);\n        bounds.right = Math.round(mTempMatrix[MATRIX_SIZE + 4]);\n        bounds.top = Math.round(mTempMatrix[MATRIX_SIZE + 1]);\n        bounds.bottom = Math.round(mTempMatrix[MATRIX_SIZE + 5]);\n        bounds.sort();\n    }\n\n    @Override\n    public GLId getGLId() {\n        return mGLId;\n    }\n\n    private abstract static class ShaderParameter {\n        protected final String mName;\n        public int handle;\n\n        public ShaderParameter(String name) {\n            mName = name;\n        }\n\n        public abstract void loadHandle(int program);\n    }\n\n    private static class UniformShaderParameter extends ShaderParameter {\n        public UniformShaderParameter(String name) {\n            super(name);\n        }\n\n        @Override\n        public void loadHandle(int program) {\n            handle = GLES20.glGetUniformLocation(program, mName);\n            checkError();\n        }\n    }\n\n    private static class AttributeShaderParameter extends ShaderParameter {\n        public AttributeShaderParameter(String name) {\n            super(name);\n        }\n\n        @Override\n        public void loadHandle(int program) {\n            handle = GLES20.glGetAttribLocation(program, mName);\n            checkError();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/GLES20IdImpl.java",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n\npackage com.hippo.glview.glrenderer;\n\nimport android.opengl.GLES20;\n\nimport javax.microedition.khronos.opengles.GL11;\nimport javax.microedition.khronos.opengles.GL11ExtensionPack;\n\npublic class GLES20IdImpl implements GLId {\n    private final int[] mTempIntArray = new int[1];\n\n    @Override\n    public int generateTexture() {\n        GLES20.glGenTextures(1, mTempIntArray, 0);\n        GLES20Canvas.checkError();\n        return mTempIntArray[0];\n    }\n\n    @Override\n    public void glGenBuffers(int n, int[] buffers, int offset) {\n        GLES20.glGenBuffers(n, buffers, offset);\n        GLES20Canvas.checkError();\n    }\n\n    @Override\n    public void glDeleteTextures(GL11 gl, int n, int[] textures, int offset) {\n        GLES20.glDeleteTextures(n, textures, offset);\n        GLES20Canvas.checkError();\n    }\n\n    @Override\n    public void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset) {\n        GLES20.glDeleteBuffers(n, buffers, offset);\n        GLES20Canvas.checkError();\n    }\n\n    @Override\n    public void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset) {\n        GLES20.glDeleteFramebuffers(n, buffers, offset);\n        GLES20Canvas.checkError();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/GLId.java",
    "content": "/*\n * Copyright (C) 2012 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\npackage com.hippo.glview.glrenderer;\n\nimport javax.microedition.khronos.opengles.GL11;\nimport javax.microedition.khronos.opengles.GL11ExtensionPack;\n\n// This mimics corresponding GL functions.\npublic interface GLId {\n    int generateTexture();\n\n    void glGenBuffers(int n, int[] buffers, int offset);\n\n    void glDeleteTextures(GL11 gl, int n, int[] textures, int offset);\n\n    void glDeleteBuffers(GL11 gl, int n, int[] buffers, int offset);\n\n    void glDeleteFramebuffers(GL11ExtensionPack gl11ep, int n, int[] buffers, int offset);\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/GLPaint.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\npublic class GLPaint {\n    private float mLineWidth = 1f;\n    private int mColor = 0;\n    private int mBackgroundColor = 0;\n\n    public int getColor() {\n        return mColor;\n    }\n\n    public void setColor(int color) {\n        mColor = color;\n    }\n\n    public int getBackgroundColor() {\n        return mBackgroundColor;\n    }\n\n    public void setBackgroundColor(int backgroundColor) {\n        mBackgroundColor = backgroundColor;\n    }\n\n    public float getLineWidth() {\n        return mLineWidth;\n    }\n\n    public void setLineWidth(float width) {\n        if (width < 0) {\n            throw new IllegalArgumentException(\"width < 0\");\n        }\n        mLineWidth = width;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/MovableTextTexture.java",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Typeface;\n\nimport java.util.Arrays;\n\n// TODO support multiline\n\n/**\n * Works like movable type.<br>\n * <br>\n * Only support single line now\n */\npublic final class MovableTextTexture extends SpriteTexture {\n    private final char[] mCharacters;\n    private final float[] mWidths;\n    private final float mHeight;\n    private final float mMaxWidth;\n\n    private MovableTextTexture(Bitmap bitmap, int count, int[] rects, char[] characters,\n                               float[] widths, float height, float maxWidth) {\n        super(bitmap, false, count, rects);\n        mCharacters = characters;\n        mWidths = widths;\n        mHeight = height;\n        mMaxWidth = maxWidth;\n    }\n\n    /**\n     * Create a TextTexture to draw text\n     *\n     * @param typeface   the typeface\n     * @param size       text size\n     * @param characters all Characters\n     * @return the TextTexture\n     */\n    public static MovableTextTexture create(Typeface typeface, int size, int color, char[] characters) {\n        Paint paint = new Paint();\n        paint.setAntiAlias(true);\n        paint.setTextSize(size);\n        paint.setColor(color);\n        paint.setTypeface(typeface);\n\n        Paint.FontMetricsInt fmi = paint.getFontMetricsInt();\n        int fixed = fmi.bottom;\n        int height = fmi.bottom - fmi.top;\n\n        int length = characters.length;\n        float[] widths = new float[length];\n        paint.getTextWidths(characters, 0, length, widths);\n\n        // Calculate bitmap size\n        float maxWidth = 0.0f;\n        for (float f : widths) {\n            maxWidth = Math.max(maxWidth, f);\n        }\n        int hCount = (int) Math.ceil(Math.sqrt(height / maxWidth * length));\n        int vCount = (int) Math.ceil(Math.sqrt(maxWidth / height * length));\n        if (hCount * (vCount - 1) > length) {\n            vCount--;\n        }\n        if ((hCount - 1) * vCount > length) {\n            hCount--;\n        }\n\n        Bitmap bitmap = Bitmap.createBitmap((int) Math.ceil(hCount * maxWidth),\n                (int) (double) (vCount * height), Bitmap.Config.ARGB_8888);\n        Canvas canvas = new Canvas(bitmap);\n        canvas.translate(0, height - fixed);\n\n        // Draw\n        int[] rects = new int[length * 4];\n        int x = 0;\n        int y = 0;\n        for (int i = 0; i < length; i++) {\n            int offset = i * 4;\n            rects[offset] = x;\n            rects[offset + 1] = y;\n            rects[offset + 2] = (int) widths[i];\n            rects[offset + 3] = height;\n\n            canvas.drawText(characters, i, 1, x, y, paint);\n\n            if (i % hCount == hCount - 1) {\n                // The end of row\n                x = 0;\n                y += height;\n            } else {\n                x += (int) maxWidth;\n            }\n        }\n\n        return new MovableTextTexture(bitmap, length, rects, characters, widths, height, maxWidth);\n    }\n\n    public int[] getTextIndexes(String text) {\n        int length = text.length();\n        int[] indexes = new int[length];\n        for (int i = 0; i < length; i++) {\n            char ch = text.charAt(i);\n            indexes[i] = Arrays.binarySearch(mCharacters, ch);\n        }\n\n        return indexes;\n    }\n\n    public float getTextWidth(String text) {\n        float width = 0.0f;\n        for (int i = 0, n = text.length(); i < n; i++) {\n            char ch = text.charAt(i);\n            int index = Arrays.binarySearch(mCharacters, ch);\n            if (index >= 0) {\n                width += mWidths[index];\n            } else {\n                width += mMaxWidth;\n            }\n        }\n\n        return width;\n    }\n\n    public float getTextWidth(int[] indexes) {\n        float width = 0.0f;\n        for (int index : indexes) {\n            if (index >= 0) {\n                width += mWidths[index];\n            } else {\n                width += mMaxWidth;\n            }\n        }\n\n        return width;\n    }\n\n    public float getMaxWidth() {\n        return mMaxWidth;\n    }\n\n    public float getTextHeight() {\n        return mHeight;\n    }\n\n    public void drawText(GLCanvas canvas, String text, int x, int y) {\n        for (int i = 0, n = text.length(); i < n; i++) {\n            char ch = text.charAt(i);\n            int index = Arrays.binarySearch(mCharacters, ch);\n            if (index >= 0) {\n                drawSprite(canvas, index, x, y);\n                x += (int) mWidths[index];\n            } else {\n                x += (int) mMaxWidth;\n            }\n        }\n    }\n\n    public void drawText(GLCanvas canvas, int[] indexes, int x, int y) {\n        for (int index : indexes) {\n            if (index >= 0) {\n                drawSprite(canvas, index, x, y);\n                x += (int) mWidths[index];\n            } else {\n                x += (int) mMaxWidth;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/NativeTexture.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.glrenderer;\n\nimport android.opengl.GLES20;\nimport android.util.Log;\n\nimport javax.microedition.khronos.opengles.GL11;\n\npublic abstract class NativeTexture extends BasicTexture {\n    private static final String TAG = NativeTexture.class.getSimpleName();\n\n    private boolean mContentValid = true;\n\n    private boolean mOpaque = true;\n\n    public static void checkError() {\n        int error = GLES20.glGetError();\n        if (error != 0) {\n            Throwable t = new Throwable();\n            Log.e(TAG, \"GL error: \" + error, t);\n        }\n    }\n\n    public void invalidateContent() {\n        mContentValid = false;\n    }\n\n    protected abstract void texImage(boolean init);\n\n    private void uploadToCanvas(GLCanvas canvas) {\n        // Get id\n        mId = canvas.getGLId().generateTexture();\n\n        // Prepare\n        canvas.setTextureParameters(this);\n\n        // Call glTexImage2D\n        GLES20.glBindTexture(getTarget(), mId);\n        checkError();\n        texImage(true);\n        checkError();\n\n        setAssociatedCanvas(canvas);\n        mState = STATE_LOADED;\n        mContentValid = true;\n    }\n\n    public void updateContent(GLCanvas canvas) {\n        if (!isLoaded()) {\n            uploadToCanvas(canvas);\n        } else if (!mContentValid) {\n            // Call glTexSubImage2D\n            GLES20.glBindTexture(getTarget(), mId);\n            checkError();\n            texImage(false);\n            checkError();\n            mContentValid = true;\n        }\n    }\n\n    /**\n     * Whether the content on GPU is valid.\n     */\n    public boolean isContentValid() {\n        return isLoaded() && mContentValid;\n    }\n\n    @Override\n    protected boolean onBind(GLCanvas canvas) {\n        updateContent(canvas);\n        return isContentValid();\n    }\n\n    @Override\n    protected int getTarget() {\n        return GL11.GL_TEXTURE_2D;\n    }\n\n    @Override\n    public boolean isOpaque() {\n        return mOpaque;\n    }\n\n    public void setOpaque(boolean isOpaque) {\n        mOpaque = isOpaque;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/RawTexture.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.util.Log;\n\nimport javax.microedition.khronos.opengles.GL11;\n\npublic class RawTexture extends BasicTexture {\n    private static final String TAG = \"RawTexture\";\n\n    private final boolean mOpaque;\n    private boolean mIsFlipped;\n\n    public RawTexture(int width, int height, boolean opaque) {\n        mOpaque = opaque;\n        setSize(width, height);\n    }\n\n    @Override\n    public boolean isOpaque() {\n        return mOpaque;\n    }\n\n    @Override\n    public boolean isFlippedVertically() {\n        return mIsFlipped;\n    }\n\n    public void setIsFlippedVertically(boolean isFlipped) {\n        mIsFlipped = isFlipped;\n    }\n\n    protected void prepare(GLCanvas canvas) {\n        GLId glId = canvas.getGLId();\n        mId = glId.generateTexture();\n        canvas.initializeTextureSize(this, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE);\n        canvas.setTextureParameters(this);\n        mState = STATE_LOADED;\n        setAssociatedCanvas(canvas);\n    }\n\n    @Override\n    protected boolean onBind(GLCanvas canvas) {\n        if (isLoaded()) return true;\n        Log.w(TAG, \"lost the content due to context change\");\n        return false;\n    }\n\n    @Override\n    public void yield() {\n        // we cannot free the texture because we have no backup.\n    }\n\n    @Override\n    protected int getTarget() {\n        return GL11.GL_TEXTURE_2D;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/SpriteTexture.java",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.RectF;\n\nimport com.hippo.yorozuya.AssertUtils;\n\npublic class SpriteTexture extends TiledTexture {\n    private final int mCount;\n    private final int[] mRects;\n\n    private final RectF mTempSource = new RectF();\n    private final RectF mTempTarget = new RectF();\n\n    public SpriteTexture(Bitmap bitmap, boolean isOpaque, int count, int[] rects) {\n        super(bitmap, isOpaque);\n\n        AssertUtils.assertEquals(\"rects.length must be count * 4\", count * 4, rects.length);\n        mCount = count;\n        mRects = rects;\n    }\n\n    public int getCount() {\n        return mCount;\n    }\n\n    public void drawSprite(GLCanvas canvas, int index, int x, int y) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        int sourceWidth = rects[offset + 2];\n        int sourceHeight = rects[offset + 3];\n        mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight);\n        mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight);\n        draw(canvas, mTempSource, mTempTarget);\n    }\n\n    public void drawSprite(GLCanvas canvas, int index, int x, int y, int width, int height) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], sourceY + rects[offset + 3]);\n        mTempTarget.set(x, y, x + width, y + height);\n        draw(canvas, mTempSource, mTempTarget);\n    }\n\n    public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, int x, int y) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        int sourceWidth = rects[offset + 2];\n        int sourceHeight = rects[offset + 3];\n        mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight);\n        mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight);\n        drawMixed(canvas, color, ratio, mTempSource, mTempTarget);\n    }\n\n    public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio,\n                                int x, int y, int width, int height) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2],\n                sourceY + rects[offset + 3]);\n        mTempTarget.set(x, y, x + width, y + height);\n        drawMixed(canvas, color, ratio, mTempSource, mTempTarget);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/StringTexture.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint.FontMetricsInt;\nimport android.graphics.Typeface;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\n\n// StringTexture is a texture shows the content of a specified String.\n//\n// To create a StringTexture, use the newInstance() method and specify\n// the String, the font size, and the color.\npublic class StringTexture extends CanvasTexture {\n    private final String mText;\n    private final TextPaint mPaint;\n    private final FontMetricsInt mMetrics;\n\n    private StringTexture(String text, TextPaint paint,\n                          FontMetricsInt metrics, int width, int height) {\n        super(width, height);\n        mText = text;\n        mPaint = paint;\n        mMetrics = metrics;\n    }\n\n    public static TextPaint getDefaultPaint(float textSize, int color) {\n        TextPaint paint = new TextPaint();\n        paint.setTextSize(textSize);\n        paint.setAntiAlias(true);\n        paint.setColor(color);\n        paint.setShadowLayer(2f, 0f, 0f, Color.BLACK);\n        return paint;\n    }\n\n    public static StringTexture newInstance(\n            String text, float textSize, int color) {\n        return newInstance(text, getDefaultPaint(textSize, color));\n    }\n\n    public static StringTexture newInstance(\n            String text, float textSize, int color,\n            float lengthLimit, boolean isBold) {\n        TextPaint paint = getDefaultPaint(textSize, color);\n        if (isBold) {\n            paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));\n        }\n        if (lengthLimit > 0) {\n            text = TextUtils.ellipsize(\n                    text, paint, lengthLimit, TextUtils.TruncateAt.END).toString();\n        }\n        return newInstance(text, paint);\n    }\n\n    private static StringTexture newInstance(String text, TextPaint paint) {\n        FontMetricsInt metrics = paint.getFontMetricsInt();\n        int width = (int) Math.ceil(paint.measureText(text));\n        int height = metrics.bottom - metrics.top;\n        // The texture size needs to be at least 1x1.\n        if (width <= 0) width = 1;\n        if (height <= 0) height = 1;\n        return new StringTexture(text, paint, metrics, width, height);\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas, Bitmap backing) {\n        canvas.translate(0, -mMetrics.ascent);\n        canvas.drawText(mText, 0, 0, mPaint);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/Texture.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.RectF;\n\n// Texture is a rectangular image which can be drawn on GLCanvas.\n// The isOpaque() function gives a hint about whether the texture is opaque,\n// so the drawing can be done faster.\n//\n// This is the current texture hierarchy:\n//\n// Texture\n// -- BasicTexture\n//    -- UploadedTexture\n//       -- BitmapTexture\n//       -- Tile\n//       -- CanvasTexture\n//          -- StringTexture\n//\npublic interface Texture {\n    int getWidth();\n\n    int getHeight();\n\n    void draw(GLCanvas canvas, int x, int y);\n\n    void draw(GLCanvas canvas, int x, int y, int w, int h);\n\n    void draw(GLCanvas canvas, RectF source, RectF target);\n\n    boolean isOpaque();\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/TiledTexture.java",
    "content": "/*\n * Copyright (C) 2012 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.PorterDuff;\nimport android.graphics.PorterDuffXfermode;\nimport android.graphics.RectF;\nimport android.os.SystemClock;\n\nimport com.hippo.glview.view.GLRoot;\n\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\n\n// This class is similar to BitmapTexture, except the bitmap is\n// split into tiles. By doing so, we may increase the time required to\n// upload the whole bitmap but we reduce the time of uploading each tile\n// so it make the animation more smooth and prevents jank.\npublic class TiledTexture implements Texture {\n    private static final int CONTENT_SIZE = 254;\n    private static final int BORDER_SIZE = 1;\n    private static final int TILE_SIZE = CONTENT_SIZE + 2 * BORDER_SIZE;\n    private static final int INIT_CAPACITY = 8;\n\n    // We are targeting at 60fps, so we have 16ms for each frame.\n    // In this 16ms, we use about 4~8 ms to upload tiles.\n    private static final long UPLOAD_TILE_LIMIT = 4; // ms\n    private static final Object sFreeTileLock = new Object();\n    private static final Bitmap sUploadBitmap;\n    private static final Canvas sCanvas;\n    private static final Paint sBitmapPaint;\n    private static final Paint sPaint;\n    private static Tile sFreeTileHead = null;\n\n    static {\n        sUploadBitmap = Bitmap.createBitmap(TILE_SIZE, TILE_SIZE, Bitmap.Config.ARGB_8888);\n        sCanvas = new Canvas(sUploadBitmap);\n        sBitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);\n        sBitmapPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));\n        sPaint = new Paint();\n        sPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));\n        sPaint.setColor(Color.TRANSPARENT);\n    }\n\n    private final Tile[] mTiles;  // Can be modified in different threads.\n    // Should be protected by \"synchronized.\"\n    private final int mWidth;\n    private final int mHeight;\n    private final RectF mSrcRect = new RectF();\n    private final RectF mDestRect = new RectF();\n    private int mUploadIndex = 0;\n\n    public TiledTexture(Bitmap bitmap, boolean isOpaque) {\n        mWidth = bitmap.getWidth();\n        mHeight = bitmap.getHeight();\n        ArrayList<Tile> list = new ArrayList<>();\n\n        for (int x = 0; x < mWidth; x += CONTENT_SIZE) {\n            for (int y = 0; y < mHeight; y += CONTENT_SIZE) {\n                Tile tile = obtainTile();\n                tile.offsetX = x;\n                tile.offsetY = y;\n                tile.bitmap = bitmap;\n                tile.setSize(\n                        Math.min(CONTENT_SIZE, mWidth - x),\n                        Math.min(CONTENT_SIZE, mHeight - y));\n                tile.setOpaque(isOpaque);\n                list.add(tile);\n            }\n        }\n        mTiles = list.toArray(new Tile[0]);\n    }\n\n    private static void freeTile(Tile tile) {\n        tile.invalidateContent();\n        tile.bitmap = null;\n        synchronized (sFreeTileLock) {\n            tile.nextFreeTile = sFreeTileHead;\n            sFreeTileHead = tile;\n        }\n    }\n\n    private static Tile obtainTile() {\n        synchronized (sFreeTileLock) {\n            Tile result = sFreeTileHead;\n            if (result == null) return new Tile();\n            sFreeTileHead = result.nextFreeTile;\n            result.nextFreeTile = null;\n            return result;\n        }\n    }\n\n    // We want to draw the \"source\" on the \"target\".\n    // This method is to find the \"output\" rectangle which is\n    // the corresponding area of the \"src\".\n    //                                   (x,y)  target\n    // (x0,y0)  source                     +---------------+\n    //    +----------+                     |               |\n    //    | src      |                     | output        |\n    //    | +--+     |    linear map       | +----+        |\n    //    | +--+     |    ---------->      | |    |        |\n    //    |          | by (scaleX, scaleY) | +----+        |\n    //    +----------+                     |               |\n    //      Texture                        +---------------+\n    //                                          Canvas\n    private static void mapRect(RectF output,\n                                RectF src, float x0, float y0, float x, float y, float scaleX,\n                                float scaleY) {\n        output.set(x + (src.left - x0) * scaleX,\n                y + (src.top - y0) * scaleY,\n                x + (src.right - x0) * scaleX,\n                y + (src.bottom - y0) * scaleY);\n    }\n\n    private boolean uploadNextTile(GLCanvas canvas) {\n        if (mUploadIndex == mTiles.length) return true;\n\n        synchronized (mTiles) {\n            Tile next = mTiles[mUploadIndex++];\n\n            // Make sure tile has not already been recycled by the time\n            // this is called (race condition in onGLIdle)\n            if (next.bitmap != null) {\n                boolean hasBeenLoad = next.isLoaded();\n                next.updateContent(canvas);\n\n                // It will take some time for a texture to be drawn for the first\n                // time. When scrolling, we need to draw several tiles on the screen\n                // at the same time. It may cause a UI jank even these textures has\n                // been uploaded.\n                if (!hasBeenLoad) next.draw(canvas, 0, 0);\n            }\n        }\n        return mUploadIndex == mTiles.length;\n    }\n\n    public boolean isReady() {\n        return mUploadIndex == mTiles.length;\n    }\n\n    // Can be called in UI thread.\n    public void recycle() {\n        synchronized (mTiles) {\n            for (Tile mTile : mTiles) {\n                freeTile(mTile);\n            }\n        }\n    }\n\n    // Draws a mixed color of this texture and a specified color onto the\n    // a rectangle. The used color is: from * (1 - ratio) + to * ratio.\n    public void drawMixed(GLCanvas canvas, int color, float ratio,\n                          int x, int y, int width, int height) {\n        RectF src = mSrcRect;\n        float scaleX = (float) width / mWidth;\n        float scaleY = (float) height / mHeight;\n        synchronized (mTiles) {\n            for (Tile t : mTiles) {\n                src.set(0, 0, t.contentWidth, t.contentHeight);\n                src.offset(t.offsetX, t.offsetY);\n                mapRect(mDestRect, src, 0, 0, x, y, scaleX, scaleY);\n                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);\n                canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);\n            }\n        }\n    }\n\n    public void drawMixed(GLCanvas canvas, int color, float ratio,\n                          RectF source, RectF target) {\n        RectF src = mSrcRect;\n        float x0 = source.left;\n        float y0 = source.top;\n        float x = target.left;\n        float y = target.top;\n        float scaleX = target.width() / source.width();\n        float scaleY = target.height() / source.height();\n\n        synchronized (mTiles) {\n            for (Tile t : mTiles) {\n                src.set(0, 0, t.contentWidth, t.contentHeight);\n                src.offset(t.offsetX, t.offsetY);\n                if (!src.intersect(source)) continue;\n                mapRect(mDestRect, src, x0, y0, x, y, scaleX, scaleY);\n                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);\n                canvas.drawMixed(t, color, ratio, mSrcRect, mDestRect);\n            }\n        }\n    }\n\n    // Draws the texture on to the specified rectangle.\n    @Override\n    public void draw(GLCanvas canvas, int x, int y, int width, int height) {\n        RectF src = mSrcRect;\n        float scaleX = (float) width / mWidth;\n        float scaleY = (float) height / mHeight;\n        synchronized (mTiles) {\n            for (Tile t : mTiles) {\n                src.set(0, 0, t.contentWidth, t.contentHeight);\n                src.offset(t.offsetX, t.offsetY);\n                mapRect(mDestRect, src, 0, 0, x, y, scaleX, scaleY);\n                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);\n                canvas.drawTexture(t, mSrcRect, mDestRect);\n            }\n        }\n    }\n\n    // Draws a sub region of this texture on to the specified rectangle.\n    @Override\n    public void draw(GLCanvas canvas, RectF source, RectF target) {\n        RectF src = mSrcRect;\n        RectF dest = mDestRect;\n        float x0 = source.left;\n        float y0 = source.top;\n        float x = target.left;\n        float y = target.top;\n        float scaleX = target.width() / source.width();\n        float scaleY = target.height() / source.height();\n\n        synchronized (mTiles) {\n            for (Tile t : mTiles) {\n                src.set(0, 0, t.contentWidth, t.contentHeight);\n                src.offset(t.offsetX, t.offsetY);\n                if (!src.intersect(source)) continue;\n                mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);\n                src.offset(BORDER_SIZE - t.offsetX, BORDER_SIZE - t.offsetY);\n                canvas.drawTexture(t, src, dest);\n            }\n        }\n    }\n\n    @Override\n    public int getWidth() {\n        return mWidth;\n    }\n\n    @Override\n    public int getHeight() {\n        return mHeight;\n    }\n\n    @Override\n    public void draw(GLCanvas canvas, int x, int y) {\n        draw(canvas, x, y, mWidth, mHeight);\n    }\n\n    @Override\n    public boolean isOpaque() {\n        return false;\n    }\n\n    public static class Uploader implements GLRoot.OnGLIdleListener {\n        private final ArrayDeque<TiledTexture> mTextures =\n                new ArrayDeque<>(INIT_CAPACITY);\n\n        private final GLRoot mGlRoot;\n        private boolean mIsQueued = false;\n\n        public Uploader(GLRoot glRoot) {\n            mGlRoot = glRoot;\n        }\n\n        public synchronized void clear() {\n            mTextures.clear();\n        }\n\n        public synchronized void addTexture(TiledTexture t) {\n            if (t.isReady()) return;\n            mTextures.addLast(t);\n\n            if (mIsQueued) return;\n            mIsQueued = true;\n            mGlRoot.addOnGLIdleListener(this);\n        }\n\n        @Override\n        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {\n            ArrayDeque<TiledTexture> deque = mTextures;\n            synchronized (this) {\n                long now = SystemClock.uptimeMillis();\n                long dueTime = now + UPLOAD_TILE_LIMIT;\n                while (now < dueTime && !deque.isEmpty()) {\n                    TiledTexture t = deque.peekFirst();\n                    if (t != null && t.uploadNextTile(canvas)) {\n                        deque.removeFirst();\n                        mGlRoot.requestRender();\n                    }\n                    now = SystemClock.uptimeMillis();\n                }\n                mIsQueued = !mTextures.isEmpty();\n\n                // return true to keep this listener in the queue\n                return mIsQueued;\n            }\n        }\n    }\n\n    private static class Tile extends UploadedTexture {\n        public int offsetX;\n        public int offsetY;\n        public Bitmap bitmap;\n        public Tile nextFreeTile;\n        public int contentWidth;\n        public int contentHeight;\n\n        @Override\n        public void setSize(int width, int height) {\n            contentWidth = width;\n            contentHeight = height;\n            mWidth = width + 2 * BORDER_SIZE;\n            mHeight = height + 2 * BORDER_SIZE;\n            mTextureWidth = TILE_SIZE;\n            mTextureHeight = TILE_SIZE;\n        }\n\n        @Override\n        protected Bitmap onGetBitmap() {\n            // make a local copy of the reference to the bitmap,\n            // since it might be null'd in a different thread. b/8694871\n            Bitmap localBitmapRef = bitmap;\n            bitmap = null;\n\n            if (localBitmapRef != null) {\n                int x = BORDER_SIZE - offsetX;\n                int y = BORDER_SIZE - offsetY;\n                int r = localBitmapRef.getWidth() + x;\n                int b = localBitmapRef.getHeight() + y;\n                sCanvas.drawBitmap(localBitmapRef, x, y, sBitmapPaint);\n\n                // draw borders if need\n                if (x > 0) sCanvas.drawLine(x - 1, 0, x - 1, TILE_SIZE, sPaint);\n                if (y > 0) sCanvas.drawLine(0, y - 1, TILE_SIZE, y - 1, sPaint);\n                if (r < CONTENT_SIZE) sCanvas.drawLine(r, 0, r, TILE_SIZE, sPaint);\n                if (b < CONTENT_SIZE) sCanvas.drawLine(0, b, TILE_SIZE, b, sPaint);\n            }\n\n            return sUploadBitmap;\n        }\n\n        @Override\n        protected void onFreeBitmap(Bitmap bitmap) {\n            // do nothing\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/glrenderer/UploadedTexture.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.glrenderer;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Bitmap.Config;\nimport android.opengl.GLUtils;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.HashMap;\n\nimport javax.microedition.khronos.opengles.GL11;\n\n// UploadedTextures use a Bitmap for the content of the texture.\n//\n// Subclasses should implement onGetBitmap() to provide the Bitmap and\n// implement onFreeBitmap(mBitmap) which will be called when the Bitmap\n// is not needed anymore.\n//\n// isContentValid() is meaningful only when the isLoaded() returns true.\n// It means whether the content needs to be updated.\n//\n// The user of this class should call recycle() when the texture is not\n// needed anymore.\n//\n// By default an UploadedTexture is opaque (so it can be drawn faster without\n// blending). The user or subclass can override it using setOpaque().\npublic abstract class UploadedTexture extends BasicTexture {\n    // To prevent keeping allocation the borders, we store those used borders here.\n    // Since the length will be power of two, it won't use too much memory.\n    private static final HashMap<BorderKey, Bitmap> sBorderLines =\n            new HashMap<>();\n    private static final BorderKey sBorderKey = new BorderKey();\n\n    @SuppressWarnings(\"unused\")\n    private static final String TAG = \"Texture\";\n    private static final int UPLOAD_LIMIT = 100;\n    private static int sUploadedCount;\n    protected Bitmap mBitmap;\n    private boolean mContentValid = true;\n    // indicate this textures is being uploaded in background\n    private boolean mIsUploading = false;\n    private boolean mOpaque = true;\n    private boolean mThrottled = false;\n    private int mBorder;\n\n    protected UploadedTexture() {\n        this(false);\n    }\n\n    protected UploadedTexture(boolean hasBorder) {\n        super(null, 0, STATE_UNLOADED);\n        if (hasBorder) {\n            setBorder(true);\n            mBorder = 1;\n        }\n    }\n\n    private static Bitmap getBorderLine(\n            boolean vertical, Config config, int length) {\n        BorderKey key = sBorderKey;\n        key.vertical = vertical;\n        key.config = config;\n        key.length = length;\n        Bitmap bitmap = sBorderLines.get(key);\n        if (bitmap == null) {\n            bitmap = vertical\n                    ? Bitmap.createBitmap(1, length, config)\n                    : Bitmap.createBitmap(length, 1, config);\n            sBorderLines.put(key.clone(), bitmap);\n        }\n        return bitmap;\n    }\n\n    public static void resetUploadLimit() {\n        sUploadedCount = 0;\n    }\n\n    public static boolean uploadLimitReached() {\n        return sUploadedCount > UPLOAD_LIMIT;\n    }\n\n    protected void setIsUploading(boolean uploading) {\n        mIsUploading = uploading;\n    }\n\n    public boolean isUploading() {\n        return mIsUploading;\n    }\n\n    protected void setThrottled(boolean throttled) {\n        mThrottled = throttled;\n    }\n\n    private Bitmap getBitmap() {\n        if (mBitmap == null) {\n            mBitmap = onGetBitmap();\n            if (mWidth == UNSPECIFIED) {\n                int w = mBitmap.getWidth() + mBorder * 2;\n                int h = mBitmap.getHeight() + mBorder * 2;\n                setSize(w, h);\n            }\n        }\n        return mBitmap;\n    }\n\n    private void freeBitmap() {\n        if (mBitmap == null) {\n            throw new IllegalStateException(\"mBitmap == null\");\n        }\n        onFreeBitmap(mBitmap);\n        mBitmap = null;\n    }\n\n    @Override\n    public int getWidth() {\n        if (mWidth == UNSPECIFIED) getBitmap();\n        return mWidth;\n    }\n\n    @Override\n    public int getHeight() {\n        if (mWidth == UNSPECIFIED) getBitmap();\n        return mHeight;\n    }\n\n    protected abstract Bitmap onGetBitmap();\n\n    protected abstract void onFreeBitmap(Bitmap bitmap);\n\n    public void invalidateContent() {\n        if (mBitmap != null) freeBitmap();\n        mContentValid = false;\n        mWidth = UNSPECIFIED;\n        mHeight = UNSPECIFIED;\n    }\n\n    /**\n     * Whether the content on GPU is valid.\n     */\n    public boolean isContentValid() {\n        return isLoaded() && mContentValid;\n    }\n\n    /**\n     * Updates the content on GPU's memory.\n     *\n     * @param canvas canvas\n     */\n    public void updateContent(GLCanvas canvas) {\n        if (!isLoaded()) {\n            if (mThrottled && ++sUploadedCount > UPLOAD_LIMIT) {\n                return;\n            }\n            uploadToCanvas(canvas);\n        } else if (!mContentValid) {\n            Bitmap bitmap = getBitmap();\n            int format = GLUtils.getInternalFormat(bitmap);\n            int type = GLUtils.getType(bitmap);\n            canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);\n            freeBitmap();\n            mContentValid = true;\n        }\n    }\n\n    private void uploadToCanvas(GLCanvas canvas) {\n        Bitmap bitmap = getBitmap();\n        if (bitmap != null) {\n            try {\n                int bWidth = bitmap.getWidth();\n                int bHeight = bitmap.getHeight();\n                int texWidth = getTextureWidth();\n                int texHeight = getTextureHeight();\n\n                if (bWidth > texWidth || bHeight > texHeight) {\n                    throw new IllegalStateException(\"bWidth > texWidth || bHeight > texHeight\");\n                }\n\n                // Upload the bitmap to a new texture.\n                mId = canvas.getGLId().generateTexture();\n                canvas.setTextureParameters(this);\n\n                if (bWidth == texWidth && bHeight == texHeight) {\n                    canvas.initializeTexture(this, bitmap);\n                } else {\n                    int format = GLUtils.getInternalFormat(bitmap);\n                    int type = GLUtils.getType(bitmap);\n                    Config config = bitmap.getConfig();\n\n                    canvas.initializeTextureSize(this, format, type);\n                    canvas.texSubImage2D(this, mBorder, mBorder, bitmap, format, type);\n\n                    if (mBorder > 0) {\n                        // Left border\n                        Bitmap line = getBorderLine(true, config, texHeight);\n                        canvas.texSubImage2D(this, 0, 0, line, format, type);\n\n                        // Top border\n                        line = getBorderLine(false, config, texWidth);\n                        canvas.texSubImage2D(this, 0, 0, line, format, type);\n                    }\n\n                    // Right border\n                    if (mBorder + bWidth < texWidth) {\n                        Bitmap line = getBorderLine(true, config, texHeight);\n                        canvas.texSubImage2D(this, mBorder + bWidth, 0, line, format, type);\n                    }\n\n                    // Bottom border\n                    if (mBorder + bHeight < texHeight) {\n                        Bitmap line = getBorderLine(false, config, texWidth);\n                        canvas.texSubImage2D(this, 0, mBorder + bHeight, line, format, type);\n                    }\n                }\n            } finally {\n                freeBitmap();\n            }\n            // Update texture state.\n            setAssociatedCanvas(canvas);\n            mState = STATE_LOADED;\n            mContentValid = true;\n        } else {\n            mState = STATE_ERROR;\n            throw new RuntimeException(\"Texture load fail, no bitmap\");\n        }\n    }\n\n    @Override\n    protected boolean onBind(GLCanvas canvas) {\n        updateContent(canvas);\n        return isContentValid();\n    }\n\n    @Override\n    protected int getTarget() {\n        return GL11.GL_TEXTURE_2D;\n    }\n\n    @Override\n    public boolean isOpaque() {\n        return mOpaque;\n    }\n\n    public void setOpaque(boolean isOpaque) {\n        mOpaque = isOpaque;\n    }\n\n    @Override\n    public void recycle() {\n        super.recycle();\n        if (mBitmap != null) freeBitmap();\n    }\n\n    private static class BorderKey implements Cloneable {\n        public boolean vertical;\n        public Config config;\n        public int length;\n\n        @Override\n        public int hashCode() {\n            int x = config.hashCode() ^ length;\n            return vertical ? x : -x;\n        }\n\n        @Override\n        public boolean equals(Object object) {\n            if (!(object instanceof BorderKey o)) return false;\n            return vertical == o.vertical\n                    && config == o.config && length == o.length;\n        }\n\n        @NonNull\n        @Override\n        public BorderKey clone() {\n            try {\n                return (BorderKey) super.clone();\n            } catch (CloneNotSupportedException e) {\n                throw new AssertionError(e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/image/GLImageMovableTextView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.image;\n\nimport android.graphics.Rect;\nimport android.text.TextUtils;\n\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.view.GLView;\nimport com.hippo.glview.view.Gravity;\n\npublic class GLImageMovableTextView extends GLView {\n    ImageMovableTextTexture mTextTexture;\n\n    private String mText = \"\";\n    private int[] mIndexes = new int[0];\n\n    private int mGravity = Gravity.NO_GRAVITY;\n\n    private void generateIndexes() {\n        if (mTextTexture == null || TextUtils.isEmpty(mText)) {\n            mIndexes = new int[0];\n        } else {\n            mIndexes = mTextTexture.getTextIndexes(mText);\n        }\n    }\n\n    public void setTextTexture(ImageMovableTextTexture textTexture) {\n        if (mTextTexture == textTexture) {\n            return;\n        }\n        mTextTexture = textTexture;\n\n        generateIndexes();\n        requestLayout();\n    }\n\n    public void setText(String text) {\n        if (text == null) {\n            text = \"\";\n        }\n        if (text.equals(mText)) {\n            return;\n        }\n        mText = text;\n\n        generateIndexes();\n        requestLayout();\n    }\n\n    public void setGravity(int gravity) {\n        if (mGravity != gravity) {\n            mGravity = gravity;\n            invalidate();\n        }\n    }\n\n    @Override\n    protected int getSuggestedMinimumWidth() {\n        if (mTextTexture == null) {\n            return super.getSuggestedMinimumWidth();\n        } else {\n            return Math.max((int) mTextTexture.getTextWidth(mIndexes) + mPaddings.left + mPaddings.right,\n                    super.getSuggestedMinimumWidth());\n        }\n    }\n\n    @Override\n    protected int getSuggestedMinimumHeight() {\n        if (mTextTexture == null) {\n            return super.getSuggestedMinimumHeight();\n        } else {\n            return Math.max((int) mTextTexture.getTextHeight() + mPaddings.top + mPaddings.bottom,\n                    super.getSuggestedMinimumHeight());\n        }\n    }\n\n    @Override\n    public void onRender(GLCanvas canvas) {\n        if (mTextTexture == null || mIndexes == null) {\n            return;\n        }\n\n        Rect paddings = getPaddings();\n        int x = getDefaultBegin(getWidth(), (int) mTextTexture.getTextWidth(mIndexes),\n                paddings.left, paddings.right, Gravity.getPosition(mGravity, Gravity.HORIZONTAL));\n        int y = getDefaultBegin(getHeight(), (int) mTextTexture.getTextHeight(),\n                paddings.top, paddings.bottom, Gravity.getPosition(mGravity, Gravity.VERTICAL));\n        mTextTexture.drawText(canvas, mIndexes, x, y);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.image;\n\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Typeface;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.image.Image;\n\nimport java.util.Arrays;\n\npublic class ImageMovableTextTexture extends ImageSpriteTexture {\n    private final char[] mCharacters;\n    private final float[] mWidths;\n    private final float mHeight;\n    private final float mMaxWidth;\n\n    public ImageMovableTextTexture(@NonNull ImageWrapper image, int count, int[] rects,\n                                   char[] characters, float[] widths, float height, float maxWidth) {\n        super(image, count, rects);\n\n        mCharacters = characters;\n        mWidths = widths;\n        mHeight = height;\n        mMaxWidth = maxWidth;\n    }\n\n    /**\n     * Create a TextTexture to draw text\n     *\n     * @param typeface   the typeface\n     * @param size       text size\n     * @param characters all Characters\n     * @return the TextTexture\n     */\n    @Nullable\n    public static ImageMovableTextTexture create(Typeface typeface, int size, int color, char[] characters) {\n        Paint paint = new Paint();\n        paint.setAntiAlias(true);\n        paint.setTextSize(size);\n        paint.setColor(color);\n        paint.setTypeface(typeface);\n\n        Paint.FontMetricsInt fmi = paint.getFontMetricsInt();\n        int fixed = fmi.bottom;\n        int height = fmi.bottom - fmi.top;\n\n        int length = characters.length;\n        float[] widths = new float[length];\n        paint.getTextWidths(characters, 0, length, widths);\n\n        // Calculate bitmap size\n        float maxWidth = 0.0f;\n        for (float f : widths) {\n            maxWidth = Math.max(maxWidth, f);\n        }\n        int hCount = (int) Math.ceil(Math.sqrt(height / maxWidth * length));\n        int vCount = (int) Math.ceil(Math.sqrt(maxWidth / height * length));\n        if (hCount * (vCount - 1) > length) {\n            vCount--;\n        }\n        if ((hCount - 1) * vCount > length) {\n            hCount--;\n        }\n\n        Bitmap bitmap = Bitmap.createBitmap((int) Math.ceil(hCount * maxWidth),\n                (int) (double) (vCount * height), Bitmap.Config.ARGB_8888);\n        Canvas canvas = new Canvas(bitmap);\n        canvas.translate(0, height - fixed);\n\n        // Draw\n        int[] rects = new int[length * 4];\n        int x = 0;\n        int y = 0;\n        for (int i = 0; i < length; i++) {\n            int offset = i * 4;\n            rects[offset] = x;\n            rects[offset + 1] = y;\n            rects[offset + 2] = (int) widths[i];\n            rects[offset + 3] = height;\n\n            canvas.drawText(characters, i, 1, x, y, paint);\n\n            if (i % hCount == hCount - 1) {\n                // The end of row\n                x = 0;\n                y += height;\n            } else {\n                x += (int) maxWidth;\n            }\n        }\n\n        Image image = Image.create(bitmap);\n        ImageWrapper imageWrapper = new ImageWrapper(image);\n        if (imageWrapper.obtain()) {\n            return new ImageMovableTextTexture(imageWrapper, length, rects, characters, widths, height, maxWidth);\n        } else {\n            return null;\n        }\n    }\n\n    public int[] getTextIndexes(String text) {\n        int length = text.length();\n        int[] indexes = new int[length];\n        for (int i = 0; i < length; i++) {\n            char ch = text.charAt(i);\n            indexes[i] = Arrays.binarySearch(mCharacters, ch);\n        }\n\n        return indexes;\n    }\n\n    public float getTextWidth(String text) {\n        float width = 0.0f;\n        for (int i = 0, n = text.length(); i < n; i++) {\n            char ch = text.charAt(i);\n            int index = Arrays.binarySearch(mCharacters, ch);\n            if (index >= 0) {\n                width += mWidths[index];\n            } else {\n                width += mMaxWidth;\n            }\n        }\n\n        return width;\n    }\n\n    public float getTextWidth(int[] indexes) {\n        float width = 0.0f;\n        for (int index : indexes) {\n            if (index >= 0) {\n                width += mWidths[index];\n            } else {\n                width += mMaxWidth;\n            }\n        }\n\n        return width;\n    }\n\n    public float getMaxWidth() {\n        return mMaxWidth;\n    }\n\n    public float getTextHeight() {\n        return mHeight;\n    }\n\n    public void drawText(GLCanvas canvas, String text, int x, int y) {\n        for (int i = 0, n = text.length(); i < n; i++) {\n            char ch = text.charAt(i);\n            int index = Arrays.binarySearch(mCharacters, ch);\n            if (index >= 0) {\n                drawSprite(canvas, index, x, y);\n                x += (int) mWidths[index];\n            } else {\n                x += (int) mMaxWidth;\n            }\n        }\n    }\n\n    public void drawText(GLCanvas canvas, int[] indexes, int x, int y) {\n        for (int index : indexes) {\n            if (index >= 0) {\n                drawSprite(canvas, index, x, y);\n                x += (int) mWidths[index];\n            } else {\n                x += (int) mMaxWidth;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/image/ImageSpriteTexture.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.image;\n\nimport android.graphics.RectF;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.yorozuya.AssertUtils;\n\npublic class ImageSpriteTexture extends ImageTexture {\n    private final int mCount;\n    private final int[] mRects;\n\n    private final RectF mTempSource = new RectF();\n    private final RectF mTempTarget = new RectF();\n\n    public ImageSpriteTexture(@NonNull ImageWrapper image, int count, int[] rects) {\n        super(image);\n\n        AssertUtils.assertEquals(\"rects.length must be count * 4\", count * 4, rects.length);\n        mCount = count;\n        mRects = rects;\n    }\n\n    public int getCount() {\n        return mCount;\n    }\n\n    public void drawSprite(GLCanvas canvas, int index, int x, int y) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        int sourceWidth = rects[offset + 2];\n        int sourceHeight = rects[offset + 3];\n        mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight);\n        mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight);\n        draw(canvas, mTempSource, mTempTarget);\n    }\n\n    public void drawSprite(GLCanvas canvas, int index, int x, int y, int width, int height) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2], sourceY + rects[offset + 3]);\n        mTempTarget.set(x, y, x + width, y + height);\n        draw(canvas, mTempSource, mTempTarget);\n    }\n\n    public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio, int x, int y) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        int sourceWidth = rects[offset + 2];\n        int sourceHeight = rects[offset + 3];\n        mTempSource.set(sourceX, sourceY, sourceX + sourceWidth, sourceY + sourceHeight);\n        mTempTarget.set(x, y, x + sourceWidth, y + sourceHeight);\n        drawMixed(canvas, color, ratio, mTempSource, mTempTarget);\n    }\n\n    public void drawSpriteMixed(GLCanvas canvas, int index, int color, float ratio,\n                                int x, int y, int width, int height) {\n        int[] rects = mRects;\n        int offset = index * 4;\n        int sourceX = rects[offset];\n        int sourceY = rects[offset + 1];\n        mTempSource.set(sourceX, sourceY, sourceX + rects[offset + 2],\n                sourceY + rects[offset + 3]);\n        mTempTarget.set(x, y, x + width, y + height);\n        drawMixed(canvas, color, ratio, mTempSource, mTempTarget);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/image/ImageTexture.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.glview.image;\n\nimport android.graphics.RectF;\nimport android.graphics.drawable.Animatable;\nimport android.os.Process;\nimport android.os.SystemClock;\n\nimport androidx.annotation.IntDef;\nimport androidx.annotation.NonNull;\n\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.glrenderer.NativeTexture;\nimport com.hippo.glview.glrenderer.Texture;\nimport com.hippo.glview.view.GLRoot;\nimport com.hippo.yorozuya.thread.InfiniteThreadExecutor;\nimport com.hippo.yorozuya.thread.PriorityThreadFactory;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.ref.WeakReference;\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.LinkedList;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.atomic.AtomicBoolean;\n\npublic class ImageTexture implements Texture, Animatable {\n    private static final int TILE_SMALL = 0;\n    private static final int TILE_LARGE = 1;\n    private static final int SMALL_CONTENT_SIZE = 254;\n    private static final int SMALL_BORDER_SIZE = 1;\n    private static final int SMALL_TILE_SIZE = SMALL_CONTENT_SIZE + 2 * SMALL_BORDER_SIZE;\n    private static final int LARGE_CONTENT_SIZE = SMALL_CONTENT_SIZE * 2;\n    private static final int LARGE_BORDER_SIZE = SMALL_BORDER_SIZE * 2;\n    private static final int LARGE_TILE_SIZE = LARGE_CONTENT_SIZE + 2 * LARGE_BORDER_SIZE;\n    private static final int INIT_CAPACITY = 8;\n    // We are targeting at 60fps, so we have 16ms for each frame.\n    // In this 16ms, we use about 4~8 ms to upload tiles.\n    private static final long UPLOAD_TILE_LIMIT = 4; // ms\n    private static final Executor sThreadExecutor;\n    private static final Object sFreeTileLock = new Object();\n    private static Tile sSmallFreeTileHead = null;\n    private static Tile sLargeFreeTileHead = null;\n\n    static {\n        sThreadExecutor = new InfiniteThreadExecutor(10 * 1000, new LinkedList<>(),\n                new PriorityThreadFactory(\"ImageTexture$AnimateTask\", Process.THREAD_PRIORITY_BACKGROUND));\n    }\n\n    private final ImageWrapper mImage;\n    private final Tile[] mTiles;  // Can be modified in different threads.\n    private final int mWidth;\n    // Should be protected by \"synchronized.\"\n    private final int mHeight;\n    private final boolean mOpaque;\n    private final RectF mSrcRect = new RectF();\n    private final RectF mDestRect = new RectF();\n    private final AtomicBoolean mRunning = new AtomicBoolean();\n    private final AtomicBoolean mRequestAnimation = new AtomicBoolean();\n    private final AtomicBoolean mFrameDirty = new AtomicBoolean();\n    private final AtomicBoolean mNeedRelease = new AtomicBoolean();\n    private final AtomicBoolean mReleased = new AtomicBoolean();\n    private int mUploadIndex = 0;\n    private boolean mImageBusy = false;\n    private Runnable mAnimateRunnable = null;\n\n    private WeakReference<Callback> mCallback;\n\n    /**\n     * Call {@link ImageWrapper#obtain()} first\n     */\n    public ImageTexture(@NonNull ImageWrapper image) {\n        mImage = image;\n        int width = mWidth = image.getWidth();\n        int height = mHeight = image.getHeight();\n        boolean opaque = mOpaque = image.isOpaque();\n        ArrayList<Tile> list = new ArrayList<>();\n\n        for (int x = 0; x < width; x += LARGE_CONTENT_SIZE) {\n            for (int y = 0; y < height; y += LARGE_CONTENT_SIZE) {\n                int w = Math.min(LARGE_CONTENT_SIZE, width - x);\n                int h = Math.min(LARGE_CONTENT_SIZE, height - y);\n\n                if (w <= SMALL_CONTENT_SIZE) {\n                    Tile tile = obtainSmallTile();\n                    tile.offsetX = x;\n                    tile.offsetY = y;\n                    tile.image = image;\n                    tile.setSize(TILE_SMALL, w, Math.min(SMALL_CONTENT_SIZE, h));\n                    tile.setOpaque(opaque);\n                    list.add(tile);\n\n                    int nextHeight = h - SMALL_CONTENT_SIZE;\n                    if (nextHeight > 0) {\n                        Tile nextTile = obtainSmallTile();\n                        nextTile.offsetX = x;\n                        nextTile.offsetY = y + SMALL_CONTENT_SIZE;\n                        nextTile.image = image;\n                        nextTile.setSize(TILE_SMALL, w, nextHeight);\n                        nextTile.setOpaque(opaque);\n                        list.add(nextTile);\n                    }\n                } else if (h <= SMALL_CONTENT_SIZE) {\n                    Tile tile = obtainSmallTile();\n                    tile.offsetX = x;\n                    tile.offsetY = y;\n                    tile.image = image;\n                    tile.setSize(TILE_SMALL, SMALL_CONTENT_SIZE, h);\n                    tile.setOpaque(opaque);\n                    list.add(tile);\n\n                    int nextWidth = w - SMALL_CONTENT_SIZE;\n                    Tile nextTile = obtainSmallTile();\n                    nextTile.offsetX = x + SMALL_CONTENT_SIZE;\n                    nextTile.offsetY = y;\n                    nextTile.image = image;\n                    nextTile.setSize(TILE_SMALL, nextWidth, h);\n                    nextTile.setOpaque(opaque);\n                    list.add(nextTile);\n                } else {\n                    Tile tile = obtainLargeTile();\n                    tile.offsetX = x;\n                    tile.offsetY = y;\n                    tile.image = image;\n                    tile.setSize(TILE_LARGE, w, h);\n                    tile.setOpaque(opaque);\n                    list.add(tile);\n                }\n            }\n        }\n\n        mTiles = list.toArray(new Tile[0]);\n    }\n\n    private static Tile obtainSmallTile() {\n        synchronized (sFreeTileLock) {\n            Tile result = sSmallFreeTileHead;\n            if (result == null) {\n                return new Tile();\n            } else {\n                sSmallFreeTileHead = result.nextFreeTile;\n                result.nextFreeTile = null;\n            }\n            return result;\n        }\n    }\n\n    private static Tile obtainLargeTile() {\n        synchronized (sFreeTileLock) {\n            Tile result = sLargeFreeTileHead;\n            if (result == null) {\n                return new Tile();\n            } else {\n                sLargeFreeTileHead = result.nextFreeTile;\n                result.nextFreeTile = null;\n            }\n            return result;\n        }\n    }\n\n    // We want to draw the \"source\" on the \"target\".\n    // This method is to find the \"output\" rectangle which is\n    // the corresponding area of the \"src\".\n    //                                   (x,y)  target\n    // (x0,y0)  source                     +---------------+\n    //    +----------+                     |               |\n    //    | src      |                     | output        |\n    //    | +--+     |    linear map       | +----+        |\n    //    | +--+     |    ---------->      | |    |        |\n    //    |          | by (scaleX, scaleY) | +----+        |\n    //    +----------+                     |               |\n    //      Texture                        +---------------+\n    //                                          Canvas\n    private static void mapRect(RectF output,\n                                RectF src, float x0, float y0, float x, float y, float scaleX,\n                                float scaleY) {\n        output.set(x + (src.left - x0) * scaleX,\n                y + (src.top - y0) * scaleY,\n                x + (src.right - x0) * scaleX,\n                y + (src.bottom - y0) * scaleY);\n    }\n\n    public Callback getCallback() {\n        if (mCallback != null) {\n            return mCallback.get();\n        }\n        return null;\n    }\n\n    public final void setCallback(Callback cb) {\n        mCallback = new WeakReference<>(cb);\n    }\n\n    public void invalidateSelf() {\n        final Callback callback = getCallback();\n        if (callback != null) {\n            callback.invalidateImageTexture(this);\n        }\n    }\n\n    @Override\n    public void start() {\n        synchronized (mImage) {\n            if (!mImageBusy) {\n                mImageBusy = true;\n            } else {\n                mRequestAnimation.lazySet(true);\n                return;\n            }\n        }\n\n        boolean end = mReleased.get() || mImage.isImageRecycled() || mNeedRelease.get() ||\n                (!mImage.getAnimated()) || mRunning.get();\n\n        synchronized (mImage) {\n            mImageBusy = false;\n        }\n\n        if (end) {\n            return;\n        }\n\n        mRunning.lazySet(true);\n\n        synchronized (mImage) {\n            if (mAnimateRunnable == null) {\n                Runnable runnable = new AnimateRunnable();\n                mAnimateRunnable = runnable;\n                sThreadExecutor.execute(runnable);\n            }\n        }\n    }\n\n    @Override\n    public void stop() {\n        mRunning.lazySet(false);\n        mRequestAnimation.lazySet(false);\n    }\n\n    @Override\n    public boolean isRunning() {\n        return mRunning.get();\n    }\n\n    private boolean uploadNextTile(GLCanvas canvas) {\n        if (mUploadIndex == mTiles.length) return true;\n\n        synchronized (mTiles) {\n            Tile next = mTiles[mUploadIndex++];\n\n            // Make sure tile has not already been recycled by the time\n            // this is called (race condition in onGLIdle)\n            if (next.image != null) {\n                boolean hasBeenLoad = next.isLoaded();\n                next.updateContent(canvas);\n\n                // It will take some time for a texture to be drawn for the first\n                // time. When scrolling, we need to draw several tiles on the screen\n                // at the same time. It may cause a UI jank even these textures has\n                // been uploaded.\n                if (!hasBeenLoad) next.draw(canvas, 0, 0);\n            }\n        }\n        return mUploadIndex == mTiles.length;\n    }\n\n    @Override\n    public int getWidth() {\n        return mWidth;\n    }\n\n    @Override\n    public int getHeight() {\n        return mHeight;\n    }\n\n    private void syncFrame() {\n        if (mFrameDirty.getAndSet(false)) {\n            // invalid tiles\n            for (Tile tile : mTiles) {\n                tile.invalidateContent();\n            }\n            mImage.setFrameUpdateAllowed(true);\n        }\n    }\n\n    @Override\n    public void draw(GLCanvas canvas, int x, int y) {\n        draw(canvas, x, y, mWidth, mHeight);\n    }\n\n    // Draws the texture on to the specified rectangle.\n    @Override\n    public void draw(GLCanvas canvas, int x, int y, int w, int h) {\n        RectF src = mSrcRect;\n        RectF dest = mDestRect;\n        float scaleX = (float) w / mWidth;\n        float scaleY = (float) h / mHeight;\n\n        syncFrame();\n        for (Tile t : mTiles) {\n            src.set(0, 0, t.contentWidth, t.contentHeight);\n            src.offset(t.offsetX, t.offsetY);\n            mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);\n            src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY);\n            canvas.drawTexture(t, src, dest);\n        }\n    }\n\n    // Draws a sub region of this texture on to the specified rectangle.\n    @Override\n    public void draw(GLCanvas canvas, RectF source, RectF target) {\n        RectF src = mSrcRect;\n        RectF dest = mDestRect;\n        float x0 = source.left;\n        float y0 = source.top;\n        float x = target.left;\n        float y = target.top;\n        float scaleX = target.width() / source.width();\n        float scaleY = target.height() / source.height();\n\n        syncFrame();\n        for (Tile t : mTiles) {\n            src.set(0, 0, t.contentWidth, t.contentHeight);\n            src.offset(t.offsetX, t.offsetY);\n            if (!src.intersect(source)) {\n                continue;\n            }\n            mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);\n            src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY);\n            canvas.drawTexture(t, src, dest);\n        }\n    }\n\n    // Draws a mixed color of this texture and a specified color onto the\n    // a rectangle. The used color is: from * (1 - ratio) + to * ratio.\n    public void drawMixed(GLCanvas canvas, int color, float ratio,\n                          int x, int y, int width, int height) {\n        RectF src = mSrcRect;\n        RectF dest = mDestRect;\n        float scaleX = (float) width / mWidth;\n        float scaleY = (float) height / mHeight;\n\n        syncFrame();\n        for (Tile t : mTiles) {\n            src.set(0, 0, t.contentWidth, t.contentHeight);\n            src.offset(t.offsetX, t.offsetY);\n            mapRect(dest, src, 0, 0, x, y, scaleX, scaleY);\n            src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY);\n            canvas.drawMixed(t, color, ratio, src, dest);\n        }\n    }\n\n    public void drawMixed(GLCanvas canvas, int color, float ratio,\n                          RectF source, RectF target) {\n        RectF src = mSrcRect;\n        RectF dest = mDestRect;\n        float x0 = source.left;\n        float y0 = source.top;\n        float x = target.left;\n        float y = target.top;\n        float scaleX = target.width() / source.width();\n        float scaleY = target.height() / source.height();\n\n        syncFrame();\n        for (Tile t : mTiles) {\n            src.set(0, 0, t.contentWidth, t.contentHeight);\n            src.offset(t.offsetX, t.offsetY);\n            if (!src.intersect(source)) {\n                continue;\n            }\n            mapRect(dest, src, x0, y0, x, y, scaleX, scaleY);\n            src.offset(t.borderSize - t.offsetX, t.borderSize - t.offsetY);\n            canvas.drawMixed(t, color, ratio, src, dest);\n        }\n    }\n\n    @Override\n    public boolean isOpaque() {\n        return mOpaque;\n    }\n\n    public boolean isReady() {\n        return mUploadIndex == mTiles.length;\n    }\n\n    public void recycle() {\n        mRunning.lazySet(false);\n\n        for (Tile mTile : mTiles) {\n            mTile.free();\n        }\n\n        boolean releaseNow;\n\n        synchronized (mImage) {\n            if (!mImageBusy) {\n                releaseNow = true;\n                mImageBusy = true;\n            } else {\n                releaseNow = false;\n                mNeedRelease.lazySet(true);\n            }\n        }\n\n        if (releaseNow) {\n            if (!mReleased.get()) {\n                mImage.release();\n                mReleased.lazySet(true);\n            }\n            synchronized (mImage) {\n                mImageBusy = false;\n            }\n        }\n    }\n\n    @IntDef({TILE_SMALL, TILE_LARGE})\n    @Retention(RetentionPolicy.SOURCE)\n    private @interface TileType {\n    }\n\n    public interface Callback {\n        void invalidateImageTexture(ImageTexture who);\n    }\n\n    public static class Uploader implements GLRoot.OnGLIdleListener {\n        private final ArrayDeque<ImageTexture> mTextures =\n                new ArrayDeque<>(INIT_CAPACITY);\n\n        private final GLRoot mGlRoot;\n        private boolean mIsQueued = false;\n\n        public Uploader(GLRoot glRoot) {\n            mGlRoot = glRoot;\n        }\n\n        public synchronized void clear() {\n            mTextures.clear();\n        }\n\n        public synchronized void addTexture(ImageTexture t) {\n            if (t.isReady()) return;\n            mTextures.addLast(t);\n\n            if (mIsQueued) return;\n            mIsQueued = true;\n            mGlRoot.addOnGLIdleListener(this);\n        }\n\n        @Override\n        public boolean onGLIdle(GLCanvas canvas, boolean renderRequested) {\n            ArrayDeque<ImageTexture> deque = mTextures;\n            synchronized (this) {\n                long now = SystemClock.uptimeMillis();\n                long dueTime = now + UPLOAD_TILE_LIMIT;\n                while (now < dueTime && !deque.isEmpty()) {\n                    ImageTexture t = deque.peekFirst();\n                    if (t != null && t.uploadNextTile(canvas)) {\n                        deque.removeFirst();\n                        mGlRoot.requestRender();\n                    }\n                    now = SystemClock.uptimeMillis();\n                }\n                mIsQueued = !mTextures.isEmpty();\n\n                // return true to keep this listener in the queue\n                return mIsQueued;\n            }\n        }\n    }\n\n    private static class Tile extends NativeTexture {\n        public int offsetX;\n        public int offsetY;\n        public ImageWrapper image;\n        public Tile nextFreeTile;\n        public int contentWidth;\n        public int contentHeight;\n        public int borderSize;\n        @TileType\n        private int mTileType;\n\n        private static void freeSmallTile(Tile tile) {\n            tile.invalidate();\n            synchronized (sFreeTileLock) {\n                tile.nextFreeTile = sSmallFreeTileHead;\n                sSmallFreeTileHead = tile;\n            }\n        }\n\n        private static void freeLargeTile(Tile tile) {\n            tile.invalidate();\n            synchronized (sFreeTileLock) {\n                tile.nextFreeTile = sLargeFreeTileHead;\n                sLargeFreeTileHead = tile;\n            }\n        }\n\n        public void setSize(@TileType int tileType, int width, int height) {\n            mTileType = tileType;\n            int tileSize;\n            if (tileType == TILE_SMALL) {\n                borderSize = SMALL_BORDER_SIZE;\n                tileSize = SMALL_TILE_SIZE;\n            } else if (tileType == TILE_LARGE) {\n                borderSize = LARGE_BORDER_SIZE;\n                tileSize = LARGE_TILE_SIZE;\n            } else {\n                throw new IllegalStateException(\"Not support tile type: \" + tileType);\n            }\n            contentWidth = width;\n            contentHeight = height;\n\n            mWidth = width + 2 * borderSize;\n            mHeight = height + 2 * borderSize;\n            mTextureWidth = tileSize;\n            mTextureHeight = tileSize;\n        }\n\n        @Override\n        protected void texImage(boolean init) {\n            if (image != null && !image.isRecycled()) {\n                int w, h;\n                if (init) {\n                    w = mTextureWidth;\n                    h = mTextureHeight;\n                } else {\n                    w = mWidth;\n                    h = mHeight;\n                }\n                image.texImage(init, offsetX - borderSize, offsetY - borderSize, w, h);\n            }\n        }\n\n        private void invalidate() {\n            invalidateContent();\n            image = null;\n        }\n\n        public void free() {\n            switch (mTileType) {\n                case TILE_SMALL:\n                    freeSmallTile(this);\n                    break;\n                case TILE_LARGE:\n                    freeLargeTile(this);\n                    break;\n                default:\n                    throw new IllegalStateException(\"Not support tile type: \" + mTileType);\n            }\n        }\n    }\n\n    private class AnimateRunnable implements Runnable {\n        @Override\n        public void run() {\n            if (!prepareAnimation()) return;\n\n            if (mRequestAnimation.get()) {\n                mRunning.lazySet(true);\n            }\n\n            runAnimationLoop();\n\n            if (mNeedRelease.get()) {\n                performReleaseIfNeeded();\n            }\n        }\n\n        private boolean prepareAnimation() {\n            synchronized (mImage) {\n                if (mReleased.get() || mImage.isImageRecycled() || mImageBusy || mNeedRelease.get()) {\n                    mAnimateRunnable = null;\n                    return false;\n                }\n                mImageBusy = true;\n            }\n            synchronized (mImage) {\n                mImageBusy = false;\n                if (mNeedRelease.get() || !mImage.getAnimated()) {\n                    mAnimateRunnable = null;\n                    return false;\n                }\n            }\n            return true;\n        }\n\n        private void runAnimationLoop() {\n            long nextFrameTime = System.nanoTime();\n            long delay = mImage.getDelay();\n\n            while (true) {\n                if (!shouldContinueAnimation()) {\n                    mAnimateRunnable = null;\n                    return;\n                }\n\n                setImageBusy(true);\n\n                mImage.start();\n                mFrameDirty.lazySet(true);\n                invalidateSelf();\n\n                setImageBusy(false);\n\n                nextFrameTime += delay * 1000000;\n                long sleepTimeMs = (nextFrameTime - System.nanoTime()) / 1000000;\n                if (sleepTimeMs > 0) {\n                    try {\n                        //noinspection BusyWait\n                        Thread.sleep(sleepTimeMs);\n                    } catch (InterruptedException ignored) {\n                    }\n                } else {\n                    nextFrameTime = System.nanoTime();\n                }\n            }\n        }\n\n        private boolean shouldContinueAnimation() {\n            synchronized (mImage) {\n                return !(mReleased.get() || mImage.isImageRecycled() || mImageBusy || mNeedRelease.get()) && mRunning.get();\n            }\n        }\n\n        private void setImageBusy(boolean busy) {\n            synchronized (mImage) {\n                mImageBusy = busy;\n            }\n        }\n\n        private void performReleaseIfNeeded() {\n            while (mNeedRelease.get()) {\n                synchronized (mImage) {\n                    if (mReleased.get() || mImage.isImageRecycled() || mImageBusy) {\n                        break;\n                    }\n                    mImageBusy = true;\n                }\n                if (!mReleased.get()) {\n                    mImage.release();\n                    mReleased.lazySet(true);\n                }\n                setImageBusy(false);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/image/ImageWrapper.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.image;\n\nimport android.graphics.Rect;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.image.Image;\n\n/**\n * A wrapper for {@link Image}. It is useful for multi-usage.\n * It handles image recycle automatically.\n */\npublic class ImageWrapper {\n    private static final String LOG_TAG = \"ImageWrapper\";\n\n    private final Image mImage;\n    private final Rect mCut;\n    private int mReferences;\n\n    /**\n     * Create ImageWrapper\n     *\n     * @param image the image should not be obtained or recycled.\n     */\n    public ImageWrapper(@NonNull Image image) {\n        mImage = image;\n        mCut = new Rect(0, 0, image.getWidth(), image.getHeight());\n    }\n\n    /**\n     * Cuts this image to a specified region.\n     * If the region is out of the image size, clamp the region.\n     */\n    public void setCutRect(int left, int top, int right, int bottom) {\n        mCut.left = Math.max(0, left);\n        mCut.top = Math.max(0, top);\n        mCut.right = Math.min(mImage.getWidth(), right);\n        mCut.bottom = Math.min(mImage.getHeight(), bottom);\n\n        if (mCut.isEmpty()) {\n            // Empty mCut has unspecified behavior\n            Log.e(LOG_TAG, \"Cut rect is empty\");\n            mCut.set(0, 0, mImage.getWidth(), mImage.getHeight());\n        }\n    }\n\n    /**\n     * Cuts this image to a specified region.\n     * The region is described in percent, {@code [0.0f, 1.0f]}.\n     * If the region is out of the image size, clamp the region.\n     */\n    public void setCutPercent(float left, float top, float right, float bottom) {\n        setCutRect((int) (getWidth() * left), (int) (getHeight() * top),\n                (int) (getWidth() * right), (int) (getHeight() * bottom));\n    }\n\n    /**\n     * Obtain the image\n     *\n     * @return false for the image is recycled and obtain failed\n     */\n    public synchronized boolean obtain() {\n        if (mImage.isRecycled()) {\n            return false;\n        } else {\n            ++mReferences;\n            return true;\n        }\n    }\n\n    /**\n     * Release the image\n     */\n    public synchronized void release() {\n        --mReferences;\n        if (mReferences <= 0 && !mImage.isRecycled()) {\n            mImage.recycle();\n        }\n    }\n\n    public boolean isImageRecycled() {\n        return mImage.isRecycled();\n    }\n\n    /**\n     * @see Image#getAnimated()\n     */\n    public Boolean getAnimated() {\n        return mImage.getAnimated();\n    }\n\n    /**\n     * @see Image#getAnimated()\n     */\n    public int getWidth() {\n        return mCut.width();\n    }\n\n    /**\n     * @see Image#getHeight()\n     */\n    public int getHeight() {\n        return mCut.height();\n    }\n\n    /**\n     * @see Image#setFrameUpdateAllowed(boolean)\n     */\n    public void setFrameUpdateAllowed(boolean allowed) {\n        mImage.setFrameUpdateAllowed(allowed);\n    }\n\n    /**\n     * @see Image#texImage(boolean, int, int, int, int)\n     */\n    public void texImage(boolean init, int offsetX, int offsetY, int width, int height) {\n        mImage.texImage(init, offsetX + mCut.left, offsetY + mCut.top, width, height);\n    }\n\n    /**\n     * @see Image#start()\n     */\n    public void start() {\n        mImage.start();\n    }\n\n    /**\n     * @see Image#getDelay()\n     */\n    public int getDelay() {\n        return mImage.getDelay();\n    }\n\n    /**\n     * @see Image#isOpaque()\n     */\n    public boolean isOpaque() {\n        return mImage.isOpaque();\n    }\n\n    /**\n     * @see Image#isRecycled()\n     */\n    public boolean isRecycled() {\n        return mImage.isRecycled();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/util/GalleryUtils.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.util;\n\nimport android.graphics.Color;\n\nimport com.hippo.yorozuya.AssertError;\n\nimport java.util.List;\nimport java.util.concurrent.CopyOnWriteArrayList;\n\npublic class GalleryUtils {\n    private static final List<Integer> sRenderThreads = new CopyOnWriteArrayList<>();\n\n    public static float[] intColorToFloatARGBArray(int from) {\n        return new float[]{\n                Color.alpha(from) / 255f,\n                Color.red(from) / 255f,\n                Color.green(from) / 255f,\n                Color.blue(from) / 255f\n        };\n    }\n\n    // Below are used the detect using database in the render thread. It only\n    // works most of the time, but that's ok because it's for debugging only.\n\n    public static int floatARGBArrayTointColor(float[] from) {\n        return Color.argb((int) (from[0] * 255), (int) (from[1] * 255), (int) (from[2] * 255), (int) (from[3] * 255));\n    }\n\n    public static void setRenderThread() {\n        sRenderThreads.add(Thread.currentThread().hashCode());\n    }\n\n    public static boolean isRenderThread() {\n        return sRenderThreads.contains(Thread.currentThread().hashCode());\n    }\n\n    public static void assertInRenderThread() {\n        if (!isRenderThread()) {\n            throw new AssertError(\"Should not do this in non-render thread\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/AnimationTime.java",
    "content": "/*\n * Copyright (C) 2012 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\npackage com.hippo.glview.view;\n\nimport android.os.SystemClock;\n\n//\n// The animation time should ideally be the vsync time the frame will be\n// displayed, but that is an unknown time in the future. So we use the system\n// time just after eglSwapBuffers (when GLSurfaceView.onDrawFrame is called)\n// as a approximation.\n//\npublic class AnimationTime {\n    private static volatile long sTime;\n\n    // Sets current time as the animation time.\n    public static void update() {\n        sTime = SystemClock.uptimeMillis();\n    }\n\n    // Returns the animation time.\n    public static long get() {\n        return sTime;\n    }\n\n    public static long startTime() {\n        sTime = SystemClock.uptimeMillis();\n        return sTime;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/GLRoot.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.view;\n\nimport android.content.Context;\nimport android.graphics.Matrix;\n\nimport com.hippo.glview.anim.CanvasAnimation;\nimport com.hippo.glview.glrenderer.GLCanvas;\n\npublic interface GLRoot {\n    void addOnGLIdleListener(OnGLIdleListener listener);\n\n    void registerLaunchedAnimation(CanvasAnimation animation);\n\n    void requestRenderForced();\n\n    void requestRender();\n\n    void requestLayoutContentPane();\n\n    void lockRenderThread();\n\n    void unlockRenderThread();\n\n    void setContentPane(GLView content);\n\n    void setOrientationSource(OrientationSource source);\n\n    int getDisplayRotation();\n\n    int getCompensation();\n\n    Matrix getCompensationMatrix();\n\n    void freeze();\n\n    void unfreeze();\n\n    void setLightsOutMode(boolean enabled);\n\n    Context getContext();\n\n    int getWidth();\n\n    int getHeight();\n\n    // Listener will be called when GL is idle AND before each frame.\n    // Mainly used for uploading textures.\n    interface OnGLIdleListener {\n        boolean onGLIdle(GLCanvas canvas, boolean renderRequested);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/GLRootView.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.view;\n\nimport android.content.Context;\nimport android.graphics.Matrix;\nimport android.graphics.PixelFormat;\nimport android.opengl.GLSurfaceView;\nimport android.opengl.GLUtils;\nimport android.os.Parcelable;\nimport android.os.Process;\nimport android.os.SystemClock;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.util.SparseArray;\nimport android.view.MotionEvent;\nimport android.view.SurfaceHolder;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.glview.anim.CanvasAnimation;\nimport com.hippo.glview.glrenderer.BasicTexture;\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.glrenderer.GLES11Canvas;\nimport com.hippo.glview.glrenderer.GLES20Canvas;\nimport com.hippo.glview.glrenderer.UploadedTexture;\nimport com.hippo.glview.util.GalleryUtils;\nimport com.hippo.yorozuya.AssertUtils;\n\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.ReentrantLock;\n\nimport javax.microedition.khronos.egl.EGL10;\nimport javax.microedition.khronos.egl.EGLConfig;\nimport javax.microedition.khronos.egl.EGLContext;\nimport javax.microedition.khronos.egl.EGLDisplay;\nimport javax.microedition.khronos.opengles.GL10;\nimport javax.microedition.khronos.opengles.GL11;\n\n// TODO Call attachToRoot and detachFromRoot in render thread\n// The root component of all <code>GLView</code>s. The rendering is done in GL\n// thread while the event handling is done in the main thread.  To synchronize\n// the two threads, the entry points of this package need to synchronize on the\n// <code>GLRootView</code> instance unless it can be proved that the rendering\n// thread won't access the same thing as the method. The entry points include:\n// (1) The public methods of HeadUpDisplay\n// (2) The public methods of CameraHeadUpDisplay\n// (3) The overridden methods in GLRootView.\npublic class GLRootView extends GLSurfaceView\n        implements GLRoot {\n    private static final String TAG = \"GLRootView\";\n\n    private static final boolean DEBUG_FPS = false;\n    private static final boolean DEBUG_INVALIDATE = false;\n    private static final boolean DEBUG_DRAWING_STAT = false;\n    private static final int FLAG_INITIALIZED = 1;\n    private static final int FLAG_NEED_LAYOUT = 2;\n    // mCompensationMatrix maps the coordinates of touch events. It is kept sync\n    // with mCompensation.\n    private final Matrix mCompensationMatrix = new Matrix();\n    private final ArrayList<CanvasAnimation> mAnimations =\n            new ArrayList<>();\n    private final ArrayDeque<OnGLIdleListener> mIdleListeners =\n            new ArrayDeque<>();\n    private final IdleRunner mIdleRunner = new IdleRunner();\n    private final ReentrantLock mRenderLock = new ReentrantLock();\n    private final Condition mFreezeCondition =\n            mRenderLock.newCondition();\n    private final Runnable mRequestRenderOnAnimationFrame = this::superRequestRender;\n    private int mFrameCount = 0;\n    private long mFrameCountingStart = 0;\n    private int mInvalidateColor = 0;\n    private GL11 mGL;\n    private GLCanvas mCanvas;\n    private GLView mContentView;\n    private OrientationSource mOrientationSource;\n    // mCompensation is the difference between the UI orientation on GLCanvas\n    // and the framework orientation. See OrientationManager for details.\n    private int mCompensation;\n    private int mDisplayRotation;\n    private int mFlags = FLAG_NEED_LAYOUT;\n    private volatile boolean mRenderRequested = false;\n    private boolean mFreeze;\n    private boolean mInDownState = false;\n    private int mEGLContextClientVersion;\n\n    public GLRootView(Context context) {\n        this(context, null);\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    public GLRootView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        mFlags |= FLAG_INITIALIZED;\n        setBackgroundDrawable(null);\n\n        setEGLConfigChooser(new ConfigChooser());\n        setEGLContextFactory(new ContextFactory());\n        setRenderer(new GLRootRenderer());\n        getHolder().setFormat(PixelFormat.RGB_888);\n\n        // Uncomment this to enable gl error check.\n        // setDebugFlags(DEBUG_CHECK_GL_ERROR);\n    }\n\n    @Override\n    public void registerLaunchedAnimation(CanvasAnimation animation) {\n        // Register the newly launched animation so that we can set the start\n        // time more precisely. (Usually, it takes much longer for first\n        // rendering, so we set the animation start time as the time we\n        // complete rendering)\n        mAnimations.add(animation);\n    }\n\n    @Override\n    public void addOnGLIdleListener(OnGLIdleListener listener) {\n        synchronized (mIdleListeners) {\n            mIdleListeners.addLast(listener);\n            if (mCanvas != null) {\n                // Wait for onSurfaceCreated\n                mIdleRunner.enable();\n            }\n        }\n    }\n\n    @Override\n    public void setContentPane(GLView content) {\n        if (mContentView == content) return;\n        if (mContentView != null) {\n            if (mInDownState) {\n                long now = SystemClock.uptimeMillis();\n                MotionEvent cancelEvent = MotionEvent.obtain(\n                        now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);\n                mContentView.dispatchTouchEvent(cancelEvent);\n                cancelEvent.recycle();\n                mInDownState = false;\n            }\n            mContentView.detachFromRoot();\n            BasicTexture.yieldAllTextures();\n        }\n        mContentView = content;\n        if (content != null) {\n            content.attachToRoot(this);\n            requestLayoutContentPane();\n        }\n    }\n\n    @Override\n    public void requestRenderForced() {\n        superRequestRender();\n    }\n\n    @Override\n    public void requestRender() {\n        if (DEBUG_INVALIDATE) {\n            StackTraceElement e = Thread.currentThread().getStackTrace()[4];\n            String caller = e.getFileName() + \":\" + e.getLineNumber() + \" \";\n            Log.d(TAG, \"invalidate: \" + caller);\n        }\n        if (mRenderRequested) return;\n        mRenderRequested = true;\n        postOnAnimation(mRequestRenderOnAnimationFrame);\n    }\n\n    private void superRequestRender() {\n        super.requestRender();\n    }\n\n    @Override\n    public void requestLayoutContentPane() {\n        mRenderLock.lock();\n        try {\n            if (mContentView == null || (mFlags & FLAG_NEED_LAYOUT) != 0) return;\n\n            // \"View\" system will invoke onLayout() for initialization(bug ?), we\n            // have to ignore it since the GLThread is not ready yet.\n            if ((mFlags & FLAG_INITIALIZED) == 0) return;\n\n            mFlags |= FLAG_NEED_LAYOUT;\n            requestRender();\n        } finally {\n            mRenderLock.unlock();\n        }\n    }\n\n    private void layoutContentPane() {\n        mFlags &= ~FLAG_NEED_LAYOUT;\n\n        int w = getWidth();\n        int h = getHeight();\n        int displayRotation;\n        int compensation;\n\n        // Get the new orientation values\n        if (mOrientationSource != null) {\n            displayRotation = mOrientationSource.getDisplayRotation();\n            compensation = mOrientationSource.getCompensation();\n        } else {\n            displayRotation = 0;\n            compensation = 0;\n        }\n\n        if (mCompensation != compensation) {\n            mCompensation = compensation;\n            if (mCompensation % 180 != 0) {\n                mCompensationMatrix.setRotate(mCompensation);\n                // move center to origin before rotation\n                mCompensationMatrix.preTranslate((float) -w / 2, (float) -h / 2);\n                // align with the new origin after rotation\n                mCompensationMatrix.postTranslate((float) h / 2, (float) w / 2);\n            } else {\n                mCompensationMatrix.setRotate(mCompensation, (float) w / 2, (float) h / 2);\n            }\n        }\n        mDisplayRotation = displayRotation;\n\n        // Do the actual layout.\n        if (mCompensation % 180 != 0) {\n            int tmp = w;\n            w = h;\n            h = tmp;\n        }\n        Log.i(TAG, \"layout content pane \" + w + \"x\" + h + \" (compensation \" + mCompensation + \")\");\n        if (mContentView != null && w != 0 && h != 0) {\n            mContentView.measure(GLView.MeasureSpec.makeMeasureSpec(w, GLView.MeasureSpec.EXACTLY),\n                    GLView.MeasureSpec.makeMeasureSpec(h, GLView.MeasureSpec.EXACTLY));\n            mContentView.layout(0, 0, w, h);\n        }\n        // Uncomment this to dump the view hierarchy.\n        //mContentView.dumpTree(\"\");\n    }\n\n    @Override\n    protected void onLayout(\n            boolean changed, int left, int top, int right, int bottom) {\n        if (changed) requestLayoutContentPane();\n    }\n\n    private void outputFps() {\n        long now = System.nanoTime();\n        if (mFrameCountingStart == 0) {\n            mFrameCountingStart = now;\n        } else if ((now - mFrameCountingStart) > 1000000000) {\n            Log.d(TAG, \"fps: \" + (double) mFrameCount\n                    * 1000000000 / (now - mFrameCountingStart));\n            mFrameCountingStart = now;\n            mFrameCount = 0;\n        }\n        ++mFrameCount;\n    }\n\n    private void onDrawFrameLocked() {\n        if (DEBUG_FPS) outputFps();\n        // release the unbound textures and deleted buffers.\n        mCanvas.deleteRecycledResources();\n\n        // reset texture upload limit\n        UploadedTexture.resetUploadLimit();\n\n        mRenderRequested = false;\n\n        if ((mOrientationSource != null\n                && mDisplayRotation != mOrientationSource.getDisplayRotation())\n                || (mFlags & FLAG_NEED_LAYOUT) != 0) {\n            layoutContentPane();\n        }\n\n        mCanvas.save(GLCanvas.SAVE_FLAG_ALL);\n        rotateCanvas(-mCompensation);\n        if (mContentView != null) {\n            mContentView.render(mCanvas);\n        } else {\n            // Make sure we always draw something to prevent displaying garbage\n            mCanvas.clearBuffer();\n        }\n        mCanvas.restore();\n\n        if (!mAnimations.isEmpty()) {\n            long now = AnimationTime.get();\n            for (int i = 0, n = mAnimations.size(); i < n; i++) {\n                mAnimations.get(i).setStartTime(now);\n            }\n            mAnimations.clear();\n        }\n\n        if (UploadedTexture.uploadLimitReached()) {\n            requestRender();\n        }\n\n        synchronized (mIdleListeners) {\n            if (!mIdleListeners.isEmpty()) mIdleRunner.enable();\n        }\n\n        if (DEBUG_INVALIDATE) {\n            mCanvas.fillRect(10, 10, 5, 5, mInvalidateColor);\n            mInvalidateColor = ~mInvalidateColor;\n        }\n\n        if (DEBUG_DRAWING_STAT) {\n            mCanvas.dumpStatisticsAndClear();\n        }\n    }\n\n    private void rotateCanvas(int degrees) {\n        if (degrees == 0) return;\n        int w = getWidth();\n        int h = getHeight();\n        int cx = w / 2;\n        int cy = h / 2;\n        mCanvas.translate(cx, cy);\n        mCanvas.rotate(degrees, 0, 0, 1);\n        if (degrees % 180 != 0) {\n            mCanvas.translate(-cy, -cx);\n        } else {\n            mCanvas.translate(-cx, -cy);\n        }\n    }\n\n    @Override\n    public boolean dispatchTouchEvent(MotionEvent event) {\n        if (!isEnabled()) return false;\n\n        int action = event.getAction();\n        if (action == MotionEvent.ACTION_CANCEL\n                || action == MotionEvent.ACTION_UP) {\n            mInDownState = false;\n        } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) {\n            return false;\n        }\n\n        if (mCompensation != 0) {\n            event = MotionEvent.obtain(event);\n            event.transform(mCompensationMatrix);\n        }\n\n        mRenderLock.lock();\n        try {\n            // If this has been detached from root, we don't need to handle event\n            boolean handled = mContentView != null\n                    && mContentView.dispatchTouchEvent(event);\n            if (action == MotionEvent.ACTION_DOWN && handled) {\n                mInDownState = true;\n            }\n            return handled;\n        } finally {\n            mRenderLock.unlock();\n        }\n    }\n\n    @Override\n    public void lockRenderThread() {\n        mRenderLock.lock();\n    }\n\n    @Override\n    public void unlockRenderThread() {\n        mRenderLock.unlock();\n    }\n\n    @Override\n    public void onPause() {\n        unfreeze();\n        if (mContentView != null) {\n            mContentView.pause();\n        }\n        super.onPause();\n    }\n\n    @Override\n    public void onResume() {\n        if (mContentView != null) {\n            mContentView.resume();\n        }\n        super.onResume();\n    }\n\n    @Override\n    public void setOrientationSource(OrientationSource source) {\n        mOrientationSource = source;\n    }\n\n    @Override\n    public int getDisplayRotation() {\n        return mDisplayRotation;\n    }\n\n    @Override\n    public int getCompensation() {\n        return mCompensation;\n    }\n\n    @Override\n    public Matrix getCompensationMatrix() {\n        return mCompensationMatrix;\n    }\n\n    @Override\n    public void freeze() {\n        mRenderLock.lock();\n        mFreeze = true;\n        mRenderLock.unlock();\n    }\n\n    @Override\n    public void unfreeze() {\n        mRenderLock.lock();\n        mFreeze = false;\n        mFreezeCondition.signalAll();\n        mRenderLock.unlock();\n    }\n\n    @Override\n    public void setLightsOutMode(boolean enabled) {\n        int flags = 0;\n        if (enabled) {\n            flags = SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_STABLE;\n        }\n        setSystemUiVisibility(flags);\n    }\n\n    // We need to unfreeze in the following methods and in onPause().\n    // These methods will wait on GLThread. If we have freezed the GLRootView,\n    // the GLThread will wait on main thread to call unfreeze and cause dead\n    // lock.\n    @Override\n    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {\n        unfreeze();\n        super.surfaceChanged(holder, format, w, h);\n    }\n\n    @Override\n    public void surfaceCreated(SurfaceHolder holder) {\n        unfreeze();\n        super.surfaceCreated(holder);\n    }\n\n    @Override\n    public void surfaceDestroyed(SurfaceHolder holder) {\n        unfreeze();\n        super.surfaceDestroyed(holder);\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        unfreeze();\n        super.onDetachedFromWindow();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    protected void dispatchSaveInstanceState(@NonNull SparseArray<Parcelable> container) {\n        super.dispatchSaveInstanceState(container);\n        if (mContentView != null) {\n            mContentView.saveHierarchyState(container);\n        }\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    protected void dispatchRestoreInstanceState(@NonNull SparseArray<Parcelable> container) {\n        super.dispatchRestoreInstanceState(container);\n        if (mContentView != null) {\n            mContentView.restoreHierarchyState(container);\n        }\n    }\n\n    @Override\n    protected void finalize() throws Throwable {\n        try {\n            unfreeze();\n        } finally {\n            super.finalize();\n        }\n    }\n\n    private class GLRootRenderer implements GLSurfaceView.Renderer {\n        /**\n         * Called when the context is created, possibly after automatic destruction.\n         */\n        @Override\n        public void onSurfaceCreated(GL10 gl1, EGLConfig config) {\n            GL11 gl = (GL11) gl1;\n            if (mGL != null) {\n                // The GL Object has changed\n                Log.i(TAG, \"GLObject has changed from \" + mGL + \" to \" + gl);\n            }\n            mRenderLock.lock();\n            try {\n                mGL = gl;\n                mCanvas = mEGLContextClientVersion == 2 ? new GLES20Canvas() : new GLES11Canvas(gl);\n                BasicTexture.invalidateAllTextures();\n            } finally {\n                mRenderLock.unlock();\n            }\n\n            if (DEBUG_FPS) {\n                setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);\n            } else {\n                setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);\n            }\n        }\n\n        /**\n         * Called when the OpenGL surface is recreated without destroying the\n         * context.\n         */\n        @Override\n        public void onSurfaceChanged(GL10 gl1, int width, int height) {\n            Log.i(TAG, \"onSurfaceChanged: \" + width + \"x\" + height + \", gl10: \" + gl1.toString());\n            Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);\n            GalleryUtils.setRenderThread();\n            GL11 gl = (GL11) gl1;\n            AssertUtils.assertTrue(mGL == gl);\n\n            mCanvas.setSize(width, height);\n        }\n\n        @Override\n        public void onDrawFrame(GL10 gl) {\n            AnimationTime.update();\n\n            long t0 = System.nanoTime();\n\n            mRenderLock.lock();\n\n            while (mFreeze) {\n                mFreezeCondition.awaitUninterruptibly();\n            }\n\n            try {\n                onDrawFrameLocked();\n            } finally {\n                mRenderLock.unlock();\n            }\n\n            long t = System.nanoTime();\n            long duration = (t - t0) / 1000000;\n\n            if (duration > 100) {\n                Log.v(TAG, \"--- \" + duration + \" ---\");\n            }\n        }\n    }\n\n    private class IdleRunner implements Runnable {\n        // true if the idle runner is in the queue\n        private boolean mActive = false;\n\n        @Override\n        public void run() {\n            OnGLIdleListener listener;\n            synchronized (mIdleListeners) {\n                mActive = false;\n                if (mIdleListeners.isEmpty()) return;\n                listener = mIdleListeners.removeFirst();\n            }\n            mRenderLock.lock();\n            boolean keepInQueue;\n            try {\n                keepInQueue = listener.onGLIdle(mCanvas, mRenderRequested);\n            } finally {\n                mRenderLock.unlock();\n            }\n            synchronized (mIdleListeners) {\n                if (keepInQueue) mIdleListeners.addLast(listener);\n                if (!mRenderRequested && !mIdleListeners.isEmpty()) enable();\n            }\n        }\n\n        public void enable() {\n            // Who gets the flag can add it to the queue\n            if (mActive) return;\n            mActive = true;\n            queueEvent(this);\n        }\n    }\n\n    // Always chose a config\n    private class ConfigChooser implements EGLConfigChooser {\n        private final int[] mValue = new int[1];\n\n        @Override\n        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {\n            int[] num_config = new int[1];\n            int[] configSpec = new int[]{EGL10.EGL_NONE};\n            if (!egl.eglChooseConfig(display, configSpec, null, 0, num_config)) {\n                throw new IllegalArgumentException(\"eglChooseConfig failed\");\n            }\n\n            int numConfigs = num_config[0];\n\n            if (numConfigs <= 0) {\n                throw new IllegalArgumentException(\n                        \"No configs match configSpec\");\n            }\n\n            EGLConfig[] configs = new EGLConfig[numConfigs];\n            if (!egl.eglChooseConfig(display, configSpec, configs, numConfigs, num_config)) {\n                throw new IllegalArgumentException(\"eglChooseConfig#2 failed\");\n            }\n            EGLConfig config = chooseConfig(egl, display, configs);\n            if (config == null) {\n                throw new IllegalArgumentException(\"No config chosen\");\n            }\n\n            int renderableType = findConfigAttrib(egl, display, config, EGL10.EGL_RENDERABLE_TYPE);\n            if ((renderableType & 0x0004) == 0x0004 /* EGL_OPENGL_ES2_BIT */) {\n                mEGLContextClientVersion = 2;\n            } else {\n                mEGLContextClientVersion = 1;\n            }\n\n            return config;\n        }\n\n        private EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) {\n            // Use score to avoid \"No config chosen\"\n            int configIndex = 0;\n            int maxScore = 0;\n\n            for (int i = 0, n = configs.length; i < n; i++) {\n                final EGLConfig config = configs[i];\n                final int redSize = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_RED_SIZE);\n                final int greenSize = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_GREEN_SIZE);\n                final int blueSize = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_BLUE_SIZE);\n                final int alphaSize = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_ALPHA_SIZE);\n                final int sampleBuffers = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_SAMPLE_BUFFERS);\n                final int samples = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_SAMPLES);\n                final int depth = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_DEPTH_SIZE);\n                final int stencil = findConfigAttrib(egl, display, config,\n                        EGL10.EGL_STENCIL_SIZE);\n\n                final int score = redSize + greenSize + blueSize + alphaSize +\n                        sampleBuffers + samples - depth - stencil;\n\n                if (score > maxScore) {\n                    maxScore = score;\n                    configIndex = i;\n                }\n            }\n\n            return configs[configIndex];\n        }\n\n        private int findConfigAttrib(EGL10 egl, EGLDisplay display,\n                                     EGLConfig config, int attribute) {\n            if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {\n                return mValue[0];\n            }\n            return 0;\n        }\n    }\n\n    private class ContextFactory implements EGLContextFactory {\n        private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;\n\n        @Override\n        public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) {\n            int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion,\n                    EGL10.EGL_NONE};\n\n            return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT,\n                    mEGLContextClientVersion != 0 ? attrib_list : null);\n        }\n\n        @Override\n        public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {\n            if (!egl.eglDestroyContext(display, context)) {\n                Log.e(\"DefaultContextFactory\", \"display:\" + display + \" context: \" + context);\n                throw new RuntimeException(\"eglDestroyContex failed: \" + GLUtils.getEGLErrorString(egl.eglGetError()));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/GLView.java",
    "content": "/*\n * Copyright (C) 2010 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\npackage com.hippo.glview.view;\n\nimport android.graphics.Color;\nimport android.graphics.Rect;\nimport android.os.Parcelable;\nimport android.os.SystemClock;\nimport android.util.Log;\nimport android.util.SparseArray;\nimport android.view.MotionEvent;\n\nimport androidx.annotation.IntDef;\n\nimport com.hippo.glview.anim.CanvasAnimation;\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.glrenderer.GLPaint;\nimport com.hippo.yorozuya.AssertUtils;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\n\n// GLView is a UI component. It can render to a GLCanvas and accept touch\n// events. A GLView may have zero or more child GLView and they form a tree\n// structure. The rendering and event handling will pass through the tree\n// structure.\n//\n// A GLView tree should be attached to a GLRoot before event dispatching and\n// rendering happens. GLView asks GLRoot to re-render or re-layout the\n// GLView hierarchy using requestRender() and requestLayoutContentPane().\n//\n// The render() method is called in a separate thread. Before calling\n// dispatchTouchEvent() and layout(), GLRoot acquires a lock to avoid the\n// rendering thread running at the same time. If there are other entry points\n// from main thread (like a Handler) in your GLView, you need to call\n// lockRendering() if the rendering thread should not run at the same time.\n//\npublic class GLView implements TouchOwner {\n    public static final int VISIBLE = 0b00000000;\n    public static final int INVISIBLE = 0b00000001;\n    public static final int GONE = 0b00000010;\n    public static final int VISIBILITY_INVALID = 0b00000011;\n    /**\n     * Used to mark a GlView that has no ID.\n     */\n    public static final int NO_ID = -1;\n    protected final static GLPaint mDrawBoundsPaint;\n    private static final String TAG = \"GLView\";\n    private static final boolean DEBUG_DRAW_BOUNDS = false;\n    private static final int FLAG_INVISIBLE = 0b00000011;\n    private static final int FLAG_SET_MEASURED_SIZE = 0b00000100;\n    private static final int FLAG_LAYOUT_REQUESTED = 0b00001000;\n\n    static {\n        mDrawBoundsPaint = new GLPaint();\n        mDrawBoundsPaint.setColor(Color.RED);\n        mDrawBoundsPaint.setLineWidth(2);\n    }\n\n    /**\n     * Position in parent, just like left, top, right, bottom in {@link android.view.View}\n     */\n    protected final Rect mBounds = new Rect();\n    protected final Rect mPaddings = new Rect();\n    /**\n     * Position in root\n     */\n    private final int[] mPositionInRoot = new int[2];\n    private final Rect mTempRect = new Rect();\n    protected GLView mParent;\n    protected int mMeasuredWidth = 0;\n    protected int mMeasuredHeight = 0;\n    protected int mScrollY = 0;\n    protected int mScrollX = 0;\n    private GLRoot mRoot;\n    private ArrayList<GLView> mComponents;\n    private GLView mMotionTarget;\n    private CanvasAnimation mAnimation;\n    private int mViewFlags = 0;\n    private int mLastWidthSpec = -1;\n    private int mLastHeightSpec = -1;\n    /**\n     * The GlView's identifier.\n     *\n     * @see #setId(int)\n     * @see #getId()\n     */\n    private int mID = NO_ID;\n    /**\n     * The minimum height of the view. We'll try our best to have the height\n     * of this view to at least this amount.\n     */\n    private int mMinHeight;\n    /**\n     * The minimum width of the view. We'll try our best to have the width\n     * of this view to at least this amount.\n     */\n    private int mMinWidth;\n    private LayoutParams mLayoutParams;\n    private int mBackgroundColor = Color.TRANSPARENT;\n    private TouchHelper mTouchHelper;\n    private float mHotspotX;\n    private float mHotspotY;\n    private boolean mPressed;\n    private OnClickListener mOnClickListener;\n    private OnLongClickListener mOnLongClickListener;\n\n    /**\n     * Utility to return a default begin. Uses the supplied number as left or top.\n     *\n     * @param size          the parent size\n     * @param specSize      the child size\n     * @param paddingBeign  the padding begin\n     * @param paddingFinish the padding finish\n     * @param position      the {@link Gravity#POSITION_BEGIN} or {@link Gravity#POSITION_CENTER} or\n     *                      {@link Gravity#POSITION_FINISH}\n     * @return the begin size\n     */\n    public static int getDefaultBegin(int size, int specSize, int paddingBeign, int paddingFinish,\n                                      @Gravity.PositionMode int position) {\n        if (position == Gravity.POSITION_FINISH) {\n            return size - paddingFinish - specSize;\n        } else if (position == Gravity.POSITION_CENTER) {\n            return ((size - paddingBeign - paddingFinish) / 2) - (specSize / 2) + paddingBeign;\n        } else {\n            return paddingBeign;\n        }\n    }\n\n    /**\n     * Utility to return a default size. Uses the supplied size if the\n     * MeasureSpec imposed no constraints. Will get larger if allowed\n     * by the MeasureSpec.\n     * <p>\n     * It works differently from {@link android.view.View#getDefaultSize(int, int)}.\n     * For {@link MeasureSpec#AT_MOST}, size is suggestion size\n     * if it is small then spec size or it is not zero,\n     * otherwise spec size.\n     *\n     * @param size        Default size for this view\n     * @param measureSpec Constraints imposed by the parent\n     * @return The size this view should be.\n     */\n    public static int getDefaultSize(int size, int measureSpec) {\n        int result = size;\n        int specMode = MeasureSpec.getMode(measureSpec);\n        int specSize = MeasureSpec.getSize(measureSpec);\n\n        switch (specMode) {\n            case MeasureSpec.UNSPECIFIED:\n                break;\n            case MeasureSpec.EXACTLY:\n                result = specSize;\n                break;\n            case MeasureSpec.AT_MOST:\n                return size == 0 ? specSize : Math.min(size, specSize);\n        }\n        return result;\n    }\n\n    /**\n     * Utility to return a max size. Uses the supplied size if the\n     * MeasureSpec imposed no constraints. Will get larger if allowed\n     * by the MeasureSpec.\n     * <p>\n     * It works the same as {@link android.view.View#getDefaultSize(int, int)}.\n     *\n     * @param size        Default size for this view\n     * @param measureSpec Constraints imposed by the parent\n     * @return The size this view should be.\n     */\n    public static int getMaxSize(int size, int measureSpec) {\n        int result = size;\n        int specMode = MeasureSpec.getMode(measureSpec);\n        int specSize = MeasureSpec.getSize(measureSpec);\n\n        result = switch (specMode) {\n            case MeasureSpec.UNSPECIFIED -> size;\n            case MeasureSpec.EXACTLY, MeasureSpec.AT_MOST -> specSize;\n            default -> result;\n        };\n        return result;\n    }\n\n    public static int getComponentSpec(int spec, int childSize) {\n        int specMode = MeasureSpec.getMode(spec);\n        int specSize = MeasureSpec.getSize(spec);\n\n        int size = Math.max(0, specSize);\n\n        int resultSize = 0;\n        int resultMode = 0;\n\n        switch (specMode) {\n            // Parent has imposed an exact size on us\n            case MeasureSpec.EXACTLY:\n                if (childSize >= 0) {\n                    resultSize = childSize;\n                    resultMode = MeasureSpec.EXACTLY;\n                } else if (childSize == LayoutParams.MATCH_PARENT) {\n                    // Child wants to be our size. So be it.\n                    resultSize = size;\n                    resultMode = MeasureSpec.EXACTLY;\n                } else if (childSize == LayoutParams.WRAP_CONTENT) {\n                    // Child wants to determine its own size. It can't be\n                    // bigger than us.\n                    resultSize = size;\n                    resultMode = MeasureSpec.AT_MOST;\n                }\n                break;\n\n            // Parent has imposed a maximum size on us\n            case MeasureSpec.AT_MOST:\n                if (childSize >= 0) {\n                    // Child wants a specific size... so be it\n                    resultSize = childSize;\n                    resultMode = MeasureSpec.EXACTLY;\n                } else if (childSize == LayoutParams.MATCH_PARENT) {\n                    // Child wants to be our size, but our size is not fixed.\n                    // Constrain child to not be bigger than us.\n                    resultSize = size;\n                    resultMode = MeasureSpec.AT_MOST;\n                } else if (childSize == LayoutParams.WRAP_CONTENT) {\n                    // Child wants to determine its own size. It can't be\n                    // bigger than us.\n                    resultSize = size;\n                    resultMode = MeasureSpec.AT_MOST;\n                }\n                break;\n\n            // Parent asked to see how big we want to be\n            case MeasureSpec.UNSPECIFIED:\n                if (childSize >= 0) {\n                    // Child wants a specific size... let him have it\n                    resultSize = childSize;\n                    resultMode = MeasureSpec.EXACTLY;\n                }\n                break;\n        }\n        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);\n    }\n\n    public static void measureComponent(GLView component, int widthSpec, int heightSpec) {\n        final LayoutParams lp = component.getLayoutParams();\n        final int componentWidthSpec = getComponentSpec(widthSpec, lp.width);\n        final int componentHeightSpec = getComponentSpec(heightSpec, lp.height);\n        component.measure(componentWidthSpec, componentHeightSpec);\n    }\n\n    public static void measureAllComponents(GLView parent, int widthSpec, int heightSpec) {\n        for (int i = 0, n = parent.getComponentCount(); i < n; i++) {\n            GLView component = parent.getComponent(i);\n            if (component.getVisibility() == GONE) {\n                continue;\n            }\n            measureComponent(component, widthSpec, heightSpec);\n        }\n    }\n\n    public void startAnimation(CanvasAnimation animation, boolean atOnce) {\n        GLRoot root = getGLRoot();\n        if (root == null) throw new IllegalStateException();\n        mAnimation = animation;\n        if (mAnimation != null) {\n            mAnimation.start();\n            if (atOnce) {\n                mAnimation.setStartTime(AnimationTime.get());\n            } else {\n                root.registerLaunchedAnimation(mAnimation);\n            }\n        }\n        invalidate();\n    }\n\n    /**\n     * Returns the visibility status for this view.\n     *\n     * @return One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.\n     */\n    @Visibility\n    @SuppressWarnings(\"WrongConstant\")\n    public int getVisibility() {\n        int visibility = mViewFlags & FLAG_INVISIBLE;\n\n        if (visibility == VISIBILITY_INVALID) {\n            visibility = VISIBLE;\n            mViewFlags &= ~FLAG_INVISIBLE;\n            mViewFlags |= visibility;\n        }\n\n        return visibility;\n    }\n\n    /**\n     * Set the enabled state of this view.\n     *\n     * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.\n     */\n    public void setVisibility(@Visibility int visibility) {\n        @Visibility\n        int oldVisibility = getVisibility();\n        if (visibility == oldVisibility) {\n            return;\n        }\n\n        mViewFlags &= ~FLAG_INVISIBLE;\n        mViewFlags |= visibility;\n\n        onVisibilityChanged(this, visibility);\n\n        if (oldVisibility == GLView.GONE || visibility == GLView.GONE) {\n            requestLayout();\n        } else {\n            invalidate();\n        }\n    }\n\n    // This should only be called on the content pane (the topmost GLView).\n    void attachToRoot(GLRoot root) {\n        AssertUtils.assertTrue(mParent == null && mRoot == null);\n        onAttachToRoot(root);\n    }\n\n    // This should only be called on the content pane (the topmost GLView).\n    void detachFromRoot() {\n        AssertUtils.assertTrue(mParent == null && mRoot != null);\n        onDetachFromRoot();\n    }\n\n    // This should only be called on the content pane (the topmost GLView).\n    void pause() {\n        onPause();\n    }\n\n    // This should only be called on the content pane (the topmost GLView).\n    void resume() {\n        onResume();\n    }\n\n    public boolean isAttachedToRoot() {\n        return mRoot != null;\n    }\n\n    // Returns the number of children of the GLView.\n    public int getComponentCount() {\n        return mComponents == null ? 0 : mComponents.size();\n    }\n\n    // Returns the children for the given index.\n    public GLView getComponent(int index) {\n        if (mComponents == null) {\n            throw new ArrayIndexOutOfBoundsException(index);\n        }\n        return mComponents.get(index);\n    }\n\n    // Adds a child to this GLView.\n    public void addComponent(GLView component) {\n        addComponent(component, -1, null);\n    }\n\n    public void addComponent(GLView component, int index) {\n        addComponent(component, index, null);\n    }\n\n    public void addComponent(GLView component, LayoutParams params) {\n        addComponent(component, -1, params);\n    }\n\n    public void addComponent(GLView component, int index, LayoutParams params) {\n        // Make component is not null\n        if (component == null) {\n            throw new IllegalArgumentException(\"Cannot add a null child view to a ViewGroup\");\n        }\n        // Make sure the component doesn't have a parent currently.\n        if (component.mParent != null) {\n            throw new IllegalStateException(\n                    \"Component \" + this + \" being added, but it already has a parent\");\n        }\n\n        // Ensure index is valid\n        if (index < 0) {\n            index = getComponentCount();\n        }\n\n        // Ensure params is valid\n        if (params == null) {\n            params = generateDefaultLayoutParams();\n        } else if (!checkLayoutParams(params)) {\n            params = generateLayoutParams(params);\n        }\n\n        // Build parent-child links\n        if (mComponents == null) {\n            mComponents = new ArrayList<>();\n        }\n\n        mComponents.add(index, component);\n        component.mParent = this;\n        component.setLayoutParams(params);\n\n        // If this is added after we have a root, tell the component.\n        if (mRoot != null) {\n            component.onAttachToRoot(mRoot);\n        }\n    }\n\n    // Removes a child from this GLView.\n    public void removeComponent(GLView component) {\n        if (mComponents == null) return;\n        if (mComponents.remove(component)) {\n            removeOneComponent(component);\n        }\n    }\n\n    public boolean removeComponentAt(int index) {\n        if (mComponents == null) {\n            return false;\n        }\n        if (index >= 0 && index < getComponentCount()) {\n            GLView component = mComponents.remove(index);\n            removeOneComponent(component);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    // Removes all children of this GLView.\n    public void removeAllComponents() {\n        for (int i = 0, n = mComponents.size(); i < n; ++i) {\n            removeOneComponent(mComponents.get(i));\n        }\n        mComponents.clear();\n    }\n\n    private void removeOneComponent(GLView component) {\n        if (mMotionTarget == component) {\n            long now = SystemClock.uptimeMillis();\n            MotionEvent cancelEvent = MotionEvent.obtain(\n                    now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);\n            dispatchTouchEvent(cancelEvent);\n            cancelEvent.recycle();\n        }\n        component.onDetachFromRoot();\n        component.mParent = null;\n    }\n\n    /**\n     * Returns this GlView's identifier.\n     *\n     * @return a positive integer used to identify the view or {@link #NO_ID}\n     * if the view has no ID\n     */\n    public int getId() {\n        return mID;\n    }\n\n    /**\n     * Sets the identifier for this GlView. The identifier does not have to be\n     * unique in this GlView's hierarchy. The identifier should be a positive\n     * number.\n     *\n     * @param id a number used to identify the view\n     */\n    public void setId(int id) {\n        mID = id;\n    }\n\n    public Rect bounds() {\n        return mBounds;\n    }\n\n    @Override\n    public void setHotspot(float x, float y) {\n        mHotspotX = x;\n        mHotspotY = y;\n    }\n\n    public float getHotspotX() {\n        return mHotspotX;\n    }\n\n    public float getHotspotY() {\n        return mHotspotY;\n    }\n\n    @Override\n    public boolean isEnabled() {\n        return true; // TODO\n    }\n\n    @Override\n    public boolean isPressed() {\n        return mPressed;\n    }\n\n    @Override\n    public void setPressed(boolean pressed) {\n        mPressed = pressed;\n    }\n\n    @Override\n    public boolean isClickable() {\n        return mOnClickListener != null;\n    }\n\n    @Override\n    public boolean isLongClickable() {\n        return mOnLongClickListener != null;\n    }\n\n    @Override\n    public void performClick() {\n        if (mOnClickListener != null) {\n            mOnClickListener.onClick(this);\n        } else {\n        }\n    }\n\n    @Override\n    public boolean performLongClick() {\n        if (mOnLongClickListener != null) {\n            return mOnLongClickListener.onLongClick(this);\n        } else {\n            return false;\n        }\n    }\n\n    public void setOnClickListener(OnClickListener listener) {\n        mOnClickListener = listener;\n    }\n\n    public void setOnLongClickListener(OnLongClickListener listener) {\n        mOnLongClickListener = listener;\n    }\n\n    @Override\n    public int getWidth() {\n        return mBounds.right - mBounds.left;\n    }\n\n    @Override\n    public int getHeight() {\n        return mBounds.bottom - mBounds.top;\n    }\n\n    /**\n     * Offset this GLView's vertical location by the specified number of pixels.\n     *\n     * @param offset the number of pixels to offset the view by\n     */\n    public void offsetTopAndBottom(int offset) {\n        if (offset != 0) {\n            mBounds.offset(0, offset);\n            dispatchNotifyPositionInRoot();\n            invalidate();\n        }\n    }\n\n    /**\n     * Offset this GLView's horizontal location by the specified amount of pixels.\n     *\n     * @param offset the number of pixels to offset the view by\n     */\n    public void offsetLeftAndRight(int offset) {\n        if (offset != 0) {\n            mBounds.offset(offset, 0);\n            dispatchNotifyPositionInRoot();\n            invalidate();\n        }\n    }\n\n    public GLRoot getGLRoot() {\n        return mRoot;\n    }\n\n    // Request re-rendering of the view hierarchy.\n    // This is used for animation or when the contents changed.\n    public void invalidate() {\n        GLRoot root = getGLRoot();\n        if (root != null) root.requestRender();\n    }\n\n    // Request re-layout of the view hierarchy.\n    public void requestLayout() {\n        mViewFlags |= FLAG_LAYOUT_REQUESTED;\n        mLastWidthSpec = -1;\n        mLastHeightSpec = -1;\n        if (mParent != null) {\n            mParent.requestLayout();\n        } else {\n            // Is this a content pane ?\n            GLRoot root = getGLRoot();\n            if (root != null) root.requestLayoutContentPane();\n        }\n    }\n\n    public void render(GLCanvas canvas) {\n        // render background color\n        if (Color.alpha(mBackgroundColor) != 0) {\n            canvas.fillRect(0, 0, getWidth(), getHeight(), mBackgroundColor);\n        }\n\n        // render content\n        onRender(canvas);\n\n        // render child\n        canvas.save();\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            renderChild(canvas, getComponent(i));\n        }\n        canvas.restore();\n\n        // render bounds\n        if (DEBUG_DRAW_BOUNDS) {\n            canvas.drawRect(0, 0, getWidth(), getHeight(), mDrawBoundsPaint);\n        }\n    }\n\n    public void onRender(GLCanvas canvas) {\n    }\n\n    public void setBackgroundColor(int color) {\n        mBackgroundColor = color;\n    }\n\n    protected void renderChild(GLCanvas canvas, GLView component) {\n        if (component.getVisibility() != GLView.VISIBLE) {\n            return;\n        }\n\n        getValidRect(mTempRect);\n        if (mTempRect.isEmpty()) {\n            return;\n        }\n\n        int xOffset = component.mBounds.left - mScrollX;\n        int yOffset = component.mBounds.top - mScrollY;\n\n        canvas.translate(xOffset, yOffset);\n\n        CanvasAnimation anim = component.mAnimation;\n        if (anim != null) {\n            canvas.save(anim.getCanvasSaveFlags());\n            if (anim.calculate(AnimationTime.get())) {\n                invalidate();\n            } else {\n                component.mAnimation = null;\n            }\n            anim.apply(canvas);\n        }\n        component.render(canvas);\n        if (anim != null) canvas.restore();\n        canvas.translate(-xOffset, -yOffset);\n    }\n\n    protected boolean onTouch(MotionEvent event) {\n        if (mTouchHelper == null) {\n            mTouchHelper = new TouchHelper(this);\n        }\n        return mTouchHelper.onTouch(event);\n    }\n\n    protected boolean dispatchTouchEvent(MotionEvent event,\n                                         int x, int y, GLView component, boolean checkBounds) {\n        Rect rect = component.mBounds;\n        int left = rect.left;\n        int top = rect.top;\n        if (!checkBounds || rect.contains(x, y)) {\n            event.offsetLocation(-left, -top);\n            if (component.dispatchTouchEvent(event)) {\n                event.offsetLocation(left, top);\n                return true;\n            }\n            event.offsetLocation(left, top);\n        }\n        return false;\n    }\n\n    protected boolean dispatchTouchEvent(MotionEvent event) {\n        int x = (int) event.getX();\n        int y = (int) event.getY();\n        int action = event.getAction();\n        if (mMotionTarget != null) {\n            if (action == MotionEvent.ACTION_DOWN) {\n                MotionEvent cancel = MotionEvent.obtain(event);\n                cancel.setAction(MotionEvent.ACTION_CANCEL);\n                dispatchTouchEvent(cancel, x, y, mMotionTarget, false);\n                mMotionTarget = null;\n            } else {\n                dispatchTouchEvent(event, x, y, mMotionTarget, false);\n                if (action == MotionEvent.ACTION_CANCEL\n                        || action == MotionEvent.ACTION_UP) {\n                    mMotionTarget = null;\n                }\n                return true;\n            }\n        }\n        if (action == MotionEvent.ACTION_DOWN) {\n            // in the reverse rendering order\n            for (int i = getComponentCount() - 1; i >= 0; --i) {\n                GLView component = getComponent(i);\n                if (component.getVisibility() != GLView.VISIBLE) continue;\n                if (dispatchTouchEvent(event, x, y, component, true)) {\n                    mMotionTarget = component;\n                    return true;\n                }\n            }\n        }\n        return onTouch(event);\n    }\n\n    /**\n     * Called by {@link GLRootView#dispatchSaveInstanceState(SparseArray)} to\n     * store this GlView hierarchy's frozen state into the given container.\n     *\n     * @param container The SparseArray in which to save the view's state.\n     */\n    public void saveHierarchyState(SparseArray<Parcelable> container) {\n        dispatchSaveInstanceState(container);\n    }\n\n    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {\n        if (mID != NO_ID) {\n            Parcelable state = onSaveInstanceState();\n            if (state != null) {\n                container.put(mID, state);\n            }\n        }\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).dispatchSaveInstanceState(container);\n        }\n    }\n\n    protected Parcelable onSaveInstanceState() {\n        return null;\n    }\n\n    /**\n     * Called by {@link GLRootView#dispatchRestoreInstanceState(SparseArray)} to\n     * restore this view hierarchy's frozen state from the given container.\n     *\n     * @param container The SparseArray which holds previously frozen states.\n     */\n    public void restoreHierarchyState(SparseArray<Parcelable> container) {\n        dispatchRestoreInstanceState(container);\n    }\n\n    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {\n        if (mID != NO_ID) {\n            Parcelable state = container.get(mID);\n            if (state != null) {\n                onRestoreInstanceState(state);\n            }\n        }\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).dispatchRestoreInstanceState(container);\n        }\n    }\n\n    protected void onRestoreInstanceState(Parcelable state) {\n        if (state != null) {\n            throw new IllegalArgumentException(\"Please override onRestoreInstanceState\");\n        }\n    }\n\n    public void setPaddings(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) {\n        mPaddings.set(paddingLeft, paddingTop, paddingRight, paddingBottom);\n    }\n\n    public void setPaddingLeft(int paddingLeft) {\n        mPaddings.left = paddingLeft;\n    }\n\n    public void setPaddingTop(int paddingTop) {\n        mPaddings.top = paddingTop;\n    }\n\n    public void setPaddingRight(int paddingRight) {\n        mPaddings.right = paddingRight;\n    }\n\n    public void setPaddingBottom(int paddingBottom) {\n        mPaddings.bottom = paddingBottom;\n    }\n\n    public Rect getPaddings() {\n        return mPaddings;\n    }\n\n    // There is a little different between GLView.layout() and View.layout().\n    // onMeasure() is not called in GLView.layout().\n    // For the content view of GLRootView, GLView.measure() is called before GLView.layout().\n    // For component, GLView.measure() in called in parent's GLView.onMeasure().\n    // So it is OK.\n    public void layout(int left, int top, int right, int bottom) {\n        boolean sizeChanged = setBounds(left, top, right, bottom);\n        final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED;\n        if (sizeChanged || forceLayout) {\n            notifyPositionInRoot();\n            onLayout(sizeChanged, left, top, right, bottom);\n        } else {\n            dispatchNotifyPositionInRoot();\n        }\n        mViewFlags &= ~FLAG_LAYOUT_REQUESTED;\n    }\n\n    public void dispatchNotifyPositionInRoot() {\n        if (notifyPositionInRoot()) {\n            for (int i = 0, size = getComponentCount(); i < size; i++) {\n                getComponent(i).dispatchNotifyPositionInRoot();\n            }\n        }\n    }\n\n    public boolean notifyPositionInRoot() {\n        // Update position in root\n        int[] position = mPositionInRoot;\n        int oldX = position[0];\n        int oldY = position[1];\n        if (mParent == null) {\n            position[0] = 0;\n            position[1] = 0;\n        } else {\n            mParent.getPositionInRoot(position);\n            // Apply offset in parent\n            position[0] += mBounds.left;\n            position[1] += mBounds.top;\n        }\n        int x = position[0];\n        int y = position[1];\n        if (x != oldX || y != oldY) {\n            onPositionInRootChanged(x, y, oldX, oldY);\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    private boolean setBounds(int left, int top, int right, int bottom) {\n        Rect bounds = mBounds;\n        int oldW = bounds.right - bounds.left;\n        int oldH = bounds.bottom - bounds.top;\n        int newW = right - left;\n        int newH = bottom - top;\n\n        boolean sizeChanged = oldW != newW || oldH != newH;\n        bounds.set(left, top, right, bottom);\n\n        if (sizeChanged) {\n            onSizeChanged(newW, newH, oldW, oldH);\n        }\n\n        return sizeChanged;\n    }\n\n    protected void onSizeChanged(int newW, int newH, int oldW, int oldH) {\n    }\n\n    protected void onPositionInRootChanged(int x, int y, int oldX, int oldY) {\n    }\n\n    public void getPositionInRoot(int[] position) {\n        AssertUtils.assertEquals(\"position should be 2 length int array\", position.length, 2);\n        position[0] = mPositionInRoot[0];\n        position[1] = mPositionInRoot[1];\n    }\n\n    /**\n     * Get the rect of the view clipped by root rect\n     *\n     * @param rect the result\n     */\n    public void getValidRect(Rect rect) {\n        GLRoot root = mRoot;\n        if (root == null) {\n            rect.setEmpty();\n            return;\n        }\n\n        int x = mPositionInRoot[0];\n        int y = mPositionInRoot[1];\n        rect.set(x, y, x + getWidth(), y + getHeight());\n        if (!rect.intersect(0, 0, root.getWidth(), root.getHeight())) {\n            rect.setEmpty();\n            return;\n        }\n\n        rect.offset(-x, -y);\n    }\n\n    public void measure(int widthSpec, int heightSpec) {\n        final boolean forceLayout = (mViewFlags & FLAG_LAYOUT_REQUESTED) == FLAG_LAYOUT_REQUESTED;\n        final boolean isExactly = MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY &&\n                MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY;\n        final boolean matchingSize = isExactly &&\n                getMeasuredWidth() == MeasureSpec.getSize(widthSpec) &&\n                getMeasuredHeight() == MeasureSpec.getSize(heightSpec);\n        if (forceLayout || !matchingSize &&\n                (widthSpec != mLastWidthSpec ||\n                        heightSpec != mLastHeightSpec)) {\n            mLastWidthSpec = widthSpec;\n            mLastHeightSpec = heightSpec;\n\n            mViewFlags &= ~FLAG_SET_MEASURED_SIZE;\n            onMeasure(widthSpec, heightSpec);\n            if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) {\n                throw new IllegalStateException(getClass().getName()\n                        + \" should call setMeasuredSize() in onMeasure()\");\n            }\n        }\n    }\n\n    protected void onMeasure(int widthSpec, int heightSpec) {\n        setMeasuredSize(getDefaultSize(getSuggestedMinimumWidth(), widthSpec),\n                getDefaultSize(getSuggestedMinimumHeight(), heightSpec));\n    }\n\n    protected int getSuggestedMinimumHeight() {\n        return getMinimumHeight() + mPaddings.top + mPaddings.bottom;\n    }\n\n    protected int getSuggestedMinimumWidth() {\n        return getMinimumWidth() + mPaddings.left + mPaddings.right;\n    }\n\n    /**\n     * Returns the minimum height of the view.\n     *\n     * @return the minimum height the view will try to be.\n     * @see #setMinimumHeight(int)\n     */\n    public int getMinimumHeight() {\n        return mMinHeight;\n    }\n\n    /**\n     * Sets the minimum height of the view. It is not guaranteed the view will\n     * be able to achieve this minimum height (for example, if its parent layout\n     * constrains it with less available height).\n     *\n     * @param minHeight The minimum height the view will try to be.\n     * @see #getMinimumHeight()\n     */\n    public void setMinimumHeight(int minHeight) {\n        mMinHeight = minHeight;\n        requestLayout();\n    }\n\n    /**\n     * Returns the minimum width of the view.\n     *\n     * @return the minimum width the view will try to be.\n     * @see #setMinimumWidth(int)\n     */\n    public int getMinimumWidth() {\n        return mMinWidth;\n    }\n\n    /**\n     * Sets the minimum width of the view. It is not guaranteed the view will\n     * be able to achieve this minimum width (for example, if its parent layout\n     * constrains it with less available width).\n     *\n     * @param minWidth The minimum width the view will try to be.\n     * @see #getMinimumWidth()\n     */\n    public void setMinimumWidth(int minWidth) {\n        mMinWidth = minWidth;\n        requestLayout();\n    }\n\n    protected void setMeasuredSize(int width, int height) {\n        mViewFlags |= FLAG_SET_MEASURED_SIZE;\n        mMeasuredWidth = width;\n        mMeasuredHeight = height;\n    }\n\n    public int getMeasuredWidth() {\n        return mMeasuredWidth;\n    }\n\n    public int getMeasuredHeight() {\n        return mMeasuredHeight;\n    }\n\n    protected void onLayout(\n            boolean changeSize, int left, int top, int right, int bottom) {\n    }\n\n    public LayoutParams getLayoutParams() {\n        return mLayoutParams;\n    }\n\n    public void setLayoutParams(LayoutParams params) {\n        if (params == null) {\n            if (mParent == null) {\n                throw new NullPointerException(\"Layout parameters cannot be null\");\n            } else {\n                params = mParent.generateDefaultLayoutParams();\n            }\n        }\n        mLayoutParams = params;\n        requestLayout();\n    }\n\n    protected boolean checkLayoutParams(LayoutParams p) {\n        return p != null;\n    }\n\n    protected LayoutParams generateDefaultLayoutParams() {\n        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);\n    }\n\n    protected LayoutParams generateLayoutParams(LayoutParams p) {\n        return new LayoutParams(p);\n    }\n\n    /**\n     * Gets the bounds of the given descendant that relative to this view.\n     */\n    public boolean getBoundsOf(GLView descendant, Rect out) {\n        int xOffset = 0;\n        int yOffset = 0;\n        GLView view = descendant;\n        while (view != this) {\n            if (view == null) return false;\n            Rect bounds = view.mBounds;\n            xOffset += bounds.left;\n            yOffset += bounds.top;\n            view = view.mParent;\n        }\n        out.set(xOffset, yOffset, xOffset + descendant.getWidth(),\n                yOffset + descendant.getHeight());\n        return true;\n    }\n\n    protected void onVisibilityChanged(GLView changedView, @Visibility int visibility) {\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).onVisibilityChanged(changedView, visibility);\n        }\n    }\n\n    public void onAttachToRoot(GLRoot root) {\n        mRoot = root;\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).onAttachToRoot(root);\n        }\n    }\n\n    public void onDetachFromRoot() {\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).onDetachFromRoot();\n        }\n        mRoot = null;\n    }\n\n    public void onPause() {\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).onPause();\n        }\n    }\n\n    public void onResume() {\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).onResume();\n        }\n    }\n\n    public void lockRendering() {\n        if (mRoot != null) {\n            mRoot.lockRenderThread();\n        }\n    }\n\n    public void unlockRendering() {\n        if (mRoot != null) {\n            mRoot.unlockRenderThread();\n        }\n    }\n\n    // This is for debugging only.\n    // Dump the view hierarchy into log.\n    void dumpTree(String prefix) {\n        Log.d(TAG, prefix + getClass().getSimpleName());\n        for (int i = 0, n = getComponentCount(); i < n; ++i) {\n            getComponent(i).dumpTree(prefix + \"....\");\n        }\n    }\n\n    @IntDef({VISIBLE, INVISIBLE, GONE})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Visibility {\n    }\n\n    public interface OnClickListener {\n        boolean onClick(GLView v);\n    }\n\n    public interface OnLongClickListener {\n        boolean onLongClick(GLView v);\n    }\n\n    /**\n     * A MeasureSpec encapsulates the layout requirements passed from parent to child.\n     * Each MeasureSpec represents a requirement for either the width or the height.\n     * A MeasureSpec is comprised of a size and a mode. There are three possible\n     * modes:\n     * <dl>\n     * <dt>UNSPECIFIED</dt>\n     * <dd>\n     * The parent has not imposed any constraint on the child. It can be whatever size\n     * it wants.\n     * </dd>\n     *\n     * <dt>EXACTLY</dt>\n     * <dd>\n     * The parent has determined an exact size for the child. The child is going to be\n     * given those bounds regardless of how big it wants to be.\n     * </dd>\n     *\n     * <dt>AT_MOST</dt>\n     * <dd>\n     * The child can be as large as it wants up to the specified size.\n     * </dd>\n     * </dl>\n     * <p>\n     * MeasureSpecs are implemented as ints to reduce object allocation. This class\n     * is provided to pack and unpack the &lt;size, mode&gt; tuple into the int.\n     */\n    public static class MeasureSpec {\n        private static final int MODE_SHIFT = 30;\n        /**\n         * Measure specification mode: The parent has not imposed any constraint\n         * on the child. It can be whatever size it wants.\n         */\n        public static final int UNSPECIFIED = 0;\n        /**\n         * Measure specification mode: The parent has determined an exact size\n         * for the child. The child is going to be given those bounds regardless\n         * of how big it wants to be.\n         */\n        public static final int EXACTLY = 1 << MODE_SHIFT;\n        /**\n         * Measure specification mode: The child can be as large as it wants up\n         * to the specified size.\n         */\n        public static final int AT_MOST = 2 << MODE_SHIFT;\n        private static final int MODE_MASK = 0x3 << MODE_SHIFT;\n\n        /**\n         * Creates a measure specification based on the supplied size and mode.\n         * <p>\n         * The mode must always be one of the following:\n         * <ul>\n         *  <li>{@link #UNSPECIFIED}</li>\n         *  <li>{@link #EXACTLY}</li>\n         *  <li>{@link #AT_MOST}</li>\n         * </ul>\n         *\n         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's\n         * implementation was such that the order of arguments did not matter\n         * and overflow in either value could impact the resulting MeasureSpec.\n         * {@link android.widget.RelativeLayout} was affected by this bug.\n         * Apps targeting API levels greater than 17 will get the fixed, more strict\n         * behavior.</p>\n         *\n         * @param size the size of the measure specification\n         * @param mode the mode of the measure specification\n         * @return the measure specification based on size and mode\n         */\n        public static int makeMeasureSpec(int size, int mode) {\n            return (size & ~MODE_MASK) | (mode & MODE_MASK);\n        }\n\n        /**\n         * Extracts the mode from the supplied measure specification.\n         *\n         * @param measureSpec the measure specification to extract the mode from\n         * @return {@link #UNSPECIFIED},\n         * {@link #AT_MOST} or\n         * {@link #EXACTLY}\n         */\n        public static int getMode(int measureSpec) {\n            return (measureSpec & MODE_MASK);\n        }\n\n        /**\n         * Extracts the size from the supplied measure specification.\n         *\n         * @param measureSpec the measure specification to extract the size from\n         * @return the size in pixels defined in the supplied measure specification\n         */\n        public static int getSize(int measureSpec) {\n            return (measureSpec & ~MODE_MASK);\n        }\n\n        /**\n         * Returns a String representation of the specified measure\n         * specification.\n         *\n         * @param measureSpec the measure specification to convert to a String\n         * @return a String with the following format: \"MeasureSpec: MODE SIZE\"\n         */\n        public static String toString(int measureSpec) {\n            int mode = getMode(measureSpec);\n            int size = getSize(measureSpec);\n\n            StringBuilder sb = new StringBuilder(\"MeasureSpec: \");\n\n            if (mode == UNSPECIFIED)\n                sb.append(\"UNSPECIFIED \");\n            else if (mode == EXACTLY)\n                sb.append(\"EXACTLY \");\n            else if (mode == AT_MOST)\n                sb.append(\"AT_MOST \");\n            else\n                sb.append(mode).append(\" \");\n\n            sb.append(size);\n            return sb.toString();\n        }\n    }\n\n    /**\n     * The LayoutParams look like {@link android.view.ViewGroup.LayoutParams},\n     * and work like it.\n     */\n    public static class LayoutParams {\n        /**\n         * Special value for the height or width requested by a View.\n         * MATCH_PARENT means that the view wants to be as big as its parent,\n         * minus the parent's padding, if any. Introduced in API Level 8.\n         */\n        public static final int MATCH_PARENT = -1;\n\n        /**\n         * Special value for the height or width requested by a View.\n         * WRAP_CONTENT means that the view wants to be just large enough to fit\n         * its own internal content, taking its own padding into account.\n         */\n        public static final int WRAP_CONTENT = -2;\n\n        /**\n         * Information about how wide the view wants to be. Can be one of the\n         * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size.\n         */\n        public int width;\n\n        /**\n         * Information about how tall the view wants to be. Can be one of the\n         * constants {@link #MATCH_PARENT} or {@link #WRAP_CONTENT} or an exact size.\n         */\n        public int height;\n\n        /**\n         * Creates a new set of layout parameters with the specified width\n         * and height.\n         *\n         * @param width  the width, either {@link #WRAP_CONTENT},\n         *               {@link #MATCH_PARENT}, or a fixed size in pixels\n         * @param height the height, either {@link #WRAP_CONTENT},\n         *               {@link #MATCH_PARENT}, or a fixed size in pixels\n         */\n        public LayoutParams(int width, int height) {\n            this.width = width;\n            this.height = height;\n        }\n\n        /**\n         * Copy constructor. Clones the width and height values of the source.\n         *\n         * @param source The layout params to copy from.\n         */\n        public LayoutParams(LayoutParams source) {\n            this.width = source.width;\n            this.height = source.height;\n        }\n    }\n\n    /**\n     * LayoutParams with gravity inside\n     */\n    public static class GravityLayoutParams extends LayoutParams {\n        public int gravity = Gravity.NO_GRAVITY;\n\n        public GravityLayoutParams(GLView.LayoutParams source) {\n            super(source);\n\n            if (source instanceof GravityLayoutParams) {\n                gravity = ((GravityLayoutParams) source).gravity;\n            }\n        }\n\n        public GravityLayoutParams(int width, int height) {\n            super(width, height);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/Gravity.java",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\n\npackage com.hippo.glview.view;\n\nimport androidx.annotation.IntDef;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\npublic class Gravity {\n    public static final int NO_GRAVITY = 0x0000;\n    public static final int LEFT = 0x0001;\n    public static final int TOP = 0x0002;\n    public static final int RIGHT = 0x0004;\n    public static final int BOTTOM = 0x0008;\n    public static final int CENTER_HORIZONTAL = LEFT | RIGHT;\n    public static final int HORIZONTAL_MASK = CENTER_HORIZONTAL;\n    public static final int CENTER_VERTICAL = TOP | BOTTOM;\n    public static final int CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL;\n    public static final int VERTICAL_MASK = CENTER_VERTICAL;\n    public static final int POSITION_BEGIN = 0;\n    public static final int POSITION_CENTER = 1;\n    public static final int POSITION_FINISH = 2;\n    public static final int HORIZONTAL = 0;\n    public static final int VERTICAL = 1;\n\n    public static boolean centerHorizontal(int gravity) {\n        return (gravity & HORIZONTAL_MASK) == CENTER_HORIZONTAL;\n    }\n\n    public static boolean right(int gravity) {\n        return (gravity & HORIZONTAL_MASK) == RIGHT;\n    }\n\n    public static boolean left(int gravity) {\n        return (gravity & HORIZONTAL_MASK) == LEFT;\n    }\n\n    public static boolean centerVertical(int gravity) {\n        return (gravity & VERTICAL_MASK) == CENTER_VERTICAL;\n    }\n\n    public static boolean top(int gravity) {\n        return (gravity & VERTICAL_MASK) == TOP;\n    }\n\n    public static boolean bottom(int gravity) {\n        return (gravity & VERTICAL_MASK) == BOTTOM;\n    }\n\n    public static @PositionMode\n    int getPosition(int gravity, @DirectionMode int direction) {\n        if (direction == HORIZONTAL) {\n            if (right(gravity)) {\n                return POSITION_FINISH;\n            } else if (centerHorizontal(gravity)) {\n                return POSITION_CENTER;\n            } else {\n                return POSITION_BEGIN;\n            }\n        } else {\n            if (bottom(gravity)) {\n                return POSITION_FINISH;\n            } else if (centerVertical(gravity)) {\n                return POSITION_CENTER;\n            } else {\n                return POSITION_BEGIN;\n            }\n        }\n    }\n\n    @IntDef({POSITION_BEGIN, POSITION_CENTER, POSITION_FINISH})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface PositionMode {\n    }\n\n    @IntDef({HORIZONTAL, VERTICAL})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface DirectionMode {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/OrientationSource.java",
    "content": "/*\n * Copyright (C) 2012 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\npackage com.hippo.glview.view;\n\npublic interface OrientationSource {\n    int getDisplayRotation();\n\n    int getCompensation();\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/TouchHelper.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.glview.view;\n\nimport android.content.Context;\nimport android.view.MotionEvent;\nimport android.view.ViewConfiguration;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.yorozuya.LayoutUtils;\nimport com.hippo.yorozuya.SimpleHandler;\n\npublic class TouchHelper {\n    /**\n     * Cache the touch slop from the context that created the view.\n     */\n    private static int sTouchSlop = 8;\n    private final TouchOwner mOwner;\n    private boolean mPrePressed = false;\n    private boolean mHasPerformedLongPress = false;\n    private CheckForLongPress mPendingCheckForLongPress = null;\n    private CheckForTap mPendingCheckForTap = null;\n    private PerformClick mPerformClick = null;\n    private UnsetPressedState mUnsetPressedState = null;\n\n    public TouchHelper(@NonNull TouchOwner owner) {\n        mOwner = owner;\n    }\n\n    public static void initialize(Context context) {\n        sTouchSlop = LayoutUtils.dp2pix(context, 8); // 8dp\n    }\n\n    public boolean onTouch(MotionEvent event) {\n        TouchOwner owner = mOwner;\n        final float x = event.getX();\n        final float y = event.getY();\n\n        if (event.getAction() == MotionEvent.ACTION_DOWN ||\n                event.getAction() == MotionEvent.ACTION_MOVE)\n            owner.setHotspot(x, y);\n\n        if (!owner.isEnabled()) {\n            if (event.getAction() == MotionEvent.ACTION_UP && owner.isPressed()) {\n                owner.setPressed(false);\n            }\n            // A disabled view that is clickable still consumes the touch\n            // events, it just doesn't respond to them.\n            return (owner.isClickable() || owner.isLongClickable());\n        }\n\n        if (owner.isClickable() || owner.isLongClickable()) {\n            switch (event.getAction()) {\n                case MotionEvent.ACTION_UP:\n                    if (owner.isPressed() || mPrePressed) {\n                        if (mPrePressed) {\n                            // The button is being released before we actually\n                            // showed it as pressed.  Make it show the pressed\n                            // state now (before scheduling the click) to ensure\n                            // the user sees it.\n                            setPressed(true, x, y);\n                        }\n\n                        if (!mHasPerformedLongPress) {\n                            // This is a tap, so remove the longpress check\n                            removeLongPressCallback();\n\n                            // Use a Runnable and post this rather than calling\n                            // performClick directly. This lets other visual state\n                            // of the view update before click actions start.\n                            if (mPerformClick == null) {\n                                mPerformClick = new PerformClick();\n                            }\n                            if (!SimpleHandler.getInstance().post(mPerformClick)) {\n                                owner.performClick();\n                            }\n                        }\n\n                        if (mUnsetPressedState == null) {\n                            mUnsetPressedState = new UnsetPressedState();\n                        }\n\n                        if (mPrePressed) {\n                            SimpleHandler.getInstance().postDelayed(mUnsetPressedState,\n                                    ViewConfiguration.getPressedStateDuration());\n                        } else if (!SimpleHandler.getInstance().post(mUnsetPressedState)) {\n                            // If the post failed, unpress right now\n                            mUnsetPressedState.run();\n                        }\n\n                        removeTapCallback();\n                    }\n                    break;\n\n                case MotionEvent.ACTION_DOWN:\n                    mHasPerformedLongPress = false;\n\n                    mPrePressed = true;\n                    if (mPendingCheckForTap == null) {\n                        mPendingCheckForTap = new CheckForTap();\n                    }\n                    mPendingCheckForTap.x = event.getX();\n                    mPendingCheckForTap.y = event.getY();\n                    SimpleHandler.getInstance().postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());\n                    break;\n\n                case MotionEvent.ACTION_CANCEL:\n                    owner.setPressed(false);\n                    removeTapCallback();\n                    removeLongPressCallback();\n                    break;\n\n                case MotionEvent.ACTION_MOVE:\n                    owner.setHotspot(x, y);\n\n                    // Be lenient about moving outside of buttons\n                    if (!pointInView(x, y, sTouchSlop)) {\n                        // Outside button\n                        removeTapCallback();\n                        if (owner.isPressed()) {\n                            // Remove any future long press/tap checks\n                            removeLongPressCallback();\n\n                            owner.setPressed(false);\n                        }\n                    }\n                    break;\n            }\n\n            return true;\n        }\n\n        return false;\n    }\n\n    private void setPressed(boolean pressed, float x, float y) {\n        mOwner.setPressed(pressed);\n        if (pressed) {\n            mOwner.setHotspot(x, y);\n        }\n    }\n\n    /**\n     * Utility method to determine whether the given point, in local coordinates,\n     * is inside the view, where the area of the view is expanded by the slop factor.\n     * This method is called while processing touch-move events to determine if the event\n     * is still within the view.\n     */\n    public boolean pointInView(float localX, float localY, float slop) {\n        return localX >= -slop && localY >= -slop && localX < (mOwner.getWidth() + slop) &&\n                localY < (mOwner.getHeight() + slop);\n    }\n\n    /**\n     * Remove the longpress detection timer.\n     */\n    private void removeLongPressCallback() {\n        if (mPendingCheckForLongPress != null) {\n            SimpleHandler.getInstance().removeCallbacks(mPendingCheckForLongPress);\n        }\n    }\n\n    /**\n     * Remove the tap detection timer.\n     */\n    private void removeTapCallback() {\n        if (mPendingCheckForTap != null) {\n            mPrePressed = false;\n            SimpleHandler.getInstance().removeCallbacks(mPendingCheckForTap);\n        }\n    }\n\n    private void checkForLongClick(int delayOffset) {\n        if (mOwner.isLongClickable()) {\n            mHasPerformedLongPress = false;\n\n            if (mPendingCheckForLongPress == null) {\n                mPendingCheckForLongPress = new CheckForLongPress();\n            }\n            SimpleHandler.getInstance().postDelayed(mPendingCheckForLongPress,\n                    ViewConfiguration.getLongPressTimeout() - delayOffset);\n        }\n    }\n\n    private final class CheckForLongPress implements Runnable {\n        @Override\n        public void run() {\n            if (mOwner.isPressed()) {\n                if (mOwner.performLongClick()) {\n                    mHasPerformedLongPress = true;\n                }\n            }\n        }\n    }\n\n    private final class CheckForTap implements Runnable {\n        public float x;\n        public float y;\n\n        @Override\n        public void run() {\n            mPrePressed = false;\n            setPressed(true, x, y);\n            checkForLongClick(ViewConfiguration.getTapTimeout());\n        }\n    }\n\n    private final class PerformClick implements Runnable {\n        @Override\n        public void run() {\n            mOwner.performClick();\n        }\n    }\n\n    private final class UnsetPressedState implements Runnable {\n        @Override\n        public void run() {\n            mOwner.setPressed(false);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/view/TouchOwner.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.glview.view;\n\npublic interface TouchOwner {\n    void setHotspot(float x, float y);\n\n    boolean isEnabled();\n\n    boolean isPressed();\n\n    void setPressed(boolean pressed);\n\n    boolean isClickable();\n\n    boolean isLongClickable();\n\n    void performClick();\n\n    boolean performLongClick();\n\n    int getWidth();\n\n    int getHeight();\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/widget/GLFrameLayout.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.widget;\n\nimport com.hippo.glview.view.GLView;\nimport com.hippo.glview.view.Gravity;\n\npublic class GLFrameLayout extends GLView {\n    @Override\n    protected void onMeasure(int widthSpec, int heightSpec) {\n        int maxWidth = 0;\n        int maxHeight = 0;\n        for (int i = 0, n = getComponentCount(); i < n; i++) {\n            GLView component = getComponent(i);\n            if (component.getVisibility() == GONE) {\n                continue;\n            }\n            measureComponent(component, widthSpec, heightSpec);\n            maxWidth = Math.max(maxWidth, component.getMeasuredWidth());\n            maxHeight = Math.max(maxHeight, component.getMeasuredHeight());\n        }\n\n        // Consider min\n        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());\n        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());\n\n        // The final\n        maxWidth = getDefaultSize(maxWidth, widthSpec);\n        maxHeight = getDefaultSize(maxHeight, heightSpec);\n\n        setMeasuredSize(maxWidth, maxHeight);\n\n        if (MeasureSpec.getSize(widthSpec) != MeasureSpec.EXACTLY ||\n                MeasureSpec.getSize(heightSpec) != MeasureSpec.EXACTLY) {\n            // Measure again\n            measureAllComponents(this, MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY),\n                    MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY));\n        }\n    }\n\n    @Override\n    protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) {\n        int width = getWidth();\n        int height = getHeight();\n\n        for (int i = 0, n = getComponentCount(); i < n; i++) {\n            GLView component = getComponent(i);\n            if (component.getVisibility() == GONE) {\n                continue;\n            }\n            int measureWidth = component.getMeasuredWidth();\n            int measureHeight = component.getMeasuredHeight();\n            int componentLeft;\n            int componentTop;\n\n            GravityLayoutParams lp = (GravityLayoutParams) component.getLayoutParams();\n            int gravity = lp.gravity;\n            if (Gravity.centerHorizontal(gravity)) {\n                componentLeft = (width / 2) - (measureWidth / 2);\n            } else if (Gravity.right(gravity)) {\n                componentLeft = width - measureWidth;\n            } else {\n                componentLeft = 0;\n            }\n            if (Gravity.centerVertical(gravity)) {\n                componentTop = (height / 2) - (measureHeight / 2);\n            } else if (Gravity.bottom(gravity)) {\n                componentTop = height - measureHeight;\n            } else {\n                componentTop = 0;\n            }\n\n            component.layout(componentLeft, componentTop,\n                    componentLeft + measureWidth, componentTop + measureHeight);\n        }\n    }\n\n    @Override\n    protected boolean checkLayoutParams(GLView.LayoutParams p) {\n        return p instanceof GravityLayoutParams;\n    }\n\n    @Override\n    protected LayoutParams generateDefaultLayoutParams() {\n        return new GravityLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);\n    }\n\n    @Override\n    protected LayoutParams generateLayoutParams(GLView.LayoutParams p) {\n        return p == null ? generateDefaultLayoutParams() : new GravityLayoutParams(p);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/widget/GLLinearLayout.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.widget;\n\nimport android.graphics.Rect;\n\nimport androidx.annotation.IntDef;\n\nimport com.hippo.glview.view.GLView;\nimport com.hippo.glview.view.Gravity;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class GLLinearLayout extends GLView {\n    public static final int HORIZONTAL = 0;\n    public static final int VERTICAL = 1;\n    private final List<GLView> mTempList = new ArrayList<>();\n    private int mInterval = 0;\n    private int mOrientation = VERTICAL;\n\n    public void setInterval(int interval) {\n        if (mInterval != interval) {\n            mInterval = interval;\n            requestLayout();\n        }\n    }\n\n    /**\n     * Should the layout be a column or a row.\n     *\n     * @param orientation Pass {@link #HORIZONTAL} or {@link #VERTICAL}. Default\n     *                    value is {@link #HORIZONTAL}.\n     */\n    public void setOrientation(@OrientationMode int orientation) {\n        if (mOrientation != orientation) {\n            mOrientation = orientation;\n            requestLayout();\n        }\n    }\n\n    @Override\n    protected void onMeasure(int widthSpec, int heightSpec) {\n        int layoutWidthSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthSpec) - mPaddings.left - mPaddings.right,\n                MeasureSpec.getMode(widthSpec));\n        int layoutHeightSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightSpec) - mPaddings.top - mPaddings.bottom,\n                MeasureSpec.getMode(heightSpec));\n\n        if (mOrientation == HORIZONTAL && MeasureSpec.getMode(widthSpec) == MeasureSpec.EXACTLY) {\n            int width = MeasureSpec.getSize(layoutWidthSpec);\n            float sumWeight = 0.0f;\n\n            for (int i = 0, n = getComponentCount(); i < n; i++) {\n                final GLView component = getComponent(i);\n                if (component.getVisibility() == GONE) {\n                    continue;\n                }\n                final LayoutParams lp = (LayoutParams) component.getLayoutParams();\n\n                if (lp.weight > 0.0f) {\n                    sumWeight += lp.weight;\n                    mTempList.add(component);\n                } else {\n                    measureComponent(component, layoutWidthSpec, layoutHeightSpec);\n                    width -= component.getMeasuredWidth();\n                }\n            }\n\n            for (GLView component : mTempList) {\n                final LayoutParams lp = (LayoutParams) component.getLayoutParams();\n                final int componentWidthSpec = MeasureSpec.makeMeasureSpec(\n                        (int) (width * lp.weight / sumWeight), MeasureSpec.EXACTLY);\n                final int componentHeightSpec = getComponentSpec(layoutHeightSpec, lp.height);\n                component.measure(componentWidthSpec, componentHeightSpec);\n            }\n\n            mTempList.clear();\n        } else if (mOrientation == VERTICAL && MeasureSpec.getMode(heightSpec) == MeasureSpec.EXACTLY) {\n            int height = MeasureSpec.getSize(layoutHeightSpec);\n            float sumWeight = 0.0f;\n\n            for (int i = 0, n = getComponentCount(); i < n; i++) {\n                final GLView component = getComponent(i);\n                if (component.getVisibility() == GONE) {\n                    continue;\n                }\n                final LayoutParams lp = (LayoutParams) component.getLayoutParams();\n\n                if (lp.weight > 0.0f) {\n                    sumWeight += lp.weight;\n                    mTempList.add(component);\n                } else {\n                    measureComponent(component, layoutWidthSpec, layoutHeightSpec);\n                    height -= component.getMeasuredHeight();\n                }\n            }\n\n            for (GLView component : mTempList) {\n                final LayoutParams lp = (LayoutParams) component.getLayoutParams();\n                final int componentWidthSpec = getComponentSpec(layoutWidthSpec, lp.width);\n                final int componentHeightSpec = MeasureSpec.makeMeasureSpec(\n                        (int) (height * lp.weight / sumWeight), MeasureSpec.EXACTLY);\n                component.measure(componentWidthSpec, componentHeightSpec);\n            }\n\n            mTempList.clear();\n        } else {\n            measureAllComponents(this, layoutWidthSpec, layoutHeightSpec);\n        }\n\n        int maxWidth = 0;\n        int maxHeight = 0;\n        for (int i = 0, n = getComponentCount(); i < n; i++) {\n            GLView component = getComponent(i);\n            if (component.getVisibility() == GONE) {\n                continue;\n            }\n\n            if (mOrientation == VERTICAL) {\n                maxWidth = Math.max(maxWidth, component.getMeasuredWidth());\n                if (i != 0) {\n                    maxHeight += mInterval;\n                }\n                maxHeight += component.getMeasuredHeight();\n            } else if (mOrientation == HORIZONTAL) {\n                maxHeight = Math.max(maxHeight, component.getMeasuredHeight());\n                if (i != 0) {\n                    maxWidth += mInterval;\n                }\n                maxWidth += component.getMeasuredWidth();\n            }\n        }\n\n        Rect paddings = getPaddings();\n        maxWidth = maxWidth + paddings.left + paddings.right;\n        maxHeight = maxHeight + paddings.top + paddings.bottom;\n\n        setMeasuredSize(getDefaultSize(maxWidth, widthSpec), getDefaultSize(maxHeight, heightSpec));\n    }\n\n    @Override\n    protected void onLayout(boolean changeSize, int left, int top, int right, int bottom) {\n        int width = getWidth();\n        int height = getHeight();\n        Rect paddings = getPaddings();\n        int componentLeft;\n        int componentTop;\n\n        if (mOrientation == VERTICAL) {\n            componentTop = paddings.top;\n\n            for (int i = 0, n = getComponentCount(); i < n; i++) {\n                GLView component = getComponent(i);\n                if (component.getVisibility() == GONE) {\n                    continue;\n                }\n                int measureWidth = component.getMeasuredWidth();\n                int measureHeight = component.getMeasuredHeight();\n\n                LayoutParams lp = (LayoutParams) component.getLayoutParams();\n                componentLeft = getDefaultBegin(width, measureWidth, paddings.left, paddings.right,\n                        Gravity.getPosition(lp.gravity, Gravity.HORIZONTAL));\n                component.layout(componentLeft, componentTop,\n                        componentLeft + measureWidth, componentTop + measureHeight);\n\n                componentTop += measureHeight + mInterval;\n            }\n        } else if (mOrientation == HORIZONTAL) {\n            componentLeft = paddings.left;\n\n            for (int i = 0, n = getComponentCount(); i < n; i++) {\n                GLView component = getComponent(i);\n                if (component.getVisibility() == GONE) {\n                    continue;\n                }\n                int measureWidth = component.getMeasuredWidth();\n                int measureHeight = component.getMeasuredHeight();\n\n                LayoutParams lp = (LayoutParams) component.getLayoutParams();\n                componentTop = getDefaultBegin(height, measureHeight, paddings.top, paddings.bottom,\n                        Gravity.getPosition(lp.gravity, Gravity.VERTICAL));\n                component.layout(componentLeft, componentTop,\n                        componentLeft + measureWidth, componentTop + measureHeight);\n\n                componentLeft += measureWidth + mInterval;\n            }\n        }\n    }\n\n    @Override\n    protected boolean checkLayoutParams(GLView.LayoutParams p) {\n        return p instanceof LayoutParams;\n    }\n\n    @Override\n    protected GLView.LayoutParams generateDefaultLayoutParams() {\n        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);\n    }\n\n    @Override\n    protected GLView.LayoutParams generateLayoutParams(GLView.LayoutParams p) {\n        return p == null ? generateDefaultLayoutParams() : new LayoutParams(p);\n    }\n\n    @IntDef({HORIZONTAL, VERTICAL})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface OrientationMode {\n    }\n\n    public static class LayoutParams extends GravityLayoutParams {\n        public float weight = 0.0f;\n\n        public LayoutParams(GLView.LayoutParams source) {\n            super(source);\n\n            if (source instanceof LayoutParams) {\n                weight = ((LayoutParams) source).weight;\n            }\n        }\n\n        public LayoutParams(int width, int height) {\n            super(width, height);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/widget/GLProgressView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.widget;\n\nimport android.graphics.Color;\nimport android.graphics.Path;\nimport android.view.animation.Interpolator;\nimport android.view.animation.LinearInterpolator;\n\nimport androidx.core.view.animation.PathInterpolatorCompat;\n\nimport com.hippo.glview.anim.Animation;\nimport com.hippo.glview.anim.FloatAnimation;\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.glrenderer.GLPaint;\nimport com.hippo.glview.view.AnimationTime;\nimport com.hippo.glview.view.GLView;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class GLProgressView extends GLView {\n    private static final Interpolator TRIM_START_INTERPOLATOR;\n    private static final Interpolator TRIM_END_INTERPOLATOR;\n    private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();\n\n    static {\n        Path trimStartPath = new Path();\n        trimStartPath.moveTo(0.0f, 0.0f);\n        trimStartPath.lineTo(0.5f, 0.0f);\n        trimStartPath.cubicTo(0.7f, 0.0f, 0.6f, 1f, 1f, 1f);\n        TRIM_START_INTERPOLATOR = PathInterpolatorCompat.create(trimStartPath);\n\n        Path trimEndPath = new Path();\n        trimEndPath.moveTo(0.0f, 0.0f);\n        trimEndPath.cubicTo(0.2f, 0.0f, 0.1f, 1f, 0.5f, 1f);\n        trimEndPath.lineTo(1f, 1f);\n        TRIM_END_INTERPOLATOR = PathInterpolatorCompat.create(trimEndPath);\n    }\n\n    private final GLPaint mGLPaint;\n    private final List<Animation> mAnimations;\n    private float mCx;\n    private float mCy;\n    private float mRadiusX;\n    private float mRadiusY;\n    private float mTrimStart = 0.0f;\n    private float mTrimEnd = 0.0f;\n    private float mTrimOffset = 0.0f;\n    private float mTrimRotation = 0.0f;\n    private boolean mIndeterminate = false;\n\n    public GLProgressView() {\n        mGLPaint = new GLPaint();\n        mGLPaint.setColor(Color.WHITE);\n        mGLPaint.setBackgroundColor(Color.BLACK);\n        mAnimations = new ArrayList<>();\n\n        setupAnimations();\n    }\n\n    public void setupAnimations() {\n        FloatAnimation trimStart = new FloatAnimation() {\n            @Override\n            protected void onCalculate(float progress) {\n                super.onCalculate(progress);\n                mTrimStart = get();\n            }\n        };\n        trimStart.setRange(0.0f, 0.75f);\n        trimStart.setDuration(1333L);\n        trimStart.setInterpolator(TRIM_START_INTERPOLATOR);\n        trimStart.setRepeatCount(Animation.INFINITE);\n\n        FloatAnimation trimEnd = new FloatAnimation() {\n            @Override\n            protected void onCalculate(float progress) {\n                super.onCalculate(progress);\n                mTrimEnd = get();\n            }\n        };\n        trimEnd.setRange(0.0f, 0.75f);\n        trimEnd.setDuration(1333L);\n        trimEnd.setInterpolator(TRIM_END_INTERPOLATOR);\n        trimEnd.setRepeatCount(Animation.INFINITE);\n\n        FloatAnimation trimOffset = new FloatAnimation() {\n            @Override\n            protected void onCalculate(float progress) {\n                super.onCalculate(progress);\n                mTrimOffset = get();\n            }\n        };\n        trimOffset.setRange(0.0f, 0.25f);\n        trimOffset.setDuration(1333L);\n        trimOffset.setInterpolator(LINEAR_INTERPOLATOR);\n        trimOffset.setRepeatCount(Animation.INFINITE);\n\n        FloatAnimation trimRotation = new FloatAnimation() {\n            @Override\n            protected void onCalculate(float progress) {\n                super.onCalculate(progress);\n                mTrimRotation = get();\n            }\n        };\n        trimRotation.setRange(0.0f, 720.0f);\n        trimRotation.setDuration(6665L);\n        trimRotation.setInterpolator(LINEAR_INTERPOLATOR);\n        trimRotation.setRepeatCount(Animation.INFINITE);\n\n        mAnimations.add(trimStart);\n        mAnimations.add(trimEnd);\n        mAnimations.add(trimOffset);\n        mAnimations.add(trimRotation);\n    }\n\n    private void startAnimations() {\n        List<Animation> animations = mAnimations;\n        for (int i = 0, n = animations.size(); i < n; i++) {\n            animations.get(i).reset();\n        }\n    }\n\n    private void stopAnimations() {\n        List<Animation> animations = mAnimations;\n        for (int i = 0, n = animations.size(); i < n; i++) {\n            animations.get(i).cancel();\n        }\n    }\n\n    @Override\n    protected void onLayout(boolean changeSize, int left, int top, int right,\n                            int bottom) {\n        super.onLayout(changeSize, left, top, right, bottom);\n\n        int width = right - left;\n        int height = bottom - top;\n        mGLPaint.setLineWidth(Math.min(width, height) / 12.0f);\n        mCx = (float) width / 2;\n        mCy = (float) height / 2;\n        mRadiusX = width / 48.0f * 19.0f;\n        mRadiusY = height / 48.0f * 19.0f;\n    }\n\n    public void setColor(int color) {\n        mGLPaint.setColor(color);\n        invalidate();\n    }\n\n    public void setBgColor(int color) {\n        mGLPaint.setBackgroundColor(color);\n        invalidate();\n    }\n\n    public boolean isIndeterminate() {\n        return mIndeterminate;\n    }\n\n    public void setIndeterminate(boolean indeterminate) {\n        if (mIndeterminate != indeterminate) {\n            mIndeterminate = indeterminate;\n            if (indeterminate) {\n                startAnimations();\n            } else {\n                stopAnimations();\n            }\n            invalidate();\n        }\n    }\n\n    public void setProgress(float progress) {\n        if (!mIndeterminate) {\n            mTrimStart = 0.0f;\n            mTrimEnd = progress;\n            mTrimOffset = 0.0f;\n            mTrimRotation = 0.0f;\n            invalidate();\n        }\n    }\n\n    @Override\n    public void onRender(GLCanvas canvas) {\n        update();\n\n        int width = getWidth();\n        int height = getHeight();\n        int cx = width / 2;\n        int cy = height / 2;\n\n        float startAngle = (mTrimStart + mTrimOffset) * 360.0f - 90;\n        float sweepAngle = Math.max(12.0f, (mTrimEnd - mTrimStart) * 360.0f);\n        float rotation = mTrimRotation + startAngle;\n\n        canvas.save();\n\n        canvas.translate(cx, cy);\n        canvas.rotate(rotation, 0, 0, 1);\n        if (rotation % 180 != 0) {\n            canvas.translate(-cy, -cx);\n        } else {\n            canvas.translate(-cx, -cy);\n        }\n\n        canvas.drawArc(mCx, mCy, mRadiusX, mRadiusY, sweepAngle, mGLPaint);\n\n        canvas.restore();\n    }\n\n    private void update() {\n        boolean invalidate = false;\n\n        if (mIndeterminate) {\n            long currentTime = AnimationTime.get();\n            List<Animation> animations = mAnimations;\n            for (int i = 0, n = animations.size(); i < n; i++) {\n                invalidate |= animations.get(i).calculate(currentTime);\n            }\n        }\n\n        if (invalidate) {\n            invalidate();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/glview/widget/GLTextureView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.glview.widget;\n\nimport android.graphics.RectF;\n\nimport com.hippo.glview.glrenderer.GLCanvas;\nimport com.hippo.glview.glrenderer.Texture;\nimport com.hippo.glview.view.GLView;\n\npublic class GLTextureView extends GLView {\n    private final RectF mSrc = new RectF();\n    private final RectF mDst = new RectF();\n\n    private Texture mTexture;\n\n    public Texture getTexture() {\n        return mTexture;\n    }\n\n    public void setTexture(Texture texture) {\n        mTexture = texture;\n        if (texture != null) {\n            mSrc.set(0.0f, 0.0f, texture.getWidth(), texture.getHeight());\n        } else {\n            mSrc.setEmpty();\n        }\n        invalidate();\n    }\n\n    @Override\n    protected int getSuggestedMinimumWidth() {\n        return mTexture == null ? super.getSuggestedMinimumWidth() : mTexture.getWidth() + mPaddings.width();\n    }\n\n    @Override\n    protected int getSuggestedMinimumHeight() {\n        return mTexture == null ? super.getSuggestedMinimumWidth() : mTexture.getHeight() + mPaddings.height();\n    }\n\n    @Override\n    public void onRender(GLCanvas canvas) {\n        if (mTexture == null || mSrc.isEmpty()) {\n            return;\n        }\n\n        mDst.set(mPaddings.left, mPaddings.top, getWidth() - mPaddings.right, getHeight() - mPaddings.bottom);\n        if (mDst.isEmpty()) {\n            return;\n        }\n\n        mTexture.draw(canvas, mSrc, mDst);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/image/Image.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.image\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.PorterDuff\nimport android.graphics.drawable.Animatable\nimport androidx.core.graphics.createBitmap\nimport coil3.BitmapImage\nimport coil3.DrawableImage\nimport coil3.Image as CoilImage\nimport coil3.asImage\nimport coil3.imageLoader\nimport coil3.request.CachePolicy\nimport coil3.request.ErrorResult\nimport coil3.request.ImageRequest\nimport coil3.request.SuccessResult\nimport coil3.request.allowHardware\nimport coil3.size.Dimension\nimport coil3.size.Precision\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.jni.isGif\nimport com.hippo.ehviewer.jni.mmap\nimport com.hippo.ehviewer.jni.munmap\nimport com.hippo.ehviewer.jni.nativeTexImage\nimport com.hippo.ehviewer.jni.rewriteGifSource\nimport com.hippo.unifile.UniFile\nimport com.hippo.util.isAtLeastU\nimport java.nio.ByteBuffer\n\nclass Image private constructor(\n    private val image: CoilImage,\n    private val src: ImageSource? = null,\n) {\n    private var mBitmap: Bitmap? = null\n    private var mCanvas: Canvas? = null\n\n    val animated get() = image is DrawableImage && image.drawable is Animatable\n    val delay get() = if (animated) 40 else 0\n    val isOpaque get() = false\n    val width get() = image.width\n    val height get() = image.height\n    var frameUpdateAllowed = true\n    var isRecycled = false\n        private set\n    var started = false\n        private set\n\n    @Synchronized\n    fun recycle() {\n        if (isRecycled) return\n        when (image) {\n            is DrawableImage -> {\n                (image.drawable as? Animatable)?.stop()\n                image.drawable.callback = null\n                src?.close()\n                mCanvas = null\n                mBitmap?.recycle()\n                mBitmap = null\n            }\n            is BitmapImage -> image.bitmap.recycle()\n        }\n        isRecycled = true\n    }\n\n    private fun prepareBitmap() {\n        if (mBitmap != null) return\n        mBitmap = createBitmap(width, height)\n        mCanvas = Canvas(mBitmap!!)\n    }\n\n    private fun updateBitmap() {\n        if (frameUpdateAllowed) {\n            frameUpdateAllowed = false\n            prepareBitmap()\n            mCanvas!!.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)\n            image.draw(mCanvas!!)\n        }\n    }\n\n    fun texImage(init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int) {\n        val bitmap = if (image is BitmapImage) {\n            image.bitmap\n        } else {\n            updateBitmap()\n            mBitmap!!\n        }\n        nativeTexImage(\n            bitmap,\n            init,\n            offsetX,\n            offsetY,\n            width,\n            height,\n        )\n    }\n\n    fun start() {\n        if (!started) {\n            started = true\n            if (image is DrawableImage) (image.drawable as? Animatable)?.start()\n        }\n    }\n\n    companion object {\n        private val appCtx = EhApplication.application\n        private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 2\n\n        private suspend fun decodeCoil(data: Any): CoilImage {\n            val req = ImageRequest.Builder(appCtx).apply {\n                data(data)\n                size(Dimension(targetWidth), Dimension.Undefined)\n                precision(Precision.INEXACT)\n                allowHardware(false)\n                memoryCachePolicy(CachePolicy.DISABLED)\n            }.build()\n            return when (val result = appCtx.imageLoader.execute(req)) {\n                is SuccessResult -> result.image\n                is ErrorResult -> throw result.throwable\n            }\n        }\n\n        suspend fun decode(src: ImageSource): Image? {\n            return runCatching {\n                val image = when (src) {\n                    is UniFileSource -> {\n                        if (!isAtLeastU) {\n                            src.source.openFileDescriptor(\"rw\").use {\n                                val fd = it.fd\n                                if (isGif(fd)) {\n                                    val buffer = mmap(fd)!!\n                                    val source = object : ByteBufferSource {\n                                        override val source = buffer\n                                        override fun close() {\n                                            munmap(buffer)\n                                            src.close()\n                                        }\n                                    }\n                                    return decode(source)\n                                }\n                            }\n                        }\n                        decodeCoil(src.source.uri)\n                    }\n                    is ByteBufferSource -> {\n                        if (!isAtLeastU) {\n                            rewriteGifSource(src.source)\n                        }\n                        decodeCoil(src.source)\n                    }\n                }\n                when (image) {\n                    is DrawableImage -> image.drawable.apply {\n                        setBounds(0, 0, intrinsicWidth, intrinsicHeight)\n                    }\n                    is BitmapImage -> src.close()\n                }\n                Image(image, src)\n            }.onFailure {\n                src.close()\n                it.printStackTrace()\n            }.getOrNull()\n        }\n\n        @JvmStatic\n        fun create(bitmap: Bitmap): Image = Image(bitmap.asImage(), null)\n    }\n}\n\nsealed interface ImageSource : AutoCloseable\n\ninterface UniFileSource : ImageSource {\n    val source: UniFile\n}\n\ninterface ByteBufferSource : ImageSource {\n    val source: ByteBuffer\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/network/CookieDatabase.kt",
    "content": "/*\n * Copyright 2017 Hippo Seven\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.hippo.network\n\nimport android.content.Context\nimport androidx.room.AutoMigration\nimport androidx.room.ColumnInfo\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\nimport androidx.room.Update\nimport okhttp3.Cookie as OkHttpCookie\n\n@Entity(tableName = \"OK_HTTP_3_COOKIE\")\nclass Cookie(\n    @ColumnInfo(name = \"NAME\")\n    var name: String,\n    @ColumnInfo(name = \"VALUE\")\n    var value: String,\n    @ColumnInfo(name = \"EXPIRES_AT\")\n    var expiresAt: Long,\n    @ColumnInfo(name = \"DOMAIN\")\n    var domain: String,\n    @ColumnInfo(name = \"PATH\")\n    var path: String,\n    @ColumnInfo(name = \"SECURE\")\n    var secure: Boolean,\n    @ColumnInfo(name = \"HTTP_ONLY\")\n    var httpOnly: Boolean,\n    @ColumnInfo(name = \"PERSISTENT\")\n    var persistent: Boolean,\n    @ColumnInfo(name = \"HOST_ONLY\")\n    var hostOnly: Boolean,\n    @PrimaryKey\n    @ColumnInfo(name = \"_id\")\n    var id: Long?,\n)\n\n@Dao\ninterface CookiesDao {\n    @Query(\"SELECT * FROM OK_HTTP_3_COOKIE\")\n    fun list(): List<Cookie>\n\n    @Delete\n    fun delete(cookie: Cookie)\n\n    @Insert\n    fun insert(cookie: Cookie): Long\n\n    @Update\n    fun update(cookie: Cookie)\n}\n\n/* 1 -> 2 some nullability changes */\n@Database(\n    entities = [Cookie::class],\n    version = 2,\n    autoMigrations = [\n        AutoMigration(\n            from = 1,\n            to = 2,\n        ),\n    ],\n)\nabstract class CookiesDatabase : RoomDatabase() {\n    abstract fun cookiesDao(): CookiesDao\n}\n\ninternal class CookieDatabase(context: Context, name: String) {\n    private val cookiesList by lazy {\n        val now = System.currentTimeMillis()\n        db.cookiesDao().list().mapNotNull {\n            it.takeUnless { !it.persistent || it.expiresAt <= now } ?: run {\n                db.cookiesDao().delete(it)\n                null\n            }\n        }.toMutableList()\n    }\n    private val db = Room.databaseBuilder(context, CookiesDatabase::class.java, name).build()\n\n    val allCookies by lazy {\n        hashMapOf<String, CookieSet>().also { map ->\n            cookiesList.map { it.toOkHttp3Cookie() }.forEach {\n                val set = map[it.domain] ?: CookieSet().apply { map[it.domain] = this }\n                set.add(it)\n            }\n        }\n    }\n\n    private fun findCookieWithOkHttpCookies(cookie: OkHttpCookie): Cookie? = cookiesList.find { it.name == cookie.name && it.domain == cookie.domain && it.value == cookie.value }\n\n    fun add(cookie: OkHttpCookie) {\n        val c = cookie.toCookie()\n        c.id = db.cookiesDao().insert(c)\n        cookiesList.add(c)\n    }\n\n    fun update(from: OkHttpCookie, to: OkHttpCookie) {\n        val origin = findCookieWithOkHttpCookies(from) ?: return\n        val new = to.toCookie(origin.id)\n        cookiesList.remove(origin)\n        cookiesList.add(new)\n        db.cookiesDao().update(new)\n    }\n\n    fun remove(cookie: OkHttpCookie) {\n        val origin = findCookieWithOkHttpCookies(cookie) ?: return\n        db.cookiesDao().delete(origin)\n        cookiesList.remove(origin)\n    }\n\n    fun clear() {\n        db.clearAllTables()\n        cookiesList.clear()\n    }\n}\n\nfun Cookie.toOkHttp3Cookie(): OkHttpCookie = OkHttpCookie.Builder().apply {\n    name(name)\n    value(value)\n    expiresAt(expiresAt)\n    if (hostOnly) hostOnlyDomain(domain) else domain(domain)\n    path(path)\n    if (secure) secure()\n    if (httpOnly) httpOnly()\n}.build()\n\nprivate fun OkHttpCookie.toCookie(id: Long? = null): Cookie = Cookie(\n    name, value, expiresAt, domain, path, secure, httpOnly, persistent, hostOnly, id,\n)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/network/CookieSet.kt",
    "content": "/*\n * Copyright 2017 Hippo Seven\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.hippo.network\n\nimport okhttp3.Cookie\nimport okhttp3.HttpUrl\n\ninternal class CookieSet {\n    private val map: MutableMap<Key, Cookie> = HashMap()\n\n    /**\n     * Adds a cookie to this `CookieSet`.\n     * Returns a previous cookie with\n     * the same name, domain and path or `null`.\n     */\n    fun add(cookie: Cookie): Cookie? = map.put(Key(cookie), cookie)\n\n    /**\n     * Removes a cookie with the same name,\n     * domain and path as the cookie.\n     * Returns the removed cookie or `null`.\n     */\n    fun remove(cookie: Cookie): Cookie? = map.remove(Key(cookie))\n\n    /**\n     * Get cookies for the url. Fill `accepted` and `expired`.\n     */\n    operator fun get(url: HttpUrl, accepted: MutableList<Cookie>, expired: MutableList<Cookie>) {\n        val now = System.currentTimeMillis()\n        val iterator = map.entries.iterator()\n        while (iterator.hasNext()) {\n            val cookie = iterator.next().value\n            if (cookie.expiresAt <= now) {\n                iterator.remove()\n                expired.add(cookie)\n            } else if (cookie.matches(url)) {\n                accepted.add(cookie)\n            }\n        }\n    }\n\n    fun get(name: String, domain: String, path: String) = map[Key(name, domain, path)]\n    internal data class Key(\n        val name: String,\n        val domain: String,\n        val path: String,\n    ) {\n        constructor(cookie: Cookie) : this(cookie.name, cookie.domain, cookie.path)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/network/InetValidator.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.network\n\nimport java.util.regex.Pattern\n\nobject InetValidator {\n    private const val IPV4_REGEX = \"^(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})$\"\n    private val IPV4_PATTERN = Pattern.compile(IPV4_REGEX)\n\n    fun isValidInet4Address(inet4Address: String?): Boolean {\n        if (null == inet4Address) {\n            return false\n        }\n        val matcher = IPV4_PATTERN.matcher(inet4Address)\n        if (!matcher.find()) {\n            return false\n        }\n\n        // verify that address subgroups are legal\n        for (i in 1..4) {\n            val ipSegment = matcher.group(i)\n            if (ipSegment == null || ipSegment.isEmpty()) {\n                return false\n            }\n            val iIpSegment: Int = try {\n                ipSegment.toInt()\n            } catch (_: NumberFormatException) {\n                return false\n            }\n            if (iIpSegment > 255) {\n                return false\n            }\n            if (ipSegment.length > 1 && ipSegment.startsWith(\"0\")) {\n                return false\n            }\n        }\n        return true\n    }\n\n    fun isValidInetPort(inetPort: Int): Boolean = inetPort in 0..65535\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/network/StatusCodeException.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.network\n\nimport android.util.SparseArray\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.R\n\nclass StatusCodeException(val responseCode: Int) : Exception() {\n    override val message: String = ERROR_MESSAGE_ARRAY[responseCode, DEFAULT_ERROR_MESSAGE]\n\n    val isIdentifiedResponseCode: Boolean\n        get() = DEFAULT_ERROR_MESSAGE != message\n\n    override fun getLocalizedMessage(): String = message\n\n    companion object {\n        private val ERROR_MESSAGE_ARRAY = SparseArray<String>(24)\n        private const val DEFAULT_ERROR_MESSAGE = \"Error response code\"\n\n        init {\n            val resources = EhApplication.application.resources\n            ERROR_MESSAGE_ARRAY.append(400, resources.getString(R.string.error_status_code_400))\n            ERROR_MESSAGE_ARRAY.append(401, resources.getString(R.string.error_status_code_401))\n            ERROR_MESSAGE_ARRAY.append(402, resources.getString(R.string.error_status_code_402))\n            ERROR_MESSAGE_ARRAY.append(403, resources.getString(R.string.error_status_code_403))\n            ERROR_MESSAGE_ARRAY.append(404, resources.getString(R.string.error_status_code_404))\n            ERROR_MESSAGE_ARRAY.append(405, resources.getString(R.string.error_status_code_405))\n            ERROR_MESSAGE_ARRAY.append(406, resources.getString(R.string.error_status_code_406))\n            ERROR_MESSAGE_ARRAY.append(407, resources.getString(R.string.error_status_code_407))\n            ERROR_MESSAGE_ARRAY.append(408, resources.getString(R.string.error_status_code_408))\n            ERROR_MESSAGE_ARRAY.append(409, resources.getString(R.string.error_status_code_409))\n            ERROR_MESSAGE_ARRAY.append(410, resources.getString(R.string.error_status_code_410))\n            ERROR_MESSAGE_ARRAY.append(411, resources.getString(R.string.error_status_code_411))\n            ERROR_MESSAGE_ARRAY.append(412, resources.getString(R.string.error_status_code_412))\n            ERROR_MESSAGE_ARRAY.append(413, resources.getString(R.string.error_status_code_413))\n            ERROR_MESSAGE_ARRAY.append(414, resources.getString(R.string.error_status_code_414))\n            ERROR_MESSAGE_ARRAY.append(415, resources.getString(R.string.error_status_code_415))\n            ERROR_MESSAGE_ARRAY.append(416, resources.getString(R.string.error_status_code_416))\n            ERROR_MESSAGE_ARRAY.append(417, resources.getString(R.string.error_status_code_417))\n            ERROR_MESSAGE_ARRAY.append(500, resources.getString(R.string.error_status_code_500))\n            ERROR_MESSAGE_ARRAY.append(501, resources.getString(R.string.error_status_code_501))\n            ERROR_MESSAGE_ARRAY.append(502, resources.getString(R.string.error_status_code_502))\n            ERROR_MESSAGE_ARRAY.append(503, resources.getString(R.string.error_status_code_503))\n            ERROR_MESSAGE_ARRAY.append(504, resources.getString(R.string.error_status_code_504))\n            ERROR_MESSAGE_ARRAY.append(505, resources.getString(R.string.error_status_code_505))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/network/UrlBuilder.kt",
    "content": "/*\n * Copyright (C) 2014-2015 Hippo Seven\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.hippo.network\n\nclass UrlBuilder(private var mRootUrl: String) {\n    private var mQueryMap: MutableMap<String, Any> = HashMap()\n    fun addQuery(key: String, value: Any) {\n        mQueryMap[key] = value\n    }\n\n    fun build(): String = if (mQueryMap.isEmpty()) {\n        mRootUrl\n    } else {\n        val sb = StringBuilder(mRootUrl)\n        sb.append(\"?\")\n        val iter: Iterator<String> = mQueryMap.keys.iterator()\n        if (iter.hasNext()) {\n            val key = iter.next()\n            val value = mQueryMap[key]\n            sb.append(key).append(\"=\").append(value)\n        }\n        while (iter.hasNext()) {\n            val key = iter.next()\n            val value = mQueryMap[key]\n            sb.append(\"&\").append(key).append(\"=\").append(value)\n        }\n        sb.toString()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/okhttp/ChromeRequestBuilder.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.okhttp\n\nimport com.hippo.ehviewer.Settings\nimport com.hippo.ehviewer.util.WebViewVersion\nimport okhttp3.Request\n\nval CHROME_USER_AGENT = \"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$WebViewVersion.0.0.0 Mobile Safari/537.36\"\nprivate const val CHROME_ACCEPT = \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\"\nprivate const val CHROME_ACCEPT_LANGUAGE = \"en-US,en;q=0.9\"\n\nopen class ChromeRequestBuilder(url: String) : Request.Builder() {\n    init {\n        this.url(url)\n        this.addHeader(\"User-Agent\", Settings.userAgent!!)\n        this.addHeader(\"Accept\", CHROME_ACCEPT)\n        this.addHeader(\"Accept-Language\", CHROME_ACCEPT_LANGUAGE)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/preference/DialogPreference.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.preference\n\nimport android.app.Dialog\nimport android.content.Context\nimport android.content.DialogInterface\nimport android.content.SharedPreferences\nimport android.graphics.drawable.Drawable\nimport android.os.Bundle\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.view.WindowManager\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.withStyledAttributes\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.R\n\n/**\n * A base class for [Preference] objects that are\n * dialog-based. These preferences will, when clicked, open a dialog showing the\n * actual preference controls.\n */\nabstract class DialogPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : Preference(\n    context,\n    attrs,\n),\n    DialogInterface.OnClickListener,\n    DialogInterface.OnDismissListener {\n    private var mBuilder: AlertDialog.Builder? = null\n    private var mDialogTitle: CharSequence? = null\n    private var mDialogIcon: Drawable? = null\n    private var mPositiveButtonText: CharSequence? = null\n    private var mNegativeButtonText: CharSequence? = null\n    private var mDialogLayoutResId: Int = 0\n\n    /**\n     * The dialog, if it is showing.\n     */\n    private var mDialog: AlertDialog? = null\n\n    /**\n     * Which button was clicked.\n     */\n    private var mWhichButtonClicked = 0\n\n    init {\n        context.withStyledAttributes(attrs, R.styleable.DialogPreference, 0, 0) {\n            mDialogTitle = getString(R.styleable.DialogPreference_dialogTitle)\n            if (mDialogTitle == null) {\n                // Fallback on the regular title of the preference\n                // (the one that is seen in the list)\n                mDialogTitle = title\n            }\n            mDialogIcon = getDrawable(R.styleable.DialogPreference_dialogIcon)\n            mPositiveButtonText = getString(R.styleable.DialogPreference_positiveButtonText)\n            mNegativeButtonText = getString(R.styleable.DialogPreference_negativeButtonText)\n            mDialogLayoutResId =\n                getResourceId(R.styleable.DialogPreference_dialogLayout, mDialogLayoutResId)\n        }\n    }\n\n    /**\n     * Returns the title to be shown on subsequent dialogs.\n     * @return The title.\n     *\n     * Sets the title of the dialog. This will be shown on subsequent dialogs.\n     * @param dialogTitle The title.\n     */\n    var dialogTitle: CharSequence?\n        get() = mDialogTitle\n        set(dialogTitle) {\n            mDialogTitle = dialogTitle\n        }\n\n    /**\n     * Returns the icon to be shown on subsequent dialogs.\n     * @return The icon, as a [Drawable].\n     *\n     * Sets the icon of the dialog. This will be shown on subsequent dialogs.\n     * @param dialogIcon The icon, as a [Drawable].\n     */\n    var dialogIcon: Drawable?\n        get() = mDialogIcon\n        set(dialogIcon) {\n            mDialogIcon = dialogIcon\n        }\n\n    /**\n     * Returns the text of the negative button to be shown on subsequent\n     * dialogs.\n     * @return The text of the positive button.\n     *\n     * Sets the text of the negative button of the dialog. This will be shown on\n     * subsequent dialogs.\n     * @param positiveButtonText The text of the negative button.\n     */\n    var positiveButtonText: CharSequence?\n        get() = mPositiveButtonText\n        set(positiveButtonText) {\n            mPositiveButtonText = positiveButtonText\n        }\n\n    /**\n     * Returns the text of the negative button to be shown on subsequent\n     * dialogs.\n     * @return The text of the negative button.\n     *\n     * Sets the text of the negative button of the dialog. This will be shown on\n     * subsequent dialogs.\n     * @param negativeButtonText The text of the negative button.\n     */\n    var negativeButtonText: CharSequence?\n        get() = mNegativeButtonText\n        set(negativeButtonText) {\n            mNegativeButtonText = negativeButtonText\n        }\n\n    /**\n     * Returns the layout resource that is used as the content View for\n     * subsequent dialogs.\n     * @return The layout resource.\n     *\n     * Sets the layout resource that is inflated as the [View] to be shown\n     * as the content View of subsequent dialogs.\n     * @param dialogLayoutResource The layout resource ID to be inflated.\n     */\n    var dialogLayoutResource: Int\n        get() = mDialogLayoutResId\n        set(dialogLayoutResource) {\n            mDialogLayoutResId = dialogLayoutResource\n        }\n\n    /**\n     * @param dialogTitleResId The dialog title as a resource.\n     * @see .setDialogTitle\n     */\n    fun setDialogTitle(dialogTitleResId: Int) {\n        mDialogTitle = context.getString(dialogTitleResId)\n    }\n\n    /**\n     * Sets the icon (resource ID) of the dialog. This will be shown on\n     * subsequent dialogs.\n     *\n     * @param dialogIconRes The icon, as a resource ID.\n     */\n    fun setDialogIcon(@DrawableRes dialogIconRes: Int) {\n        mDialogIcon = ContextCompat.getDrawable(context, dialogIconRes)\n    }\n\n    /**\n     * @param positiveButtonTextResId The positive button text as a resource.\n     * @see .setPositiveButtonText\n     */\n    fun setPositiveButtonText(@StringRes positiveButtonTextResId: Int) {\n        mPositiveButtonText = context.getString(positiveButtonTextResId)\n    }\n\n    /**\n     * @param negativeButtonTextResId The negative button text as a resource.\n     * @see .setNegativeButtonText\n     */\n    fun setNegativeButtonText(@StringRes negativeButtonTextResId: Int) {\n        mNegativeButtonText = context.getString(negativeButtonTextResId)\n    }\n\n    /**\n     * Prepares the dialog builder to be shown when the preference is clicked.\n     * Use this to set custom properties on the dialog.\n     *\n     * Do not [AlertDialog.Builder.create] or\n     * [AlertDialog.Builder.show].\n     */\n    protected open fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {}\n\n    override fun onClick() {\n        if (mDialog != null && mDialog!!.isShowing) return\n        showDialog(null)\n    }\n\n    /**\n     * Shows the dialog associated with this Preference. This is normally initiated\n     * automatically on clicking on the preference. Call this method if you need to\n     * show the dialog on some other event.\n     *\n     * @param state Optional instance state to restore on the dialog\n     */\n    protected open fun showDialog(state: Bundle?) {\n        val context = context\n\n        mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE\n        mBuilder = AlertDialog.Builder(context)\n            .setTitle(mDialogTitle)\n            .setIcon(mDialogIcon)\n            .setPositiveButton(mPositiveButtonText, this)\n            .setNegativeButton(mNegativeButtonText, this)\n\n        onCreateDialogView()?.let { view ->\n            view.parent?.let {\n                (it as ViewGroup).removeView(view)\n            }\n            onBindDialogView(view)\n            mBuilder!!.setView(view)\n        }\n\n        onPrepareDialogBuilder(mBuilder!!)\n        // PreferenceUtils.registerOnActivityDestroyListener(this, this);\n\n        // Create the dialog\n        mDialog = mBuilder!!.create()\n        val dialog = mDialog!!\n        state?.let { dialog.onRestoreInstanceState(it) }\n        if (needInputMethod) {\n            requestInputMethod(dialog)\n        }\n        dialog.setOnDismissListener(this)\n        dialog.show()\n\n        onDialogCreated(dialog)\n    }\n\n    /**\n     * Returns whether the preference needs to display a soft input method when the dialog\n     * is displayed. Default is false. Subclasses should override this method if they need\n     * the soft input method brought up automatically.\n     */\n    open val needInputMethod: Boolean = false\n\n    /**\n     * Sets the required flags on the dialog window to enable input method window to show up.\n     */\n    private fun requestInputMethod(dialog: Dialog) {\n        dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)\n    }\n\n    /**\n     * Creates the content view for the dialog (if a custom content view is\n     * required). By default, it inflates the dialog layout resource if it is\n     * set.\n     *\n     * @return The content View for the dialog.\n     * @see .setLayoutResource\n     */\n    protected open fun onCreateDialogView(): View? {\n        if (mDialogLayoutResId == 0) return null\n        val inflater = LayoutInflater.from(mBuilder!!.context)\n        return inflater.inflate(mDialogLayoutResId, null)\n    }\n\n    /**\n     * Binds views in the content View of the dialog to data.\n     *\n     * @param view The content View of the dialog, if it is custom.\n     */\n    protected open fun onBindDialogView(view: View) {}\n\n    protected open fun onDialogCreated(dialog: AlertDialog) {}\n\n    override fun onClick(dialog: DialogInterface, which: Int) {\n        mWhichButtonClicked = which\n    }\n\n    override fun onDismiss(dialog: DialogInterface) {\n        mDialog = null\n        onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE)\n    }\n\n    /**\n     * Called when the dialog is dismissed and should be used to save data to\n     * the [SharedPreferences].\n     *\n     * @param positiveResult Whether the positive button was clicked (true), or\n     * the negative button was clicked or the dialog was canceled (false).\n     */\n    protected open fun onDialogClosed(positiveResult: Boolean) {}\n\n    /**\n     * Gets the dialog that is shown by this preference.\n     *\n     * @return The dialog, or null if a dialog is not being shown.\n     */\n    val dialog: Dialog?\n        get() = mDialog\n\n    override fun onSaveInstanceState(): Parcelable? {\n        val superState = super.onSaveInstanceState()\n        if (mDialog == null || !mDialog!!.isShowing) {\n            return superState\n        }\n\n        val myState = SavedState(superState)\n        myState.isDialogShowing = true\n        myState.dialogBundle = mDialog!!.onSaveInstanceState()\n        return myState\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable?) {\n        if (state == null || state.javaClass != SavedState::class.java) {\n            // Didn't save state for us in onSaveInstanceState\n            super.onRestoreInstanceState(state)\n            return\n        }\n\n        val myState = state as SavedState\n        super.onRestoreInstanceState(myState.superState)\n        if (myState.isDialogShowing) {\n            showDialog(myState.dialogBundle)\n        }\n    }\n\n    private class SavedState : BaseSavedState {\n        var isDialogShowing: Boolean = false\n        var dialogBundle: Bundle? = null\n\n        constructor(source: Parcel) : super(source) {\n            isDialogShowing = source.readInt() == 1\n            dialogBundle = source.readBundle(DialogPreference::class.java.classLoader)\n        }\n\n        constructor(superState: Parcelable?) : super(superState)\n\n        override fun writeToParcel(dest: Parcel, flags: Int) {\n            super.writeToParcel(dest, flags)\n            dest.writeInt(if (isDialogShowing) 1 else 0)\n            dest.writeBundle(dialogBundle)\n        }\n\n        companion object CREATOR : Parcelable.Creator<SavedState> {\n            override fun createFromParcel(`in`: Parcel): SavedState = SavedState(`in`)\n\n            override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/preference/UrlPreference.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.preference\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport androidx.core.content.withStyledAttributes\nimport androidx.preference.Preference\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.UrlOpener\n\nclass UrlPreference(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : Preference(context, attrs) {\n    private var mUrl: String? = null\n\n    init {\n        context.withStyledAttributes(attrs, R.styleable.UrlPreference, 0, 0) {\n            mUrl = getString(R.styleable.UrlPreference_url)\n        }\n    }\n\n    override fun getSummary(): CharSequence? = mUrl ?: super.getSummary()\n\n    override fun onClick() {\n        UrlOpener.openUrl(context, mUrl, true)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/scene/Announcer.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.scene\n\nimport android.os.Bundle\n\nclass Announcer(var clazz: Class<*>) {\n    var args: Bundle? = null\n    var tranHelper: TransitionHelper? = null\n    var requestFrom: SceneFragment? = null\n    var requestCode = 0\n\n    fun setArgs(args: Bundle?): Announcer {\n        this.args = args\n        return this\n    }\n\n    fun setTranHelper(tranHelper: TransitionHelper?): Announcer {\n        this.tranHelper = tranHelper\n        return this\n    }\n\n    fun setRequestCode(requestFrom: SceneFragment?, requestCode: Int): Announcer {\n        this.requestFrom = requestFrom\n        this.requestCode = requestCode\n        return this\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/scene/SceneApplication.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.scene\n\nimport android.app.Application\nimport android.util.SparseArray\nimport com.hippo.yorozuya.IntIdGenerator\n\nabstract class SceneApplication : Application() {\n    private val mIdGenerator = IntIdGenerator()\n    private val mStageMap = SparseArray<StageActivity>()\n\n    fun registerStageActivity(stage: StageActivity) {\n        val id = mIdGenerator.nextId()\n        mStageMap.put(id, stage)\n        stage.onRegister(id)\n    }\n\n    fun registerStageActivity(stage: StageActivity, id: Int) {\n        check(mStageMap.indexOfKey(id) < 0) { \"The id exists: $id\" }\n        mStageMap.put(id, stage)\n        stage.onRegister(id)\n    }\n\n    fun unregisterStageActivity(id: Int) {\n        val index = mStageMap.indexOfKey(id)\n        if (index >= 0) {\n            val stage = mStageMap.valueAt(index)\n            mStageMap.remove(id)\n            stage.onUnregister()\n        }\n    }\n\n    fun findStageActivityById(id: Int): StageActivity = mStageMap[id]\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/scene/SceneFragment.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.scene\n\nimport android.app.assist.AssistContent\nimport android.os.Bundle\nimport android.view.View\nimport androidx.annotation.IntDef\nimport androidx.fragment.app.Fragment\nimport com.hippo.ehviewer.R\nimport com.hippo.yorozuya.collect.IntList\nimport kotlin.math.min\nimport rikka.core.res.resolveDrawable\n\nopen class SceneFragment : Fragment() {\n    var result: Bundle? = null\n    private var resultCode = RESULT_CANCELED\n    private var mRequestSceneTagList: MutableList<String> = ArrayList(0)\n    private var mRequestCodeList = IntList()\n\n    open fun onNewArguments(args: Bundle) {}\n\n    fun startScene(announcer: Announcer, horizontal: Boolean) {\n        val activity = activity\n        if (activity is StageActivity) {\n            activity.startScene(announcer, horizontal)\n        }\n    }\n\n    fun startScene(announcer: Announcer) {\n        val activity = activity\n        if (activity is StageActivity) {\n            activity.startScene(announcer)\n        }\n    }\n\n    fun finish(transitionHelper: TransitionHelper? = null) {\n        val activity = activity\n        if (activity is StageActivity) {\n            activity.finishScene(this, transitionHelper)\n        }\n    }\n\n    fun finishStage() {\n        val activity = activity\n        activity?.finish()\n    }\n\n    /**\n     * @return negative for error\n     */\n    val stackIndex: Int\n        get() {\n            val activity = activity\n            return if (activity is StageActivity) {\n                activity.getSceneIndex(this)\n            } else {\n                -1\n            }\n        }\n\n    open fun onBackPressed() {\n        finish()\n    }\n\n    open fun onProvideAssistContent(outContent: AssistContent) {}\n\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n        view.setTag(R.id.fragment_tag, tag)\n        view.background =\n            requireActivity().getTheme().resolveDrawable(android.R.attr.windowBackground)\n        // Notify\n        val activity = activity\n        if (activity is StageActivity) {\n            activity.onSceneViewCreated(this, savedInstanceState)\n        }\n    }\n\n    override fun onDestroyView() {\n        super.onDestroyView()\n        // Notify\n        val activity = activity\n        if (activity is StageActivity) {\n            activity.onSceneViewDestroyed(this)\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        val activity = activity\n        if (activity is StageActivity) {\n            activity.onSceneDestroyed(this)\n        }\n    }\n\n    fun addRequest(requestSceneTag: String, requestCode: Int) {\n        mRequestSceneTagList.add(requestSceneTag)\n        mRequestCodeList.add(requestCode)\n    }\n\n    fun returnResult(stage: StageActivity) {\n        for (i in 0 until min(mRequestSceneTagList.size.toDouble(), mRequestCodeList.size.toDouble()).toInt()) {\n            val tag = mRequestSceneTagList[i]\n            val code = mRequestCodeList[i]\n            val scene = stage.findSceneByTag(tag)\n            scene?.onSceneResult(code, resultCode, result)\n        }\n        mRequestSceneTagList.clear()\n        mRequestCodeList.clear()\n    }\n\n    protected open fun onSceneResult(requestCode: Int, resultCode: Int, data: Bundle?) {}\n\n    fun setResult(resultCode: Int, result: Bundle?) {\n        this.resultCode = resultCode\n        this.result = result\n    }\n\n    @IntDef(LAUNCH_MODE_STANDARD, LAUNCH_MODE_SINGLE_TOP, LAUNCH_MODE_SINGLE_TASK)\n    @Retention(\n        AnnotationRetention.SOURCE,\n    )\n    annotation class LaunchMode\n\n    companion object {\n        const val LAUNCH_MODE_STANDARD = 0\n        const val LAUNCH_MODE_SINGLE_TOP = 1\n        const val LAUNCH_MODE_SINGLE_TASK = 2\n\n        /**\n         * Standard scene result: operation canceled.\n         */\n        const val RESULT_CANCELED = 0\n\n        /**\n         * Standard scene result: operation succeeded.\n         */\n        const val RESULT_OK = -1\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/scene/StageActivity.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.scene\n\nimport android.annotation.SuppressLint\nimport android.app.assist.AssistContent\nimport android.content.Intent\nimport android.os.Bundle\nimport android.util.Log\nimport android.view.View\nimport androidx.fragment.app.Fragment\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.ui.EhActivity\nimport com.hippo.scene.SceneFragment.LaunchMode\nimport com.hippo.yorozuya.AssertUtils\nimport com.hippo.yorozuya.IntIdGenerator\nimport java.util.concurrent.atomic.AtomicInteger\n\nabstract class StageActivity : EhActivity() {\n    private val mSceneTagList = ArrayList<String?>()\n    private val mDelaySceneTagList = ArrayList<String?>()\n    private val mIdGenerator = AtomicInteger()\n    private val mSceneViewComparator = SceneViewComparator()\n    private var stageId = IntIdGenerator.INVALID_ID\n    abstract val containerViewId: Int\n\n    /**\n     * @return `true` for start scene\n     */\n    private fun startSceneFromIntent(intent: Intent): Boolean {\n        val clazzStr = intent.getStringExtra(KEY_SCENE_NAME) ?: return false\n        val clazz: Class<*> = try {\n            Class.forName(clazzStr)\n        } catch (e: ClassNotFoundException) {\n            Log.e(TAG, \"Can't find class $clazzStr\", e)\n            return false\n        }\n        val args = intent.getBundleExtra(KEY_SCENE_ARGS)\n        val announcer = onStartSceneFromIntent(clazz, args) ?: return false\n        startScene(announcer)\n        return true\n    }\n\n    /**\n     * Start scene from `Intent`, it might be not safe,\n     * Correct it here.\n     *\n     * @return `null` for do not start scene\n     */\n    protected open fun onStartSceneFromIntent(clazz: Class<*>, args: Bundle?): Announcer? = Announcer(clazz).setArgs(args)\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        if (ACTION_START_SCENE != intent.action || !startSceneFromIntent(intent)) {\n            onUnrecognizedIntent(intent)\n        }\n    }\n\n    protected abstract val launchAnnouncer: Announcer?\n\n    /**\n     * Can't recognize intent in first time `onCreate` and `onNewIntent`,\n     * null included.\n     */\n    protected open fun onUnrecognizedIntent(intent: Intent?) {}\n\n    /**\n     * Call `setContentView` here. Do **NOT** call `startScene` here\n     */\n    protected abstract fun onCreate2(savedInstanceState: Bundle?)\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        if (savedInstanceState != null) {\n            stageId = savedInstanceState.getInt(KEY_STAGE_ID, IntIdGenerator.INVALID_ID)\n            val list = savedInstanceState.getStringArrayList(KEY_SCENE_TAG_LIST)\n            if (list != null) {\n                mSceneTagList.addAll(list)\n                mDelaySceneTagList.addAll(list)\n            }\n            mIdGenerator.lazySet(savedInstanceState.getInt(KEY_NEXT_ID))\n        }\n        if (stageId == IntIdGenerator.INVALID_ID) {\n            (applicationContext as SceneApplication).registerStageActivity(this)\n        } else {\n            (applicationContext as SceneApplication).registerStageActivity(this, stageId)\n        }\n\n        // Create layout\n        onCreate2(savedInstanceState)\n        val intent = intent\n        if (savedInstanceState == null) {\n            if (intent != null) {\n                val action = intent.action\n                if (Intent.ACTION_MAIN == action) {\n                    val announcer = launchAnnouncer\n                    if (announcer != null) {\n                        startScene(announcer)\n                        return\n                    }\n                } else if (ACTION_START_SCENE == action) {\n                    if (startSceneFromIntent(intent)) {\n                        return\n                    }\n                }\n            }\n\n            // Can't recognize intent\n            onUnrecognizedIntent(intent)\n        }\n    }\n\n    override fun onSaveInstanceState(outState: Bundle) {\n        super.onSaveInstanceState(outState)\n        outState.putInt(KEY_STAGE_ID, stageId)\n        outState.putStringArrayList(KEY_SCENE_TAG_LIST, mSceneTagList)\n        outState.putInt(KEY_NEXT_ID, mIdGenerator.getAndIncrement())\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        (applicationContext as SceneApplication).unregisterStageActivity(stageId)\n    }\n\n    open fun onSceneViewCreated(scene: SceneFragment, savedInstanceState: Bundle?) {}\n\n    open fun onSceneViewDestroyed(scene: SceneFragment) {}\n\n    fun onSceneDestroyed(scene: SceneFragment) {\n        mDelaySceneTagList.remove(scene.tag)\n    }\n\n    internal fun onRegister(id: Int) {\n        stageId = id\n    }\n\n    internal fun onUnregister() {}\n\n    protected open fun onTransactScene() {}\n\n    val sceneCount: Int\n        get() = mSceneTagList.size\n\n    private fun getSceneLaunchMode(clazz: Class<*>): Int {\n        val integer = sLaunchModeMap[clazz]\n        return integer ?: throw RuntimeException(\"Not register \" + clazz.getName())\n    }\n\n    private fun newSceneInstance(clazz: Class<*>): SceneFragment = try {\n        clazz.getDeclaredConstructor().newInstance() as SceneFragment\n    } catch (e: InstantiationException) {\n        throw IllegalStateException(\"Can't instance \" + clazz.getName(), e)\n    } catch (e: IllegalAccessException) {\n        throw IllegalStateException(\n            \"The constructor of \" +\n                clazz.getName() + \" is not visible\",\n            e,\n        )\n    } catch (e: ClassCastException) {\n        throw IllegalStateException(clazz.getName() + \" can not cast to scene\", e)\n    }\n\n    fun startScene(announcer: Announcer, horizontal: Boolean = false) {\n        val clazz = announcer.clazz\n        val args = announcer.args\n        val tranHelper = announcer.tranHelper\n        val fragmentManager = supportFragmentManager\n        val launchMode = getSceneLaunchMode(clazz)\n\n        // Check LAUNCH_MODE_SINGLE_TASK\n        if (launchMode == SceneFragment.LAUNCH_MODE_SINGLE_TASK) {\n            var i = 0\n            val n = mSceneTagList.size\n            while (i < n) {\n                val tag = mSceneTagList[i]\n                val fragment = fragmentManager.findFragmentByTag(tag)\n                if (fragment == null) {\n                    Log.e(TAG, \"Can't find fragment with tag: $tag\")\n                    i++\n                    continue\n                }\n                if (clazz.isInstance(fragment)) { // Get it\n                    val transaction = fragmentManager.beginTransaction()\n\n                    // Use default animation\n                    if (horizontal) {\n                        transaction.setCustomAnimations(\n                            R.anim.scene_open_enter_horizontal,\n                            R.anim.scene_open_exit,\n                        )\n                    } else {\n                        transaction.setCustomAnimations(\n                            R.anim.scene_open_enter,\n                            R.anim.scene_open_exit,\n                        )\n                    }\n\n                    // Remove top fragments\n                    for (j in i + 1 until n) {\n                        val topTag = mSceneTagList[j]\n                        val topFragment = fragmentManager.findFragmentByTag(topTag)\n                        if (null == topFragment) {\n                            Log.e(TAG, \"Can't find fragment with tag: $topTag\")\n                            continue\n                        }\n                        // Clear shared element\n                        topFragment.sharedElementEnterTransition = null\n                        topFragment.sharedElementReturnTransition = null\n                        topFragment.enterTransition = null\n                        topFragment.exitTransition = null\n                        // Remove it\n                        transaction.remove(topFragment)\n                    }\n\n                    // Remove tag from index i+1\n                    mSceneTagList.subList(i + 1, mSceneTagList.size).clear()\n                    mDelaySceneTagList.subList(i + 1, mDelaySceneTagList.size).clear()\n\n                    // Attach fragment\n                    if (fragment.isDetached) {\n                        transaction.attach(fragment)\n                    }\n\n                    // Commit\n                    transaction.commitAllowingStateLoss()\n                    onTransactScene()\n\n                    // New arguments\n                    if (args != null && fragment is SceneFragment) {\n                        // TODO Call onNewArguments when view created ?\n                        fragment.onNewArguments(args)\n                    }\n                    return\n                }\n                i++\n            }\n        }\n\n        // Get current fragment\n        var currentScene: SceneFragment? = null\n        if (mSceneTagList.isNotEmpty()) {\n            // Get last tag\n            val tag = mSceneTagList[mSceneTagList.size - 1]\n            val fragment = fragmentManager.findFragmentByTag(tag)\n            if (fragment != null) {\n                AssertUtils.assertTrue(fragment is SceneFragment)\n                currentScene = fragment as SceneFragment?\n            }\n        }\n\n        // Check LAUNCH_MODE_SINGLE_TASK\n        if (clazz.isInstance(currentScene) && launchMode == SceneFragment.LAUNCH_MODE_SINGLE_TOP) {\n            if (args != null) {\n                currentScene!!.onNewArguments(args)\n            }\n            return\n        }\n\n        // Create new scene\n        val newScene = newSceneInstance(clazz)\n        newScene.setArguments(args)\n\n        // Create new scene tag\n        val newTag = mIdGenerator.getAndIncrement().toString()\n\n        // Add new tag to list\n        mSceneTagList.add(newTag)\n        mDelaySceneTagList.add(newTag)\n        val transaction = fragmentManager.beginTransaction()\n        // Animation\n        if (currentScene != null) {\n            if (tranHelper == null ||\n                !tranHelper.onTransition(\n                    this,\n                    transaction,\n                    currentScene,\n                    newScene,\n                )\n            ) {\n                // Clear shared item\n                currentScene.sharedElementEnterTransition = null\n                currentScene.sharedElementReturnTransition = null\n                currentScene.enterTransition = null\n                currentScene.exitTransition = null\n                newScene.sharedElementEnterTransition = null\n                newScene.sharedElementReturnTransition = null\n                newScene.enterTransition = null\n                newScene.exitTransition = null\n                // Set default animation\n                if (horizontal) {\n                    transaction.setCustomAnimations(\n                        R.anim.scene_open_enter_horizontal,\n                        R.anim.scene_open_exit,\n                    )\n                } else {\n                    transaction.setCustomAnimations(R.anim.scene_open_enter, R.anim.scene_open_exit)\n                }\n            }\n            // Detach current scene\n            if (!currentScene.isDetached) {\n                transaction.detach(currentScene)\n            } else {\n                Log.e(TAG, \"Current scene is detached\")\n            }\n        }\n\n        // Add new scene\n        transaction.add(containerViewId, newScene, newTag)\n\n        // Commit\n        transaction.commitAllowingStateLoss()\n        onTransactScene()\n\n        // Check request\n        if (announcer.requestFrom != null && announcer.requestFrom!!.tag != null) {\n            newScene.addRequest(announcer.requestFrom!!.tag!!, announcer.requestCode)\n        }\n    }\n\n    fun startSceneFirstly(announcer: Announcer) {\n        val clazz = announcer.clazz\n        val args = announcer.args\n        val fragmentManager = supportFragmentManager\n        val launchMode = getSceneLaunchMode(clazz)\n        val forceNewScene = launchMode == SceneFragment.LAUNCH_MODE_STANDARD\n        var createNewScene = true\n        var findScene = false\n        var scene: SceneFragment? = null\n        val transaction = fragmentManager.beginTransaction()\n\n        // Set default animation\n        transaction.setCustomAnimations(R.anim.scene_open_enter, R.anim.scene_open_exit)\n        var findSceneTag: String? = null\n        var i = 0\n        val n = mSceneTagList.size\n        while (i < n) {\n            val tag = mSceneTagList[i]\n            val fragment = fragmentManager.findFragmentByTag(tag)\n            if (fragment == null) {\n                Log.e(TAG, \"Can't find fragment with tag: $tag\")\n                i++\n                continue\n            }\n\n            // Clear shared element\n            fragment.sharedElementEnterTransition = null\n            fragment.sharedElementReturnTransition = null\n            fragment.enterTransition = null\n            fragment.exitTransition = null\n\n            // Check is target scene\n            if (!forceNewScene &&\n                !findScene &&\n                clazz.isInstance(fragment) &&\n                (launchMode == SceneFragment.LAUNCH_MODE_SINGLE_TASK || !fragment.isDetached)\n            ) {\n                scene = fragment as SceneFragment?\n                findScene = true\n                createNewScene = false\n                findSceneTag = tag\n                if (fragment.isDetached) {\n                    transaction.attach(fragment)\n                }\n            } else {\n                // Remove it\n                transaction.remove(fragment)\n            }\n            i++\n        }\n\n        // Handle tag list\n        mSceneTagList.clear()\n        if (null != findSceneTag) {\n            mSceneTagList.add(findSceneTag)\n        }\n        if (createNewScene) {\n            scene = newSceneInstance(clazz)\n            scene.setArguments(args)\n\n            // Create scene tag\n            val tag = mIdGenerator.getAndIncrement().toString()\n\n            // Add tag to list\n            mSceneTagList.add(tag)\n            mDelaySceneTagList.add(tag)\n\n            // Add scene\n            transaction.add(containerViewId, scene, tag)\n        }\n\n        // Commit\n        transaction.commitAllowingStateLoss()\n        onTransactScene()\n        if (!createNewScene && args != null) {\n            // TODO Call onNewArguments when view created ?\n            scene!!.onNewArguments(args)\n        }\n    }\n\n    fun getSceneIndex(scene: SceneFragment): Int = getTagIndex(scene.tag)\n\n    private fun getTagIndex(tag: String?): Int = mSceneTagList.indexOf(tag)\n\n    fun sortSceneViews(views: ArrayList<View>) {\n        views.sortWith(mSceneViewComparator)\n    }\n\n    fun finishScene(scene: SceneFragment, transitionHelper: TransitionHelper? = null) {\n        finishScene(scene.tag, transitionHelper)\n    }\n\n    private fun finishScene(tag: String?, transitionHelper: TransitionHelper?) {\n        val fragmentManager = supportFragmentManager\n\n        // Get scene\n        val scene = fragmentManager.findFragmentByTag(tag)\n        if (scene == null) {\n            Log.e(TAG, \"finishScene: Can't find scene by tag: $tag\")\n            return\n        }\n\n        // Get scene index\n        val index = mSceneTagList.indexOf(tag)\n        if (index < 0) {\n            Log.e(TAG, \"finishScene: Can't find the tag in tag list: $tag\")\n            return\n        }\n        if (mSceneTagList.size == 1) {\n            // It is the last fragment, finish Activity now\n            Log.i(TAG, \"finishScene: It is the last scene, finish activity now\")\n            finish()\n            return\n        }\n        var next: Fragment? = null\n        if (index == mSceneTagList.size - 1) {\n            // It is first fragment, show the next one\n            next = fragmentManager.findFragmentByTag(mSceneTagList[index - 1])\n        }\n        val transaction = fragmentManager.beginTransaction()\n        if (next != null) {\n            if (transitionHelper == null ||\n                !transitionHelper.onTransition(\n                    this,\n                    transaction,\n                    scene,\n                    next,\n                )\n            ) {\n                // Clear shared item\n                scene.sharedElementEnterTransition = null\n                scene.sharedElementReturnTransition = null\n                scene.enterTransition = null\n                scene.exitTransition = null\n                next.sharedElementEnterTransition = null\n                next.sharedElementReturnTransition = null\n                next.enterTransition = null\n                next.exitTransition = null\n                // Do not show animate if it is not the first fragment\n                transaction.setCustomAnimations(0, R.anim.scene_close_exit)\n            }\n            // Attach fragment\n            transaction.attach(next)\n        }\n        transaction.remove(scene)\n        transaction.commitAllowingStateLoss()\n        onTransactScene()\n\n        // Remove tag\n        mSceneTagList.removeAt(index)\n\n        // Return result\n        if (scene is SceneFragment) {\n            scene.returnResult(this)\n        }\n    }\n\n    fun refreshTopScene() {\n        val index = mSceneTagList.size - 1\n        if (index < 0) {\n            return\n        }\n        val tag = mSceneTagList[index]\n        val fragmentManager = supportFragmentManager\n        val fragment = fragmentManager.findFragmentByTag(tag) ?: return\n        fragmentManager.beginTransaction().detach(fragment).commitAllowingStateLoss()\n        fragmentManager.beginTransaction().attach(fragment).commitAllowingStateLoss()\n    }\n\n    @Deprecated(\"Deprecated in Java\")\n    @SuppressLint(\"MissingSuperCall\")\n    override fun onBackPressed() {\n        val size = mSceneTagList.size\n        val tag = mSceneTagList[size - 1]\n        val scene: SceneFragment\n        val fragment = supportFragmentManager.findFragmentByTag(tag)\n        if (fragment == null) {\n            Log.e(TAG, \"onBackPressed: Can't find scene by tag: $tag\")\n            return\n        }\n        if (fragment !is SceneFragment) {\n            Log.e(TAG, \"onBackPressed: The fragment is not SceneFragment\")\n            return\n        }\n        scene = fragment\n        scene.onBackPressed()\n    }\n\n    override fun onProvideAssistContent(outContent: AssistContent) {\n        super.onProvideAssistContent(outContent)\n        val size = mSceneTagList.size\n        val tag = mSceneTagList[size - 1]\n        val fragment = supportFragmentManager.findFragmentByTag(tag)\n        if (fragment == null) {\n            Log.e(TAG, \"onProvideAssistContent: Can't find scene by tag: $tag\")\n            return\n        }\n        (fragment as? SceneFragment)?.onProvideAssistContent(outContent)\n            ?: Log.e(\n                TAG,\n                \"onProvideAssistContent: The fragment is not SceneFragment\",\n            )\n    }\n\n    fun findSceneByTag(tag: String?): SceneFragment? {\n        val fragmentManager = supportFragmentManager\n        val fragment = fragmentManager.findFragmentByTag(tag)\n        return if (fragment != null) {\n            fragment as SceneFragment?\n        } else {\n            null\n        }\n    }\n\n    val topSceneClass: Class<*>?\n        get() {\n            val index = mSceneTagList.size - 1\n            if (index < 0) {\n                return null\n            }\n            val tag = mSceneTagList[index]\n            val fragment = supportFragmentManager.findFragmentByTag(tag) ?: return null\n            return fragment.javaClass\n        }\n\n    private inner class SceneViewComparator : Comparator<View> {\n        private fun getIndex(view: View): Int {\n            val o = view.getTag(R.id.fragment_tag)\n            return if (o is String) {\n                mDelaySceneTagList.indexOf(o)\n            } else {\n                -1\n            }\n        }\n\n        override fun compare(lhs: View, rhs: View): Int = getIndex(lhs) - getIndex(rhs)\n    }\n\n    companion object {\n        const val ACTION_START_SCENE = \"start_scene\"\n        const val KEY_SCENE_NAME = \"stage_activity_scene_name\"\n        const val KEY_SCENE_ARGS = \"stage_activity_scene_args\"\n        private val TAG = StageActivity::class.java.getSimpleName()\n        private const val KEY_STAGE_ID = \"stage_activity_stage_id\"\n        private const val KEY_SCENE_TAG_LIST = \"stage_activity_scene_tag_list\"\n        private const val KEY_NEXT_ID = \"stage_activity_next_id\"\n        private val sLaunchModeMap: MutableMap<Class<*>, Int> = HashMap()\n        fun registerLaunchMode(clazz: Class<*>, @LaunchMode launchMode: Int) {\n            check(!(launchMode != SceneFragment.LAUNCH_MODE_STANDARD && launchMode != SceneFragment.LAUNCH_MODE_SINGLE_TOP && launchMode != SceneFragment.LAUNCH_MODE_SINGLE_TASK)) { \"Invalid launch mode: $launchMode\" }\n            sLaunchModeMap[clazz] = launchMode\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/scene/StageLayout.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.scene\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.util.AttributeSet\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport com.hippo.ehviewer.R\nimport java.lang.reflect.Field\n\nopen class StageLayout @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : FrameLayout(\n    context,\n    attrs,\n    defStyleAttr,\n) {\n    private var mDisappearingChildrenField: Field? = null\n    private var mSuperDisappearingChildren: ArrayList<View>? = null\n    private var mSortedScenes: ArrayList<View>? = null\n    private var mDumpView: View? = null\n    private var mDoTrick = false\n\n    @SuppressLint(\"DiscouragedPrivateApi\")\n    private fun init(context: Context) {\n        try {\n            mDisappearingChildrenField =\n                ViewGroup::class.java.getDeclaredField(\"mDisappearingChildren\")\n            mDisappearingChildrenField!!.isAccessible = true\n        } catch (e: NoSuchFieldException) {\n            e.printStackTrace()\n        }\n        if (mDisappearingChildrenField != null) {\n            mDumpView = View(context)\n            addView(mDumpView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)\n        }\n    }\n\n    private val superDisappearingChildren: Unit\n        get() {\n            if (mDisappearingChildrenField == null || mSuperDisappearingChildren != null) {\n                return\n            }\n            try {\n                @Suppress(\"UNCHECKED_CAST\")\n                mSuperDisappearingChildren = mDisappearingChildrenField!![this] as ArrayList<View>?\n            } catch (e: IllegalAccessException) {\n                e.printStackTrace()\n            }\n        }\n\n    private fun beforeDispatchDraw(): Boolean {\n        superDisappearingChildren\n        if (mSuperDisappearingChildren == null || mSuperDisappearingChildren!!.isEmpty() || childCount <= 1) { // only dump view\n            return false\n        }\n\n        // Get stage\n        val stage: StageActivity?\n        val context = context\n        if (context is StageActivity) {\n            stage = context\n        } else {\n            return false\n        }\n        if (null == mSortedScenes) {\n            mSortedScenes = ArrayList()\n        }\n\n        // Add all scene view to mSortedScenes\n        val disappearingChildren: ArrayList<View> = mSuperDisappearingChildren!!\n        val sortedScenes = mSortedScenes!!\n        run {\n            for (i in 1 until childCount) {\n                // Skip dump view\n                val view = getChildAt(i)\n                if (null != view.getTag(R.id.fragment_tag)) {\n                    sortedScenes.add(view)\n                }\n            }\n        }\n        for (i in 0 until disappearingChildren.size) {\n            val view = disappearingChildren[i]\n            if (null != view.getTag(R.id.fragment_tag)) {\n                sortedScenes.add(view)\n            }\n        }\n        stage.sortSceneViews(sortedScenes)\n        return true\n    }\n\n    private fun afterDispatchDraw() {\n        mSortedScenes?.clear()\n    }\n\n    override fun dispatchDraw(canvas: Canvas) {\n        mDoTrick = beforeDispatchDraw()\n        super.dispatchDraw(canvas)\n        afterDispatchDraw()\n        mDoTrick = false\n    }\n\n    override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {\n        if (mDoTrick) {\n            val sortedScenes = mSortedScenes\n            if (child === mDumpView) {\n                var more = false\n                for (i in 0 until sortedScenes!!.size) {\n                    more = more or super.drawChild(canvas, sortedScenes[i], drawingTime)\n                }\n                return more\n            } else if (sortedScenes!!.contains(child)) {\n                // Skip\n                return false\n            }\n        }\n        return super.drawChild(canvas, child, drawingTime)\n    }\n\n    init {\n        init(context)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/scene/TransitionHelper.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.scene\n\nimport android.content.Context\nimport androidx.fragment.app.Fragment\nimport androidx.fragment.app.FragmentTransaction\n\ninterface TransitionHelper {\n    fun onTransition(context: Context, transaction: FragmentTransaction, exit: Fragment, enter: Fragment): Boolean\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/text/URLImageGetter.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.text\n\nimport android.graphics.drawable.Drawable\nimport android.text.Html.ImageGetter\nimport com.hippo.drawable.UnikeryDrawable\nimport com.hippo.widget.ObservedTextView\n\nclass URLImageGetter(\n    private val mTextView: ObservedTextView,\n) : ImageGetter {\n    override fun getDrawable(source: String): Drawable = UnikeryDrawable(mTextView).apply { load(source) }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/Contracts.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.unifile\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.ParcelFileDescriptor\nimport java.io.IOException\n\ninternal object Contracts {\n    fun queryForString(\n        context: Context,\n        self: Uri,\n        column: String,\n        defaultValue: String?,\n    ): String? = runCatching {\n        context.contentResolver.query(self, arrayOf(column), null, null, null).use {\n            if (it != null && it.moveToFirst() && !it.isNull(0)) {\n                it.getString(0)\n            } else {\n                defaultValue\n            }\n        }\n    }.getOrElse {\n        Utils.throwIfFatal(it)\n        defaultValue\n    }\n\n    fun queryForInt(context: Context, self: Uri, column: String?, defaultValue: Int): Int = queryForLong(context, self, column, defaultValue.toLong()).toInt()\n\n    fun queryForLong(context: Context, self: Uri, column: String?, defaultValue: Long): Long = runCatching {\n        context.contentResolver.query(self, arrayOf(column), null, null, null).use {\n            if (it != null && it.moveToFirst() && !it.isNull(0)) {\n                it.getLong(0)\n            } else {\n                defaultValue\n            }\n        }\n    }.getOrElse {\n        Utils.throwIfFatal(it)\n        defaultValue\n    }\n\n    fun openFileDescriptor(\n        context: Context,\n        uri: Uri?,\n        mode: String?,\n    ): ParcelFileDescriptor = context.contentResolver.openFileDescriptor(uri!!, mode!!)\n        ?: throw IOException(\"Can't open ParcelFileDescriptor\")\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/DocumentsContractApi19.kt",
    "content": "/*\n * Copyright (C) 2014 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 */\npackage com.hippo.unifile\n\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.provider.DocumentsContract\n\ninternal object DocumentsContractApi19 {\n    fun isDocumentUri(context: Context, self: Uri): Boolean = DocumentsContract.isDocumentUri(context, self)\n\n    fun getName(context: Context, self: Uri): String? = Contracts.queryForString(\n        context,\n        self,\n        DocumentsContract.Document.COLUMN_DISPLAY_NAME,\n        null,\n    )\n\n    private fun getRawType(context: Context, self: Uri): String? = Contracts.queryForString(\n        context,\n        self,\n        DocumentsContract.Document.COLUMN_MIME_TYPE,\n        null,\n    )\n\n    fun getType(context: Context, self: Uri): String? = getRawType(context, self).takeUnless { it == DocumentsContract.Document.MIME_TYPE_DIR }\n\n    fun isDirectory(context: Context, self: Uri): Boolean = DocumentsContract.Document.MIME_TYPE_DIR == getRawType(context, self)\n\n    fun isFile(context: Context, self: Uri): Boolean {\n        val type = getRawType(context, self)\n        return !(DocumentsContract.Document.MIME_TYPE_DIR == type || type.isNullOrEmpty())\n    }\n\n    fun lastModified(context: Context, self: Uri): Long = Contracts.queryForLong(\n        context,\n        self,\n        DocumentsContract.Document.COLUMN_LAST_MODIFIED,\n        -1L,\n    )\n\n    fun length(context: Context, self: Uri): Long = Contracts.queryForLong(context, self, DocumentsContract.Document.COLUMN_SIZE, -1L)\n\n    fun canRead(context: Context, self: Uri): Boolean {\n        // Ignore if grant doesn't allow read\n        return if (\n            context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_READ_URI_PERMISSION)\n            != PackageManager.PERMISSION_GRANTED\n        ) {\n            false\n        } else {\n            // Ignore documents without MIME\n            !getRawType(context, self).isNullOrEmpty()\n        }\n    }\n\n    fun canWrite(context: Context, self: Uri): Boolean {\n        // Ignore if grant doesn't allow write\n        if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)\n            != PackageManager.PERMISSION_GRANTED\n        ) {\n            return false\n        }\n        val type = getRawType(context, self)\n        val flags = Contracts.queryForInt(context, self, DocumentsContract.Document.COLUMN_FLAGS, 0)\n\n        // Ignore documents without MIME\n        if (type.isNullOrEmpty()) {\n            return false\n        }\n\n        // Deletable documents considered writable\n        if (flags and DocumentsContract.Document.FLAG_SUPPORTS_DELETE != 0) {\n            return true\n        }\n\n        // Writable normal files considered writable\n        return if (DocumentsContract.Document.MIME_TYPE_DIR == type && flags and DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE != 0) {\n            // Directories that allow create considered writable\n            true\n        } else {\n            flags and DocumentsContract.Document.FLAG_SUPPORTS_WRITE != 0\n        }\n    }\n\n    fun delete(context: Context, self: Uri): Boolean = try {\n        DocumentsContract.deleteDocument(context.contentResolver, self)\n    } catch (e: Throwable) {\n        Utils.throwIfFatal(e)\n        false\n    }\n\n    fun exists(context: Context, self: Uri): Boolean = runCatching {\n        context.contentResolver.query(\n            self,\n            arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),\n            null,\n            null,\n            null,\n        ).use { null != it && it.count > 0 }\n    }.getOrElse {\n        Utils.throwIfFatal(it)\n        false\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/DocumentsContractApi21.kt",
    "content": "/*\n * Copyright (C) 2014 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 */\npackage com.hippo.unifile\n\nimport android.content.Context\nimport android.net.Uri\nimport android.provider.DocumentsContract\n\ninternal object DocumentsContractApi21 {\n    private const val PATH_DOCUMENT = \"document\"\n    private const val PATH_TREE = \"tree\"\n\n    fun createFile(context: Context, self: Uri, mimeType: String, displayName: String): Uri? = try {\n        DocumentsContract.createDocument(\n            context.contentResolver,\n            self,\n            mimeType,\n            displayName,\n        )\n    } catch (e: Throwable) {\n        Utils.throwIfFatal(e)\n        null\n    }\n\n    fun createDirectory(context: Context, self: Uri, displayName: String): Uri? = createFile(context, self, DocumentsContract.Document.MIME_TYPE_DIR, displayName)\n\n    fun prepareTreeUri(treeUri: Uri?): Uri = DocumentsContract.buildDocumentUriUsingTree(\n        treeUri,\n        DocumentsContract.getTreeDocumentId(treeUri),\n    )\n\n    fun getTreeDocumentPath(documentUri: Uri): String {\n        val paths = documentUri.pathSegments\n        if (paths.size >= 4 && PATH_TREE == paths[0] && PATH_DOCUMENT == paths[2]) {\n            return paths[3]\n        }\n        throw IllegalArgumentException(\"Invalid URI: $documentUri\")\n    }\n\n    fun buildChildUri(uri: Uri, displayName: String): Uri = DocumentsContract.buildDocumentUriUsingTree(\n        uri,\n        getTreeDocumentPath(uri) + \"/\" + displayName,\n    )\n\n    fun listFiles(context: Context, self: Uri): Array<Uri> {\n        val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(\n            self,\n            DocumentsContract.getDocumentId(self),\n        )\n        val results = ArrayList<Uri>()\n        runCatching {\n            context.contentResolver.query(\n                childrenUri,\n                arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),\n                null,\n                null,\n                null,\n            ).use {\n                if (null != it) {\n                    while (it.moveToNext()) {\n                        val documentId = it.getString(0)\n                        val documentUri = DocumentsContract.buildDocumentUriUsingTree(\n                            self,\n                            documentId,\n                        )\n                        results.add(documentUri)\n                    }\n                }\n            }\n        }.onFailure {\n            Utils.throwIfFatal(it)\n        }\n        return results.toTypedArray<Uri>()\n    }\n\n    fun renameTo(context: Context, self: Uri, displayName: String): Uri? = try {\n        DocumentsContract.renameDocument(context.contentResolver, self, displayName)\n    } catch (e: Throwable) {\n        Utils.throwIfFatal(e)\n        null\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/FilenameFilter.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.unifile\n\n/**\n * An interface for filtering [UniFile] objects based on their names\n * or the directory they reside in.\n *\n * @see UniFile\n *\n * @see UniFile.listFiles\n */\ninterface FilenameFilter {\n    /**\n     * Indicates if a specific filename matches this filter.\n     *\n     * @param dir      the directory in which the `filename` was found.\n     * @param filename the name of the file in `dir` to test.\n     * @return `true` if the filename matches the filter\n     * and can be included in the list, `false`\n     * otherwise.\n     */\n    fun accept(dir: UniFile?, filename: String?): Boolean\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/MediaContract.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.unifile\n\nimport android.content.Context\nimport android.net.Uri\nimport android.provider.MediaStore\n\ninternal object MediaContract {\n    fun getName(context: Context, self: Uri): String? = Contracts.queryForString(context, self, MediaStore.MediaColumns.DISPLAY_NAME, null)\n\n    fun getType(context: Context, self: Uri): String? = Contracts.queryForString(context, self, MediaStore.MediaColumns.MIME_TYPE, null)\n\n    fun lastModified(context: Context, self: Uri): Long = Contracts.queryForLong(context, self, MediaStore.MediaColumns.DATE_MODIFIED, 0)\n\n    fun length(context: Context, self: Uri): Long = Contracts.queryForLong(context, self, MediaStore.MediaColumns.SIZE, 0)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/MediaFile.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.unifile\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.ParcelFileDescriptor\nimport java.io.IOException\n\ninternal class MediaFile(context: Context, override val uri: Uri) : UniFile(null) {\n    private val mContext = context.applicationContext\n\n    override fun createFile(displayName: String): UniFile? = null\n\n    override fun createDirectory(displayName: String): UniFile? = null\n\n    override val name: String?\n        get() = MediaContract.getName(mContext, uri)\n    override val type: String?\n        get() = MediaContract.getType(mContext, uri)\n    override val isDirectory: Boolean\n        get() = false\n    override val isFile: Boolean\n        get() = DocumentsContractApi19.isFile(mContext, uri)\n\n    override fun lastModified(): Long = MediaContract.lastModified(mContext, uri)\n\n    override fun length(): Long = MediaContract.length(mContext, uri)\n\n    override fun canRead(): Boolean = isFile\n\n    override fun canWrite(): Boolean {\n        try {\n            val fd = openFileDescriptor(\"w\")\n            fd.close()\n        } catch (_: IOException) {\n            return false\n        }\n        return true\n    }\n\n    override fun ensureDir(): Boolean = false\n\n    override fun ensureFile(): Boolean = isFile\n\n    override fun subFile(displayName: String): UniFile? = null\n\n    override fun delete(): Boolean = false\n\n    override fun exists(): Boolean = isFile\n\n    override fun listFiles(): Array<UniFile>? = null\n\n    override fun listFiles(filter: FilenameFilter?): Array<UniFile>? = null\n\n    override fun findFile(displayName: String): UniFile? = null\n\n    override fun renameTo(displayName: String): Boolean = false\n\n    override fun openFileDescriptor(mode: String): ParcelFileDescriptor = Contracts.openFileDescriptor(mContext, uri, mode)\n\n    companion object {\n        fun isMediaUri(context: Context, uri: Uri): Boolean = null != MediaContract.getName(context, uri)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/RawFile.kt",
    "content": "/*\n * Copyright (C) 2015 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 */\npackage com.hippo.unifile\n\nimport android.net.Uri\nimport android.os.ParcelFileDescriptor\nimport android.util.Log\nimport android.webkit.MimeTypeMap\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\nimport java.util.Locale\n\ninternal class RawFile(parent: UniFile?, var mFile: File) : UniFile(parent) {\n    override fun createFile(displayName: String): UniFile? {\n        val target = File(mFile, displayName)\n        return if (target.exists()) {\n            if (target.isFile) {\n                RawFile(this, target)\n            } else {\n                null\n            }\n        } else {\n            runCatching {\n                FileOutputStream(target).use {}\n                RawFile(this, target)\n            }.getOrElse {\n                Log.w(TAG, \"Failed to createFile $displayName: $it\")\n                null\n            }\n        }\n    }\n\n    override fun createDirectory(displayName: String): UniFile? {\n        val target = File(mFile, displayName)\n        return if (target.isDirectory || target.mkdirs()) {\n            RawFile(this, target)\n        } else {\n            null\n        }\n    }\n\n    override val uri: Uri\n        get() = Uri.fromFile(mFile)\n    override val name: String\n        get() = mFile.name\n    override val type: String?\n        get() = if (mFile.isDirectory) {\n            null\n        } else {\n            getTypeForName(mFile.name)\n        }\n    override val isDirectory: Boolean\n        get() = mFile.isDirectory\n    override val isFile: Boolean\n        get() = mFile.isFile\n\n    override fun lastModified(): Long = mFile.lastModified()\n\n    override fun length(): Long = mFile.length()\n\n    override fun canRead(): Boolean = mFile.canRead()\n\n    override fun canWrite(): Boolean = mFile.canWrite()\n\n    override fun ensureDir(): Boolean = mFile.isDirectory || mFile.mkdirs()\n\n    override fun ensureFile(): Boolean = if (mFile.exists()) {\n        mFile.isFile\n    } else {\n        runCatching {\n            FileOutputStream(mFile).use {}\n            true\n        }.getOrDefault(false)\n    }\n\n    override fun subFile(displayName: String): UniFile = RawFile(this, File(mFile, displayName))\n\n    override fun delete(): Boolean {\n        deleteContents(mFile)\n        return mFile.delete()\n    }\n\n    override fun exists(): Boolean = mFile.exists()\n\n    override fun listFiles(): Array<UniFile>? {\n        val files = mFile.listFiles() ?: return null\n        return files.map { RawFile(this, it) }.toTypedArray()\n    }\n\n    override fun listFiles(filter: FilenameFilter?): Array<UniFile>? {\n        if (filter == null) {\n            return listFiles()\n        }\n        val files = mFile.listFiles() ?: return null\n        val results = ArrayList<UniFile>()\n        for (file in files) {\n            if (filter.accept(this, file.name)) {\n                results.add(RawFile(this, file))\n            }\n        }\n        return results.toTypedArray<UniFile>()\n    }\n\n    override fun findFile(displayName: String): UniFile? {\n        val child = File(mFile, displayName)\n        return if (child.exists()) RawFile(this, child) else null\n    }\n\n    override fun renameTo(displayName: String): Boolean {\n        val target = File(mFile.parentFile, displayName)\n        return if (mFile.renameTo(target)) {\n            mFile = target\n            true\n        } else {\n            false\n        }\n    }\n\n    override fun openFileDescriptor(mode: String): ParcelFileDescriptor {\n        val md = ParcelFileDescriptor.parseMode(mode)\n        return ParcelFileDescriptor.open(mFile, md)\n            ?: throw IOException(\"Can't open ParcelFileDescriptor\")\n    }\n\n    companion object {\n        private val TAG = RawFile::class.java.simpleName\n        private fun getTypeForName(name: String): String {\n            val lastDot = name.lastIndexOf('.')\n            if (lastDot >= 0) {\n                val extension = name.substring(lastDot + 1).lowercase(Locale.getDefault())\n                val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)\n                if (mime != null) {\n                    return mime\n                }\n            }\n            return \"application/octet-stream\"\n        }\n\n        private fun deleteContents(dir: File): Boolean {\n            val files = dir.listFiles()\n            var success = true\n            if (files != null) {\n                for (file in files) {\n                    if (file.isDirectory) {\n                        success = success and deleteContents(file)\n                    }\n                    if (!file.delete()) {\n                        Log.w(TAG, \"Failed to delete $file\")\n                        success = false\n                    }\n                }\n            }\n            return success\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/SingleDocumentFile.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.unifile\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.ParcelFileDescriptor\n\ninternal class SingleDocumentFile(parent: UniFile?, context: Context, override val uri: Uri) : UniFile(parent) {\n    private val mContext = context.applicationContext\n\n    override fun createFile(displayName: String): UniFile? = null\n\n    override fun createDirectory(displayName: String): UniFile? = null\n\n    override val name: String?\n        get() = DocumentsContractApi19.getName(mContext, uri)\n    override val type: String?\n        get() = DocumentsContractApi19.getType(mContext, uri)\n    override val isDirectory: Boolean\n        get() = DocumentsContractApi19.isDirectory(mContext, uri)\n    override val isFile: Boolean\n        get() = DocumentsContractApi19.isFile(mContext, uri)\n\n    override fun lastModified(): Long = DocumentsContractApi19.lastModified(mContext, uri)\n\n    override fun length(): Long = DocumentsContractApi19.length(mContext, uri)\n\n    override fun canRead(): Boolean = DocumentsContractApi19.canRead(mContext, uri)\n\n    override fun canWrite(): Boolean = DocumentsContractApi19.canWrite(mContext, uri)\n\n    override fun ensureDir(): Boolean = isDirectory\n\n    override fun ensureFile(): Boolean = isFile\n\n    override fun subFile(displayName: String): UniFile? = null\n\n    override fun delete(): Boolean = DocumentsContractApi19.delete(mContext, uri)\n\n    override fun exists(): Boolean = DocumentsContractApi19.exists(mContext, uri)\n\n    override fun listFiles(): Array<UniFile>? = null\n\n    override fun listFiles(filter: FilenameFilter?): Array<UniFile>? = null\n\n    override fun findFile(displayName: String): UniFile? = null\n\n    override fun renameTo(displayName: String): Boolean = false\n\n    override fun openFileDescriptor(mode: String): ParcelFileDescriptor = Contracts.openFileDescriptor(mContext, uri, mode)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/TreeDocumentFile.kt",
    "content": "/*\n * Copyright (C) 2015 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 */\npackage com.hippo.unifile\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.ParcelFileDescriptor\nimport android.util.Log\nimport android.webkit.MimeTypeMap\n\ninternal class TreeDocumentFile : UniFile {\n    private val mContext: Context\n    override var uri: Uri\n    private var mFilename: String? = null\n\n    constructor(parent: UniFile?, context: Context, uri: Uri) : super(parent) {\n        mContext = context.applicationContext\n        this.uri = uri\n    }\n\n    private constructor(parent: UniFile, context: Context, uri: Uri, filename: String?) : super(\n        parent,\n    ) {\n        mContext = context.applicationContext\n        this.uri = uri\n        mFilename = filename\n    }\n\n    override fun createFile(displayName: String): UniFile? {\n        val child = findFile(displayName)\n        return if (child != null) {\n            if (child.isFile) {\n                child\n            } else {\n                Log.w(\n                    TAG,\n                    \"Try to create file $displayName, but it is not file\",\n                )\n                null\n            }\n        } else {\n            val index = displayName.lastIndexOf('.')\n            if (index > 0) {\n                val name = displayName.substring(0, index)\n                val extension = displayName.substring(index + 1)\n                val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)\n                if (!mimeType.isNullOrEmpty()) {\n                    val result = DocumentsContractApi21.createFile(mContext, uri, mimeType, name)\n                    return if (result != null) {\n                        TreeDocumentFile(\n                            this,\n                            mContext,\n                            result,\n                            displayName,\n                        )\n                    } else {\n                        null\n                    }\n                }\n            }\n\n            // Not dot in displayName or dot is the first char or can't get MimeType\n            val result = DocumentsContractApi21.createFile(\n                mContext,\n                uri,\n                \"application/octet-stream\",\n                displayName,\n            )\n            if (result != null) {\n                TreeDocumentFile(\n                    this,\n                    mContext,\n                    result,\n                    displayName,\n                )\n            } else {\n                null\n            }\n        }\n    }\n\n    override fun createDirectory(displayName: String): UniFile? {\n        val child = findFile(displayName)\n        return if (child != null) {\n            if (child.isDirectory) {\n                child\n            } else {\n                null\n            }\n        } else {\n            val result = DocumentsContractApi21.createDirectory(mContext, uri, displayName)\n            if (result != null) {\n                TreeDocumentFile(\n                    this,\n                    mContext,\n                    result,\n                    displayName,\n                )\n            } else {\n                null\n            }\n        }\n    }\n\n    override val name: String?\n        get() = DocumentsContractApi19.getName(mContext, uri)\n    override val type: String?\n        get() = DocumentsContractApi19.getType(mContext, uri)\n    override val isDirectory: Boolean\n        get() = DocumentsContractApi19.isDirectory(mContext, uri)\n    override val isFile: Boolean\n        get() = DocumentsContractApi19.isFile(mContext, uri)\n\n    override fun lastModified(): Long = DocumentsContractApi19.lastModified(mContext, uri)\n\n    override fun length(): Long = DocumentsContractApi19.length(mContext, uri)\n\n    override fun canRead(): Boolean = DocumentsContractApi19.canRead(mContext, uri)\n\n    override fun canWrite(): Boolean = DocumentsContractApi19.canWrite(mContext, uri)\n\n    override fun ensureDir(): Boolean {\n        if (isDirectory) {\n            return true\n        } else if (isFile) {\n            return false\n        }\n        val parent = parentFile\n        return if (parent != null && parent.ensureDir() && mFilename != null) {\n            parent.createDirectory(mFilename!!) != null\n        } else {\n            false\n        }\n    }\n\n    override fun ensureFile(): Boolean {\n        if (isFile) {\n            return true\n        } else if (isDirectory) {\n            return false\n        }\n        val parent = parentFile\n        return if (parent != null && parent.ensureDir() && mFilename != null) {\n            parent.createFile(mFilename!!) != null\n        } else {\n            false\n        }\n    }\n\n    override fun subFile(displayName: String): UniFile {\n        val childUri = DocumentsContractApi21.buildChildUri(uri, displayName)\n        return TreeDocumentFile(this, mContext, childUri, displayName)\n    }\n\n    override fun delete(): Boolean = DocumentsContractApi19.delete(mContext, uri)\n\n    override fun exists(): Boolean = DocumentsContractApi19.exists(mContext, uri)\n\n    private fun getFilenameForUri(uri: Uri): String? {\n        val path = uri.path\n        if (path != null) {\n            val index = path.lastIndexOf('/')\n            if (index >= 0) {\n                return path.substring(index + 1)\n            }\n        }\n        return null\n    }\n\n    override fun listFiles(): Array<UniFile> {\n        val result = DocumentsContractApi21.listFiles(mContext, uri)\n        return result.map { TreeDocumentFile(this, mContext, it, getFilenameForUri(it)) }.toTypedArray()\n    }\n\n    override fun listFiles(filter: FilenameFilter?): Array<UniFile> {\n        if (filter == null) {\n            return listFiles()\n        }\n        val result = DocumentsContractApi21.listFiles(mContext, uri)\n        val results = ArrayList<UniFile>()\n        for (uri in result) {\n            val name = getFilenameForUri(uri)\n            if (filter.accept(this, name)) {\n                results.add(TreeDocumentFile(this, mContext, uri, name))\n            }\n        }\n        return results.toTypedArray<UniFile>()\n    }\n\n    override fun findFile(displayName: String): UniFile? {\n        val childUri = DocumentsContractApi21.buildChildUri(uri, displayName)\n        return if (DocumentsContractApi19.exists(mContext, childUri)) {\n            TreeDocumentFile(\n                this,\n                mContext,\n                childUri,\n                displayName,\n            )\n        } else {\n            null\n        }\n    }\n\n    override fun renameTo(displayName: String): Boolean {\n        val result = DocumentsContractApi21.renameTo(mContext, uri, displayName)\n        return if (result != null) {\n            uri = result\n            true\n        } else {\n            false\n        }\n    }\n\n    override fun openFileDescriptor(mode: String): ParcelFileDescriptor = Contracts.openFileDescriptor(mContext, uri, mode)\n\n    companion object {\n        private val TAG = TreeDocumentFile::class.java.simpleName\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/UniFile.kt",
    "content": "/*\n * Copyright (C) 2015 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 */\npackage com.hippo.unifile\n\nimport android.content.ContentResolver\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Build\nimport android.os.ParcelFileDescriptor\nimport java.io.File\n\n/**\n * In Android files can be accessed via [java.io.File] and [android.net.Uri].\n * The UniFile is designed to emulate File interface for both File and Uri.\n */\nabstract class UniFile internal constructor(private val parent: UniFile?) {\n    /**\n     * Create a new file as a direct child of this directory.\n     *\n     * @param displayName name of new file\n     * @return file representing newly created document, or null if failed\n     * @see android.provider.DocumentsContract.createDocument\n     */\n    abstract fun createFile(displayName: String): UniFile?\n\n    /**\n     * Create a new directory as a direct child of this directory.\n     *\n     * @param displayName name of new directory\n     * @return file representing newly created directory, or null if failed\n     * @see android.provider.DocumentsContract.createDocument\n     */\n    abstract fun createDirectory(displayName: String): UniFile?\n\n    /**\n     * Return a Uri for the underlying document represented by this file. This\n     * can be used with other platform APIs to manipulate or share the\n     * underlying content. You can use [.isTreeUri] to\n     * test if the returned Uri is backed by a\n     * [android.provider.DocumentsProvider].\n     *\n     * @return uri of the file\n     * @see Intent.setData\n     * @see Intent.setClipData\n     * @see ContentResolver.openInputStream\n     * @see ContentResolver.openOutputStream\n     * @see ContentResolver.openFileDescriptor\n     */\n    abstract val uri: Uri\n\n    /**\n     * Return the display name of this file.\n     *\n     * @return name of the file, or null if failed\n     * @see android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME\n     */\n    abstract val name: String?\n\n    /**\n     * Return the MIME type of this file.\n     *\n     * @return MIME type of the file, or null if failed\n     * @see android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE\n     */\n    abstract val type: String?\n\n    /**\n     * Return the parent file of this file. Only defined inside of the\n     * user-selected tree; you can never escape above the top of the tree.\n     *\n     *\n     * The underlying [android.provider.DocumentsProvider] only defines a\n     * forward mapping from parent to child, so the reverse mapping of child to\n     * parent offered here is purely a convenience method, and it may be\n     * incorrect if the underlying tree structure changes.\n     *\n     * @return parent of the file, or null if it is the top of the file tree\n     */\n    val parentFile: UniFile?\n        get() = parent\n\n    /**\n     * Indicates if this file represents a *directory*.\n     *\n     * @return `true` if this file is a directory, `false`\n     * otherwise.\n     * @see android.provider.DocumentsContract.Document.MIME_TYPE_DIR\n     */\n    abstract val isDirectory: Boolean\n\n    /**\n     * Indicates if this file represents a *file*.\n     *\n     * @return `true` if this file is a file, `false` otherwise.\n     * @see android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE\n     */\n    abstract val isFile: Boolean\n\n    /**\n     * Returns the time when this file was last modified, measured in\n     * milliseconds since January 1st, 1970, midnight. Returns -1 if the file\n     * does not exist, or if the modified time is unknown.\n     *\n     * @return the time when this file was last modified, `-1L` if can't get it\n     * @see android.provider.DocumentsContract.Document.COLUMN_LAST_MODIFIED\n     */\n    abstract fun lastModified(): Long\n\n    /**\n     * Returns the length of this file in bytes. Returns -1 if the file does not\n     * exist, or if the length is unknown. The result for a directory is not\n     * defined.\n     *\n     * @return the number of bytes in this file, `-1L` if can't get it\n     * @see android.provider.DocumentsContract.Document.COLUMN_SIZE\n     */\n    abstract fun length(): Long\n\n    /**\n     * Indicates whether the current context is allowed to read from this file.\n     *\n     * @return `true` if this file can be read, `false` otherwise.\n     */\n    abstract fun canRead(): Boolean\n\n    /**\n     * Indicates whether the current context is allowed to write to this file.\n     *\n     * @return `true` if this file can be written, `false`\n     * otherwise.\n     * @see android.provider.DocumentsContract.Document.COLUMN_FLAGS\n     *\n     * @see android.provider.DocumentsContract.Document.FLAG_SUPPORTS_DELETE\n     *\n     * @see android.provider.DocumentsContract.Document.FLAG_SUPPORTS_WRITE\n     *\n     * @see android.provider.DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE\n     */\n    abstract fun canWrite(): Boolean\n\n    /**\n     * It works like mkdirs, but it will return true if the UniFile is directory\n     *\n     * @return `true` if the directory was created\n     * or if the directory already existed.\n     */\n    abstract fun ensureDir(): Boolean\n\n    /**\n     * Make sure the UniFile is file\n     *\n     * @return `true` if the file can be created\n     * or if the file already existed.\n     */\n    abstract fun ensureFile(): Boolean\n\n    /**\n     * Get child file of this directory, the child might not exist.\n     *\n     * @return the child file, `null` if not supported\n     */\n    abstract fun subFile(displayName: String): UniFile?\n\n    /**\n     * Deletes this file.\n     *\n     *\n     * Note that this method does *not* throw `IOException` on\n     * failure. Callers must check the return value.\n     *\n     * @return `true` if this file was deleted, `false` otherwise.\n     * @see android.provider.DocumentsContract.deleteDocument\n     */\n    abstract fun delete(): Boolean\n\n    /**\n     * Returns a boolean indicating whether this file can be found.\n     *\n     * @return `true` if this file exists, `false` otherwise.\n     */\n    abstract fun exists(): Boolean\n\n    /**\n     * Returns an array of files contained in the directory represented by this\n     * file.\n     *\n     * @return an array of files or `null`.\n     * @see android.provider.DocumentsContract.buildChildDocumentsUriUsingTree\n     */\n    abstract fun listFiles(): Array<UniFile>?\n\n    /**\n     * Gets a list of the files in the directory represented by this file. This\n     * list is then filtered through a FilenameFilter and the names of files\n     * with matching names are returned as an array of strings.\n     *\n     * @param filter the filter to match names against, may be `null`.\n     * @return an array of files or `null`.\n     */\n    abstract fun listFiles(filter: FilenameFilter?): Array<UniFile>?\n\n    /**\n     * Test there is a file with the display name in the directory.\n     *\n     * @return the file if found it, or `null`.\n     */\n    abstract fun findFile(displayName: String): UniFile?\n\n    /**\n     * Renames this file to `displayName`.\n     *\n     *\n     * Note that this method does *not* throw `IOException` on\n     * failure. Callers must check the return value.\n     *\n     *\n     * Some providers may need to create a new file to reflect the rename,\n     * potentially with a different MIME type, so [.getUri] and\n     * [.getType] may change to reflect the rename.\n     *\n     *\n     * When renaming a directory, children previously enumerated through\n     * [.listFiles] may no longer be valid.\n     *\n     * @param displayName the new display name.\n     * @return true on success.\n     * @see android.provider.DocumentsContract.renameDocument\n     */\n    abstract fun renameTo(displayName: String): Boolean\n\n    abstract fun openFileDescriptor(mode: String): ParcelFileDescriptor\n\n    companion object {\n        private var sUriHandlerArray: MutableList<UriHandler>? = null\n\n        /**\n         * Add a UriHandler to get UniFile from uri\n         */\n        fun addUriHandler(handler: UriHandler) {\n            if (sUriHandlerArray == null) {\n                sUriHandlerArray = ArrayList()\n            }\n            sUriHandlerArray!!.add(handler)\n        }\n\n        /**\n         * Remove the UriHandler added before\n         */\n        fun removeUriHandler(handler: UriHandler) {\n            if (sUriHandlerArray != null) {\n                sUriHandlerArray!!.remove(handler)\n            }\n        }\n\n        /**\n         * Create a [UniFile] representing the given [File].\n         *\n         * @param file the file to wrap\n         * @return the [UniFile] representing the given [File].\n         */\n        fun fromFile(file: File?): UniFile? = if (file != null) RawFile(null, file) else null\n\n        /**\n         * Create a [UniFile] representing the single document at the\n         * given [Uri]. This is only useful on devices running\n         * [android.os.Build.VERSION_CODES.KITKAT] or later, and will return\n         * `null` when called on earlier platform versions.\n         *\n         * @param singleUri the [Intent.getData] from a successful\n         * [Intent.ACTION_OPEN_DOCUMENT] or\n         * [Intent.ACTION_CREATE_DOCUMENT] request.\n         * @return the [UniFile] representing the given [Uri].\n         */\n        fun fromSingleUri(context: Context, singleUri: Uri): UniFile? {\n            val version = Build.VERSION.SDK_INT\n            return if (version >= 19) {\n                SingleDocumentFile(null, context, singleUri)\n            } else {\n                null\n            }\n        }\n\n        /**\n         * Create a [UniFile] representing the document tree rooted at\n         * the given [Uri]. This is only useful on devices running\n         * [Build.VERSION_CODES.LOLLIPOP] or later, and will return\n         * `null` when called on earlier platform versions.\n         *\n         * @param treeUri the [Intent.getData] from a successful\n         * [Intent.ACTION_OPEN_DOCUMENT_TREE] request.\n         * @return the [UniFile] representing the given [Uri].\n         */\n        fun fromTreeUri(context: Context, treeUri: Uri): UniFile? {\n            val version = Build.VERSION.SDK_INT\n            return if (version >= 21) {\n                TreeDocumentFile(\n                    null,\n                    context,\n                    DocumentsContractApi21.prepareTreeUri(treeUri),\n                )\n            } else {\n                null\n            }\n        }\n\n        /**\n         * Create a [UniFile] representing the media file rooted at\n         * the given [Uri].\n         *\n         * @param mediaUri the media uri to wrap\n         * @return the [UniFile] representing the given [Uri].\n         */\n        fun fromMediaUri(context: Context, mediaUri: Uri): UniFile = MediaFile(context, mediaUri)\n\n        /**\n         * Create a [UniFile] representing the given [Uri].\n         */\n        fun fromUri(context: Context, uri: Uri): UniFile? {\n            // Custom handler\n            if (sUriHandlerArray != null) {\n                var i = 0\n                val size = sUriHandlerArray!!.size\n                while (i < size) {\n                    val file = sUriHandlerArray!![i].fromUri(context, uri)\n                    if (file != null) {\n                        return file\n                    }\n                    i++\n                }\n            }\n            return if (isFileUri(uri)) {\n                fromFile(File(uri.path!!))\n            } else if (isDocumentUri(context, uri)) {\n                if (isTreeUri(uri)) {\n                    fromTreeUri(context, uri)\n                } else {\n                    fromSingleUri(context, uri)\n                }\n            } else if (MediaFile.isMediaUri(context, uri)) {\n                MediaFile(context, uri)\n            } else {\n                null\n            }\n        }\n\n        /**\n         * Test if given Uri is FileUri\n         */\n        fun isFileUri(uri: Uri): Boolean = ContentResolver.SCHEME_FILE == uri.scheme\n\n        /**\n         * Test if given Uri is backed by a\n         * [android.provider.DocumentsProvider].\n         */\n        fun isDocumentUri(context: Context, uri: Uri): Boolean {\n            val version = Build.VERSION.SDK_INT\n            return if (version >= 19) {\n                DocumentsContractApi19.isDocumentUri(context, uri)\n            } else {\n                false\n            }\n        }\n\n        /**\n         * Test if given Uri is TreeUri\n         */\n        fun isTreeUri(uri: Uri): Boolean {\n            val paths = uri.pathSegments\n            return ContentResolver.SCHEME_CONTENT == uri.scheme && paths.size >= 2 && \"tree\" == paths[0]\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/UniFileExtensions.kt",
    "content": "/*\n * Copyright 2023 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.unifile\n\nimport android.os.ParcelFileDescriptor.AutoCloseInputStream\nimport android.os.ParcelFileDescriptor.AutoCloseOutputStream\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\n\n/**\n * Use Native IO/NIO directly if possible, unless you need process file content on JVM!\n */\nfun UniFile.openInputStream(): FileInputStream = AutoCloseInputStream(openFileDescriptor(\"r\"))\n\n/**\n * Use Native IO/NIO directly if possible, unless you need process file content on JVM!\n */\nfun UniFile.openOutputStream(): FileOutputStream = AutoCloseOutputStream(openFileDescriptor(\"wt\"))\n\nfun UniFile.sha1() = openFileDescriptor(\"r\").use { com.hippo.ehviewer.jni.sha1(it.fd) }\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/UriHandler.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.unifile\n\nimport android.content.Context\nimport android.net.Uri\n\n/*\n * Created by Hippo on 8/16/2016.\n */\n/**\n * A UriHandler is to get UniFile from custom uri for extensions\n */\ninterface UriHandler {\n    /**\n     * Create a [UniFile] representing the uri\n     */\n    fun fromUri(context: Context?, uri: Uri?): UniFile?\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/unifile/Utils.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.unifile\n\ninternal object Utils {\n    fun throwIfFatal(t: Throwable) {\n        // values here derived from https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495\n        when (t) {\n            is VirtualMachineError, is ThreadDeath, is LinkageError -> throw t\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/AppHelper.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.util\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.widget.Toast\nimport com.hippo.ehviewer.R\n\nobject AppHelper {\n    fun share(from: Activity, text: String?): Boolean {\n        val sendIntent = Intent()\n        sendIntent.action = Intent.ACTION_SEND\n        sendIntent.putExtra(Intent.EXTRA_TEXT, text)\n        sendIntent.type = \"text/plain\"\n        val chooser = Intent.createChooser(sendIntent, from.getString(R.string.share))\n        return runCatching {\n            from.startActivity(chooser)\n            true\n        }.getOrElse {\n            ExceptionUtils.throwIfFatal(it)\n            Toast.makeText(from, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show()\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/BBCode.kt",
    "content": "/*\n * Copyright 2023 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport android.graphics.Typeface\nimport android.text.Spanned\nimport android.text.style.CharacterStyle\nimport android.text.style.ImageSpan\nimport android.text.style.StrikethroughSpan\nimport android.text.style.StyleSpan\nimport android.text.style.URLSpan\nimport android.text.style.UnderlineSpan\n\nfun Spanned.toBBCode(): String {\n    val text = this\n    tailrec fun StringBuilder.processOneSpanTransition(cur: Int = 0) {\n        val next = nextSpanTransition(cur, text.length, CharacterStyle::class.java)\n        getSpans(cur, next, CharacterStyle::class.java).forEach {\n            when (it) {\n                is StyleSpan -> {\n                    val s = it.style\n                    if (s and Typeface.BOLD != 0) {\n                        append(\"[b]\")\n                    }\n                    if (s and Typeface.ITALIC != 0) {\n                        append(\"[i]\")\n                    }\n                }\n                is UnderlineSpan -> append(\"[u]\")\n                is StrikethroughSpan -> append(\"[s]\")\n                is URLSpan -> {\n                    append(\"[url=\")\n                    append(it.url)\n                    append(\"]\")\n                }\n                is ImageSpan -> {\n                    append(\"[img]\")\n                    append(it.source)\n                    append(\"[/img]\")\n                }\n            }\n        }\n        append(text.subSequence(cur, next))\n        getSpans(cur, next, CharacterStyle::class.java).reversed().forEach {\n            when (it) {\n                is StyleSpan -> {\n                    val s = it.style\n                    if (s and Typeface.BOLD != 0) {\n                        append(\"[/b]\")\n                    }\n                    if (s and Typeface.ITALIC != 0) {\n                        append(\"[/i]\")\n                    }\n                }\n                is UnderlineSpan -> append(\"[/u]\")\n                is StrikethroughSpan -> append(\"[/s]\")\n                is URLSpan -> append(\"[/url]\")\n            }\n        }\n        if (next < text.length) processOneSpanTransition(next)\n    }\n    return StringBuilder().apply {\n        processOneSpanTransition()\n    }.toString()\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/ClipboardUtil.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport android.content.ClipData\nimport android.content.ClipDescription\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.os.PersistableBundle\nimport android.text.TextUtils\nimport android.view.textclassifier.TextClassifier\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.ui.MainActivity\nimport com.hippo.ehviewer.ui.SettingsActivity\nimport com.hippo.ehviewer.ui.scene.BaseScene\n\nfun Context.getClipboardManager(): ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n\nfun Context.addTextToClipboard(text: CharSequence?, isSensitive: Boolean) {\n    getClipboardManager().apply {\n        setPrimaryClip(\n            ClipData.newPlainText(null, text).apply {\n                if (isAtLeastT && isSensitive) {\n                    description.extras = PersistableBundle().apply {\n                        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)\n                    }\n                }\n            },\n        )\n    }\n    if (this is MainActivity) {\n        showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT)\n    } else if (this is SettingsActivity) {\n        showTip(R.string.copied_to_clipboard, BaseScene.LENGTH_SHORT)\n    }\n}\n\nfun ClipboardManager.getTextFromClipboard(context: Context): String? {\n    val item = primaryClip?.getItemAt(0)\n    val string = item?.coerceToText(context).toString()\n    return if (!TextUtils.isEmpty(string)) string else null\n}\n\nfun ClipboardManager.getUrlFromClipboard(context: Context): String? {\n    if (isAtLeastS && primaryClipDescription?.classificationStatus == ClipDescription.CLASSIFICATION_COMPLETE) {\n        if ((\n                primaryClipDescription?.getConfidenceScore(TextClassifier.TYPE_URL)\n                    ?.let { it <= 0 }\n                ) == true\n        ) {\n            return null\n        }\n    }\n    val item = primaryClip?.getItemAt(0)\n    val string = item?.coerceToText(context).toString()\n    return if (!TextUtils.isEmpty(string)) string else null\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/CoroutinesExtensions.kt",
    "content": "/*\n * Copyright 2023 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport java.io.InterruptedIOException\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.EmptyCoroutineContext\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.CoroutineStart\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.NonCancellable\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runInterruptible\nimport kotlinx.coroutines.withContext\n\n/**\n * Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.\n *\n * **Possible replacements**\n * - suspend function\n * - custom scope like view or presenter scope\n */\n@DelicateCoroutinesApi\nfun launchUI(block: suspend CoroutineScope.() -> Unit): Job = GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)\n\n/**\n * Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.\n *\n * **Possible replacements**\n * - suspend function\n * - custom scope like view or presenter scope\n */\n@DelicateCoroutinesApi\nfun launchIO(block: suspend CoroutineScope.() -> Unit): Job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT, block)\n\n/**\n * Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.\n *\n * **Possible replacements**\n * - suspend function\n * - custom scope like view or presenter scope\n */\n@DelicateCoroutinesApi\nfun launchNow(block: suspend CoroutineScope.() -> Unit): Job = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)\n\nfun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job = launch(Dispatchers.Main, block = block)\n\nfun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job = launch(Dispatchers.IO, block = block)\n\nfun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job = launchIO { withContext(NonCancellable, block) }\n\nsuspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)\n\nsuspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)\n\nsuspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) = withContext(NonCancellable, block)\n\n// moe.tarsin.coroutines\ninline fun <reified T : Throwable> Result<*>.except(): Result<*> = onFailure { if (it is T) throw it }\n\ninline fun <R> runSuspendCatching(block: () -> R): Result<R> = runCatching(block).apply { except<CancellationException>() }\n\ninline fun <T, R> T.runSuspendCatching(block: T.() -> R): Result<R> = runCatching(block).apply { except<CancellationException>() }\n\n// See https://github.com/Kotlin/kotlinx.coroutines/issues/3551\nsuspend inline fun <T> runInterruptibleOkio(\n    context: CoroutineContext = EmptyCoroutineContext,\n    crossinline block: () -> T,\n): T = runInterruptible(context) {\n    try {\n        block()\n    } catch (e: InterruptedIOException) {\n        if (Thread.currentThread().isInterrupted) {\n            // Coroutine cancelled\n            throw InterruptedException().initCause(e)\n        } else {\n            // AsyncTimeout reached\n            throw e\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/DateTimeUtil.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport kotlin.time.Instant\nimport kotlinx.datetime.LocalDate\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.atStartOfDayIn\nimport kotlinx.datetime.toInstant\nimport kotlinx.datetime.toLocalDateTime\n\nfun LocalDate.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long = atStartOfDayIn(timeZone).toEpochMilliseconds()\n\nfun LocalDateTime.toEpochMillis(timeZone: TimeZone = TimeZone.UTC): Long = toInstant(timeZone).toEpochMilliseconds()\n\nfun Long.toLocalDateTime(timeZone: TimeZone = TimeZone.UTC): LocalDateTime = Instant.fromEpochMilliseconds(this).toLocalDateTime(timeZone)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/ExceptionUtils.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.util\n\nimport com.hippo.ehviewer.GetText.getString\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.exception.CloudflareBypassException\nimport com.hippo.ehviewer.client.exception.EhException\nimport com.hippo.network.StatusCodeException\nimport java.net.MalformedURLException\nimport java.net.ProtocolException\nimport java.net.SocketException\nimport java.net.SocketTimeoutException\nimport java.net.UnknownHostException\nimport javax.net.ssl.SSLException\n\nobject ExceptionUtils {\n    fun getReadableString(e: Throwable?): String {\n        e?.printStackTrace()\n\n        if (e?.cause is CloudflareBypassException) {\n            return e.cause!!.message!!\n        }\n\n        return when (e) {\n            is MalformedURLException -> {\n                getString(R.string.error_invalid_url)\n            }\n            is SocketTimeoutException -> {\n                getString(R.string.error_timeout)\n            }\n            is UnknownHostException -> {\n                getString(R.string.error_unknown_host)\n            }\n            is StatusCodeException -> {\n                val sb = StringBuilder()\n                sb.append(getString(R.string.error_bad_status_code, e.responseCode))\n                if (e.isIdentifiedResponseCode) {\n                    sb.append(\", \").append(e.message)\n                }\n                sb.toString()\n            }\n            is ProtocolException if e.message!!.startsWith(\"Too many follow-up requests:\") -> {\n                getString(R.string.error_redirection)\n            }\n            is ProtocolException, is SocketException, is SSLException -> {\n                getString(R.string.error_socket)\n            }\n            is EhException -> {\n                e.message!!\n            }\n            else -> {\n                getString(R.string.error_unknown)\n            }\n        }\n    }\n\n    fun throwIfFatal(t: Throwable) {\n        // values here derived from https://github.com/ReactiveX/RxJava/issues/748#issuecomment-32471495\n        when (t) {\n            is VirtualMachineError -> {\n                throw t\n            }\n            is ThreadDeath -> {\n                throw t\n            }\n            is LinkageError -> {\n                throw t\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/FDUtils.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport android.os.ParcelFileDescriptor\nimport android.system.Int64Ref\nimport android.system.Os\nimport android.util.Log\nimport com.hippo.unifile.UniFile\nimport java.io.FileDescriptor\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\n\nprivate fun sendFileTotally(from: FileDescriptor, to: FileDescriptor): Long {\n    if (isAtLeastP) {\n        // sendFile may fail on some devices\n        try {\n            return Os.sendfile(to, from, Int64Ref(0), Long.MAX_VALUE)\n        } catch (e: Exception) {\n            Log.e(\"sendFile\", \"failed\", e)\n        }\n    }\n    return FileInputStream(from).use { src ->\n        FileOutputStream(to).use { dst ->\n            src.channel.transferTo(0, Long.MAX_VALUE, dst.channel)\n        }\n    }\n}\n\ninfix fun ParcelFileDescriptor.sendTo(fd: ParcelFileDescriptor) {\n    sendFileTotally(fileDescriptor, fd.fileDescriptor)\n}\n\ninfix fun UniFile.sendTo(file: UniFile) = openFileDescriptor(\"r\").use { src ->\n    file.openFileDescriptor(\"wt\").use { dst -> src sendTo dst }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/HtmlCompat.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport android.text.Html\nimport android.text.Spanned\nimport com.hippo.text.URLImageGetter\nimport com.hippo.widget.ObservedTextView\n\n@Suppress(\"DEPRECATION\")\nfun loadHtml(source: String): Spanned = if (isAtLeastN) {\n    Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY)\n} else {\n    Html.fromHtml(source)\n}\n\n@Suppress(\"DEPRECATION\")\nfun loadHtml(source: String?, textView: ObservedTextView): Spanned = if (isAtLeastN) {\n    Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY, URLImageGetter(textView), null)\n} else {\n    Html.fromHtml(source, URLImageGetter(textView), null)\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/JsoupUtils.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.util;\n\nimport androidx.annotation.Nullable;\n\nimport org.jsoup.nodes.Document;\nimport org.jsoup.nodes.Element;\nimport org.jsoup.select.Elements;\n\npublic final class JsoupUtils {\n    @Nullable\n    public static Element getElementByClass(Document doc, String className) {\n        Elements elements = doc.getElementsByClass(className);\n        if (!elements.isEmpty()) {\n            //noinspection SequencedCollectionMethodCanBeUsed\n            return elements.get(0);\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    public static Element getElementByClass(Element element, String className) {\n        Elements elements = element.getElementsByClass(className);\n        if (!elements.isEmpty()) {\n            //noinspection SequencedCollectionMethodCanBeUsed\n            return elements.get(0);\n        } else {\n            return null;\n        }\n    }\n\n    @Nullable\n    public static Element getElementByTag(Element element, String tagName) {\n        Elements elements = element.getElementsByTag(tagName);\n        if (!elements.isEmpty()) {\n            //noinspection SequencedCollectionMethodCanBeUsed\n            return elements.get(0);\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/LogCat.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.util;\n\nimport android.util.Log;\nimport com.hippo.yorozuya.IOUtils;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\npublic final class LogCat {\n    private LogCat() {\n    }\n\n    public static boolean save(OutputStream outputStream) {\n        try {\n            Process p = Runtime.getRuntime().exec(\"logcat -d\");\n            IOUtils.copy(p.getInputStream(), outputStream);\n            return true;\n        } catch (IOException e) {\n            Log.e(\"LogCat\", \"Error saving logcat output\", e);\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/ParcelableCompat.kt",
    "content": "/*\n * Copyright 2023 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport android.content.Intent\nimport android.os.Bundle\nimport android.os.Parcel\nimport android.os.Parcelable\nimport android.util.SparseArray\nimport androidx.core.content.IntentCompat\nimport androidx.core.os.BundleCompat\nimport androidx.core.os.ParcelCompat\n\ninline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String?): T? = BundleCompat.getParcelable(this, key, T::class.java)\n\ninline fun <reified T : Parcelable> Bundle.getSparseParcelableArrayCompat(key: String?): SparseArray<T?>? = BundleCompat.getSparseParcelableArray(this, key, T::class.java)\n\ninline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String?): T? = IntentCompat.getParcelableExtra(this, key, T::class.java)\n\ninline fun <reified T : Parcelable> Parcel.readParcelableCompat(key: ClassLoader?): T? = ParcelCompat.readParcelable(this, key, T::class.java)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/ReadableTime.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.util\n\nimport android.content.Context\nimport android.content.res.Resources\nimport com.hippo.ehviewer.R\nimport java.util.Locale\nimport kotlin.time.Clock\nimport kotlin.time.Instant\nimport kotlinx.datetime.LocalDate\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.format.MonthNames\nimport kotlinx.datetime.format.Padding\nimport kotlinx.datetime.format.char\nimport kotlinx.datetime.toLocalDateTime\n\nobject ReadableTime {\n    const val MAX_VALUE_MILLIS = 253402300799999L\n    private const val SECOND_MILLIS = 1000L\n    private const val MINUTE_MILLIS = 60 * SECOND_MILLIS\n    private const val HOUR_MILLIS = 60 * MINUTE_MILLIS\n    private const val DAY_MILLIS = 24 * HOUR_MILLIS\n    private const val WEEK_MILLIS = 7 * DAY_MILLIS\n    private const val YEAR_MILLIS = 365 * DAY_MILLIS\n    private val MULTIPLES = longArrayOf(\n        YEAR_MILLIS,\n        DAY_MILLIS,\n        HOUR_MILLIS,\n        MINUTE_MILLIS,\n        SECOND_MILLIS,\n    )\n    private const val SIZE = 5\n    private val UNITS = intArrayOf(\n        R.plurals.year,\n        R.plurals.day,\n        R.plurals.hour,\n        R.plurals.minute,\n        R.plurals.second,\n    )\n    private val DATE_FORMAT_WITHOUT_YEAR = LocalDate.Format {\n        monthName(MonthNames.ENGLISH_ABBREVIATED)\n        char(' ')\n        day(Padding.NONE)\n    }\n    private val DATE_FORMAT_WITH_YEAR = LocalDate.Format {\n        monthName(MonthNames.ENGLISH_ABBREVIATED)\n        char(' ')\n        day(Padding.NONE)\n        chars(\", \")\n        year()\n    }\n    private val DATE_FORMAT_WITHOUT_YEAR_ZH = LocalDate.Format {\n        monthNumber(Padding.NONE)\n        char('月')\n        day(Padding.NONE)\n        char('日')\n    }\n    private val DATE_FORMAT_WITH_YEAR_ZH = LocalDate.Format {\n        year()\n        char('年')\n        monthNumber(Padding.NONE)\n        char('月')\n        day(Padding.NONE)\n        char('日')\n    }\n\n    // yyyy-MM-dd-HH-mm-ss-SSS\n    private val FILENAMABLE_DATE_FORMAT = LocalDateTime.Format {\n        year()\n        char('-')\n        monthNumber()\n        char('-')\n        day()\n        char('-')\n        hour()\n        char('-')\n        minute()\n        char('-')\n        second()\n        char('-')\n        secondFraction(3)\n    }\n\n    // yyyy-MM-dd HH:mm\n    private val DATE_FORMAT_SHORT = LocalDateTime.Format {\n        year()\n        char('-')\n        monthNumber()\n        char('-')\n        day()\n        char(' ')\n        hour()\n        char(':')\n        minute()\n    }\n    private var sResources: Resources? = null\n\n    fun initialize(context: Context) {\n        sResources = context.applicationContext.resources\n    }\n\n    fun getTimeAgo(time: Long): String {\n        val resources = sResources!!\n        val nowInstant = Clock.System.now()\n        val now = nowInstant.toEpochMilliseconds()\n        val diff = now - time\n        return when {\n            (diff < 0 || time <= 0) -> resources.getString(R.string.from_the_future)\n            diff < MINUTE_MILLIS -> resources.getString(R.string.just_now)\n            diff < 2 * MINUTE_MILLIS -> resources.getQuantityString(R.plurals.some_minutes_ago, 1, 1)\n            diff < 50 * MINUTE_MILLIS -> {\n                val minutes = (diff / MINUTE_MILLIS).toInt()\n                resources.getQuantityString(R.plurals.some_minutes_ago, minutes, minutes)\n            }\n            diff < 90 * MINUTE_MILLIS -> resources.getQuantityString(R.plurals.some_hours_ago, 1, 1)\n            diff < 24 * HOUR_MILLIS -> {\n                val hours = (diff / HOUR_MILLIS).toInt()\n                resources.getQuantityString(R.plurals.some_hours_ago, hours, hours)\n            }\n            diff < 48 * HOUR_MILLIS -> {\n                resources.getString(R.string.yesterday)\n            }\n            diff < WEEK_MILLIS -> resources.getString(R.string.some_days_ago, (diff / DAY_MILLIS).toInt())\n            else -> {\n                val timeZone = TimeZone.currentSystemDefault()\n                val nowDate = nowInstant.toLocalDateTime(timeZone).date\n                val timeDate = time.toLocalDateTime(timeZone).date\n                val nowYear = nowDate.year\n                val timeYear = timeDate.year\n                val isZh = Locale.getDefault().language == \"zh\"\n                if (nowYear == timeYear) {\n                    if (isZh) DATE_FORMAT_WITHOUT_YEAR_ZH else DATE_FORMAT_WITHOUT_YEAR\n                } else {\n                    if (isZh) DATE_FORMAT_WITH_YEAR_ZH else DATE_FORMAT_WITH_YEAR\n                }.format(timeDate)\n            }\n        }\n    }\n\n    fun getShortTimeInterval(time: Long): String = buildString {\n        val resources: Resources = sResources!!\n        for (i in 0 until SIZE) {\n            val multiple = MULTIPLES[i]\n            val quotient = time / multiple\n            if (time > multiple * 1.5 || i == SIZE - 1) {\n                append(quotient)\n                    .append(\" \")\n                    .append(resources.getQuantityString(UNITS[i], quotient.toInt()))\n                break\n            }\n        }\n    }\n\n    @JvmOverloads\n    fun getFilenamableTime(time: Instant = Clock.System.now()): String = FILENAMABLE_DATE_FORMAT.format(time.toLocalDateTime(TimeZone.currentSystemDefault()))\n\n    fun getShortTime(time: Long): String = DATE_FORMAT_SHORT.format(time.toLocalDateTime(TimeZone.currentSystemDefault()))\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/SDKUtils.kt",
    "content": "/*\n * Copyright 2024 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport android.os.Build\nimport android.os.ext.SdkExtensions\n\nval isAtLeastN = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N\nval isAtLeastO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O\nval isAtLeastOMR1 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1\nval isAtLeastP = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P\nval isAtLeastQ = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q\nval isAtLeastR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R\nval isAtLeastS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\nval isAtLeastT = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU\nval isAtLeastU = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE\nval isAtLeastSExtension7 = isAtLeastR && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/SqlUtils.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.util;\n\nimport android.database.Cursor;\nimport android.database.SQLException;\nimport android.database.sqlite.SQLiteDatabase;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class SqlUtils {\n    public static void exeSQLSafely(SQLiteDatabase db, String sql) {\n        try {\n            db.execSQL(sql);\n        } catch (SQLException e) {\n            // Ignore\n        }\n    }\n\n    public static void dropTable(SQLiteDatabase db, String tableName) {\n        exeSQLSafely(db, \"DROP TABLE IF EXISTS \" + tableName);\n    }\n\n    public static void dropAllTable(SQLiteDatabase db) {\n        List<String> tables = new ArrayList<>();\n        Cursor cursor = db.rawQuery(\"SELECT * FROM sqlite_master WHERE type='table';\", null);\n        cursor.moveToFirst();\n        while (!cursor.isAfterLast()) {\n            String tableName = cursor.getString(1);\n            if (!tableName.equals(\"android_metadata\") &&\n                    !tableName.equals(\"sqlite_sequence\"))\n                tables.add(tableName);\n            cursor.moveToNext();\n        }\n        cursor.close();\n\n        for (String tableName : tables) {\n            dropTable(db, tableName);\n        }\n    }\n\n    public static String sqlEscapeString(String value) {\n        StringBuilder sb = new StringBuilder();\n\n        int length = value.length();\n        for (int i = 0; i < length; i++) {\n            char c = value.charAt(i);\n            if (c == '\\'') {\n                sb.append('\\'');\n            }\n            sb.append(c);\n        }\n\n        return sb.toString();\n    }\n\n    public static boolean getBoolean(Cursor cursor, String column, boolean defValue) {\n        try {\n            int index = cursor.getColumnIndex(column);\n            if (index != -1) {\n                return cursor.getInt(index) != 0;\n            }\n        } catch (Throwable e) { /* Ignore */ }\n        return defValue;\n    }\n\n    public static int getInt(Cursor cursor, String column, int defValue) {\n        try {\n            int index = cursor.getColumnIndex(column);\n            if (index != -1) {\n                return cursor.getInt(index);\n            }\n        } catch (Throwable e) { /* Ignore */ }\n        return defValue;\n    }\n\n    public static long getLong(Cursor cursor, String column, long defValue) {\n        try {\n            int index = cursor.getColumnIndex(column);\n            if (index != -1) {\n                return cursor.getLong(index);\n            }\n        } catch (Throwable e) { /* Ignore */ }\n        return defValue;\n    }\n\n    public static float getFloat(Cursor cursor, String column, float defValue) {\n        try {\n            int index = cursor.getColumnIndex(column);\n            if (index != -1) {\n                return cursor.getFloat(index);\n            }\n        } catch (Throwable e) { /* Ignore */ }\n        return defValue;\n    }\n\n    public static String getString(Cursor cursor, String column, String defValue) {\n        try {\n            int index = cursor.getColumnIndex(column);\n            if (index != -1) {\n                return cursor.getString(index);\n            }\n        } catch (Throwable e) { /* Ignore */ }\n        return defValue;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/TextUrl.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.util;\n\nimport android.text.Spannable;\nimport android.text.SpannableString;\nimport android.text.style.URLSpan;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class TextUrl {\n    private static final Pattern URL_PATTERN = Pattern.compile(\"(http|https)://[a-z0-9A-Z%-]+(\\\\.[a-z0-9A-Z%-]+)+(:\\\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/*.?+$\\\\[\\\\]()]+)?/?\");\n\n    public static CharSequence handleTextUrl(CharSequence content) {\n        Matcher m = URL_PATTERN.matcher(content);\n\n        Spannable spannable = null;\n        while (m.find()) {\n            // Ensure spannable\n            if (spannable == null) {\n                if (content instanceof Spannable) {\n                    spannable = (Spannable) content;\n                } else {\n                    spannable = new SpannableString(content);\n                }\n            }\n\n            int start = m.start();\n            int end = m.end();\n\n            URLSpan[] links = spannable.getSpans(start, end, URLSpan.class);\n            if (links.length > 0) {\n                // There has been URLSpan already, leave it alone\n                continue;\n            }\n\n            URLSpan urlSpan = new URLSpan(m.group(0));\n            spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);\n        }\n\n        return spannable == null ? content : spannable;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/util/URLEncoderCompat.kt",
    "content": "/*\n * Copyright 2023 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.util\n\nimport java.net.URLEncoder\nimport java.nio.charset.Charset\nimport java.nio.charset.StandardCharsets\n\nfun encode(s: String, charset: Charset): String = if (isAtLeastT) {\n    URLEncoder.encode(s, charset)\n} else {\n    URLEncoder.encode(s, charset.name())\n}\n\nfun encodeUTF8(s: String): String = encode(s, StandardCharsets.UTF_8)\n"
  },
  {
    "path": "app/src/main/java/com/hippo/view/BringOutTransition.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.view.View;\n\nimport com.hippo.ehviewer.widget.SearchLayout;\nimport com.hippo.widget.ContentLayout;\n\npublic class BringOutTransition extends ViewTransition {\n    public BringOutTransition(ContentLayout contentLayout, SearchLayout mSearchLayout) {\n        super(contentLayout, mSearchLayout);\n    }\n\n    @Override\n    protected void startAnimations(final View hiddenView, final View shownView) {\n        mAnimator1 = hiddenView.animate().alpha(0).scaleY(0.7f).scaleX(0.7f);\n        mAnimator1.setDuration(ANIMATE_TIME).setListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                hiddenView.setVisibility(View.GONE);\n                mAnimator1 = null;\n            }\n        }).start();\n\n        shownView.setAlpha(0);\n        shownView.setScaleX(0.7f);\n        shownView.setScaleY(0.7f);\n        shownView.setVisibility(View.VISIBLE);\n        mAnimator2 = shownView.animate().alpha(1f).scaleX(1).scaleY(1);\n        mAnimator2.setDuration(ANIMATE_TIME).setListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mAnimator2 = null;\n            }\n        }).start();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/view/ViewTransition.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.view;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.view.View;\nimport android.view.ViewPropertyAnimator;\n\npublic class ViewTransition {\n    protected static final long ANIMATE_TIME = 300L;\n\n    private final View[] mViews;\n    protected ViewPropertyAnimator mAnimator1;\n    protected ViewPropertyAnimator mAnimator2;\n    private int mShownView = -1;\n    private OnShowViewListener mOnShowViewListener;\n\n    public ViewTransition(View... views) {\n        if (views.length < 2) {\n            throw new IllegalStateException(\"You must pass view to ViewTransition\");\n        }\n        for (View v : views) {\n            if (v == null) {\n                throw new IllegalStateException(\"Any View pass to ViewTransition must not be null\");\n            }\n        }\n\n        mViews = views;\n        showView(0, false);\n    }\n\n    public void setOnShowViewListener(OnShowViewListener listener) {\n        mOnShowViewListener = listener;\n    }\n\n    public int getShownViewIndex() {\n        return mShownView;\n    }\n\n    public boolean showView(int shownView) {\n        return showView(shownView, true);\n    }\n\n    public boolean showView(int shownView, boolean animation) {\n        View[] views = mViews;\n        int length = views.length;\n        if (shownView >= length || shownView < 0) {\n            throw new IndexOutOfBoundsException(\"Only \" + length + \" view(s) in \" +\n                    \"the ViewTransition, but attempt to show \" + shownView);\n        }\n\n        if (mShownView != shownView) {\n            int oldShownView = mShownView;\n            mShownView = shownView;\n\n            // Cancel animation\n            if (mAnimator1 != null) {\n                mAnimator1.cancel();\n            }\n            if (mAnimator2 != null) {\n                mAnimator2.cancel();\n            }\n\n            if (animation) {\n                startAnimations(views[oldShownView], views[shownView]);\n            } else {\n                for (int i = 0; i < length; i++) {\n                    View v = views[i];\n                    if (i == shownView) {\n                        v.setAlpha(1f);\n                        v.setVisibility(View.VISIBLE);\n                    } else {\n                        v.setAlpha(0f);\n                        v.setVisibility(View.GONE);\n                    }\n                }\n            }\n\n            if (null != mOnShowViewListener) {\n                mOnShowViewListener.onShowView(views[oldShownView], views[shownView]);\n            }\n\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    protected void startAnimations(final View hiddenView, final View shownView) {\n        mAnimator1 = hiddenView.animate().alpha(0);\n        mAnimator1.setDuration(ANIMATE_TIME).setListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                hiddenView.setVisibility(View.GONE);\n                mAnimator1 = null;\n            }\n        }).start();\n\n        shownView.setAlpha(0);\n        shownView.setVisibility(View.VISIBLE);\n        mAnimator2 = shownView.animate().alpha(1);\n        mAnimator2.setDuration(ANIMATE_TIME).setListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                mAnimator2 = null;\n            }\n        }).start();\n    }\n\n    public interface OnShowViewListener {\n        void onShowView(View hiddenView, View shownView);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/AutoWrapLayout.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Rect;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport com.hippo.ehviewer.R;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * A ViewGroup that can layout views in line and auto wrap\n *\n * @author Hippo\n */\npublic class AutoWrapLayout extends ViewGroup {\n    private static final Alignment[] sBaseLineArray = {Alignment.TOP,\n            Alignment.CENTER, Alignment.BOTTOM};\n    private final List<Rect> rectList = new ArrayList<>();\n    private Alignment mAlignment;\n\n    public AutoWrapLayout(Context context) {\n        super(context);\n    }\n\n    public AutoWrapLayout(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public AutoWrapLayout(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n\n        //noinspection resource\n        TypedArray a = context.obtainStyledAttributes(attrs,\n                R.styleable.AutoWrapLayout, defStyle, 0);\n        try {\n            int index = a.getInt(R.styleable.AutoWrapLayout_alignment, -1);\n            if (index >= 0) {\n                setAlignment(sBaseLineArray[index]);\n            }\n        } finally {\n            a.recycle();\n        }\n    }\n\n    public Alignment getAlignment() {\n        return mAlignment;\n    }\n\n    public void setAlignment(Alignment baseLine) {\n        if (baseLine == null) {\n            return;\n        }\n\n        if (mAlignment != baseLine) {\n            mAlignment = baseLine;\n\n            requestLayout();\n            invalidate();\n        }\n    }\n\n    private void adjustBaseLine(int lineHeight, int startIndex, int endIndex) {\n        if (mAlignment == Alignment.TOP)\n            return;\n\n        for (int index = startIndex; index < endIndex; index++) {\n            final View child = getChildAt(index);\n            final MarginLayoutParams lp =\n                    (MarginLayoutParams) child.getLayoutParams();\n            Rect rect = rectList.get(index);\n            int offsetRaw = lineHeight - rect.height() - lp.topMargin - lp.bottomMargin;\n            if (mAlignment == Alignment.CENTER)\n                rect.offset(0, offsetRaw / 2);\n            else if (mAlignment == Alignment.BOTTOM)\n                rect.offset(0, offsetRaw);\n        }\n    }\n\n    /**\n     * each row or line at least show one child\n     * <p>\n     * horizontal only show child can show or partly show in parent\n     */\n    @SuppressLint(\"DrawAllocation\")\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int widthMode = MeasureSpec.getMode(widthMeasureSpec);\n        int heightMode = MeasureSpec.getMode(heightMeasureSpec);\n\n        int maxWidth = MeasureSpec.getSize(widthMeasureSpec);\n        int maxHeight = MeasureSpec.getSize(heightMeasureSpec);\n\n        if (widthMode == MeasureSpec.UNSPECIFIED)\n            maxWidth = Integer.MAX_VALUE;\n        if (heightMode == MeasureSpec.UNSPECIFIED)\n            maxHeight = Integer.MAX_VALUE;\n\n        int paddingLeft = getPaddingLeft();\n        int paddingTop = getPaddingTop();\n        int paddingRight = getPaddingRight();\n        int paddingBottom = getPaddingBottom();\n\n        int maxRightBound = maxWidth - paddingRight;\n        int maxBottomBound = maxHeight - paddingBottom;\n\n        int left;\n        int top;\n        int right;\n        int bottom;\n        int rightBound = paddingLeft;\n        int maxRightNoPadding = rightBound;\n        int bottomBound;\n        int lastMaxBottom = paddingTop;\n        int maxBottom = lastMaxBottom;\n        int childWidth;\n        int childHeight;\n\n        int lineStartIndex = 0;\n        int lineEndIndex; // endIndex + 1\n\n        rectList.clear();\n        int childCount = getChildCount();\n        for (int index = 0; index < childCount; index++) {\n            final View child = getChildAt(index);\n            child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);\n            if (child.getVisibility() == View.GONE)\n                continue;\n            final MarginLayoutParams lp =\n                    (MarginLayoutParams) child.getLayoutParams();\n            childWidth = child.getMeasuredWidth();\n            childHeight = child.getMeasuredHeight();\n\n            left = rightBound + lp.leftMargin;\n            right = left + childWidth;\n            rightBound = right + lp.rightMargin;\n            if (rightBound > maxRightBound) { // Go to next row\n                lineEndIndex = index;\n                // Adjust child position base on baseline\n                adjustBaseLine(maxBottom - lastMaxBottom, lineStartIndex, lineEndIndex);\n\n                // If child can't show in parent begin this line\n                if (maxBottom >= maxBottomBound)\n                    break;\n\n                // If it is first item in line, try to show it all\n                if (lineEndIndex == lineStartIndex) {\n                    child.measure(MeasureSpec.makeMeasureSpec(\n                                    maxWidth - paddingLeft - paddingRight\n                                            - lp.leftMargin - lp.rightMargin, MeasureSpec.AT_MOST),\n                            MeasureSpec.UNSPECIFIED);\n                    childWidth = child.getMeasuredWidth();\n                    childHeight = child.getMeasuredHeight();\n                }\n                left = paddingLeft + lp.leftMargin;\n                right = left + childWidth;\n                rightBound = right + lp.rightMargin;\n\n                lastMaxBottom = maxBottom;\n                top = lastMaxBottom + lp.topMargin;\n                bottom = top + childHeight;\n                bottomBound = bottom + lp.bottomMargin;\n\n                lineStartIndex = index;\n            } else {\n                top = lastMaxBottom + lp.topMargin;\n                bottom = top + childHeight;\n                bottomBound = bottom + lp.bottomMargin;\n            }\n            // Update max\n            if (rightBound > maxRightNoPadding)\n                maxRightNoPadding = rightBound;\n            if (bottomBound > maxBottom)\n                maxBottom = bottomBound;\n            Rect rect = new Rect();\n            rect.left = left;\n            rect.top = top;\n            rect.right = right;\n            rect.bottom = bottom;\n            rectList.add(rect);\n        }\n\n        // Handle last line baseline\n        adjustBaseLine(maxBottom - lastMaxBottom, lineStartIndex, rectList.size());\n\n        int measuredWidth;\n        int measuredHeight;\n\n        if (widthMode == MeasureSpec.EXACTLY)\n            measuredWidth = maxWidth;\n        else\n            measuredWidth = maxRightNoPadding + paddingRight;\n        if (heightMode == MeasureSpec.EXACTLY)\n            measuredHeight = maxHeight;\n        else {\n            measuredHeight = maxBottom + paddingBottom;\n            if (heightMode == MeasureSpec.AT_MOST)\n                measuredHeight = Math.min(measuredHeight, maxHeight);\n        }\n\n        setMeasuredDimension(measuredWidth, measuredHeight);\n    }\n\n    // TODO Take vertical mode\n    @Override\n    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n        final int count = rectList.size();\n        for (int i = 0; i < count; i++) {\n            final View child = this.getChildAt(i);\n            if (child.getVisibility() == View.GONE)\n                continue;\n            Rect rect = rectList.get(i);\n            child.layout(rect.left, rect.top, rect.right, rect.bottom);\n        }\n    }\n\n    @Override\n    public MarginLayoutParams generateLayoutParams(AttributeSet attrs) {\n        return new MarginLayoutParams(getContext(), attrs);\n    }\n\n    @Override\n    protected MarginLayoutParams generateDefaultLayoutParams() {\n        return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);\n    }\n\n    @Override\n    protected MarginLayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {\n        return new MarginLayoutParams(p);\n    }\n\n    public enum Alignment {\n        TOP(0),\n        CENTER(1),\n        BOTTOM(2);\n\n        final int nativeInt;\n\n        Alignment(int ni) {\n            nativeInt = ni;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/BatteryView.kt",
    "content": "/*\n * Copyright (C) 2014 Hippo Seven\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.hippo.widget\n\nimport android.annotation.SuppressLint\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.graphics.Color\nimport android.os.BatteryManager\nimport android.util.AttributeSet\nimport androidx.appcompat.widget.AppCompatTextView\nimport androidx.core.content.withStyledAttributes\nimport com.hippo.drawable.BatteryDrawable\nimport com.hippo.ehviewer.R\n\nclass BatteryView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : AppCompatTextView(\n    context,\n    attrs,\n    defStyleAttr,\n) {\n    private var mColor = 0\n    private var mWarningColor = 0\n    private var mCurrentColor = 0\n    private var mLevel = 0\n    private var mCharging = false\n    private var mDrawable: BatteryDrawable? = null\n    private var mAttached = false\n    private var mIsChargerWorking = false\n    private val mCharger: Runnable = object : Runnable {\n        private var level = 0\n        override fun run() {\n            level += 2\n            if (level > 100) {\n                level = 0\n            }\n            mDrawable!!.setElect(level, false)\n            getHandler().postDelayed(this, 200)\n        }\n    }\n    private val mIntentReceiver: BroadcastReceiver = object : BroadcastReceiver() {\n        @SuppressLint(\"SetTextI18n\")\n        override fun onReceive(context: Context, intent: Intent) {\n            val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0)\n            val charging = (\n                intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)\n                    == BatteryManager.BATTERY_STATUS_CHARGING\n                )\n            if (mLevel != level || mCharging != charging) {\n                mLevel = level\n                mCharging = charging\n                if (mCharging && mLevel != 100) {\n                    startCharger()\n                } else {\n                    stopCharger()\n                    mDrawable!!.setElect(mLevel)\n                }\n                if (level <= BatteryDrawable.WARN_LIMIT && !charging) {\n                    setTextColor(mWarningColor)\n                } else {\n                    setTextColor(mColor)\n                }\n                text = \"$mLevel%\"\n            }\n        }\n    }\n\n    init {\n        init()\n        context.withStyledAttributes(\n            attrs,\n            R.styleable.BatteryView,\n            defStyleAttr,\n            0,\n        ) {\n            mColor = getColor(R.styleable.BatteryView_color, Color.WHITE)\n            mWarningColor = getColor(R.styleable.BatteryView_warningColor, Color.RED)\n        }\n        mDrawable!!.setColor(mColor)\n        mDrawable!!.setWarningColor(mWarningColor)\n    }\n\n    private fun init() {\n        mDrawable = BatteryDrawable()\n        val height = textSize.toInt()\n        mDrawable!!.setBounds(0, 0, (height / 0.618f).toInt(), height)\n        setCompoundDrawables(mDrawable, null, null, null)\n    }\n\n    override fun setTextColor(color: Int) {\n        if (mCurrentColor == color) {\n            return\n        }\n        mCurrentColor = color\n        super.setTextColor(color)\n    }\n\n    private fun startCharger() {\n        if (!mIsChargerWorking) {\n            getHandler().post(mCharger)\n            mIsChargerWorking = true\n        }\n    }\n\n    private fun stopCharger() {\n        if (mIsChargerWorking) {\n            getHandler().removeCallbacks(mCharger)\n            mIsChargerWorking = false\n        }\n    }\n\n    override fun onAttachedToWindow() {\n        super.onAttachedToWindow()\n        if (!mAttached) {\n            mAttached = true\n            registerReceiver()\n        }\n    }\n\n    override fun onDetachedFromWindow() {\n        super.onDetachedFromWindow()\n        if (mAttached) {\n            unregisterReceiver()\n            stopCharger()\n            mAttached = false\n        }\n    }\n\n    private fun registerReceiver() {\n        val filter = IntentFilter()\n        filter.addAction(Intent.ACTION_BATTERY_CHANGED)\n        context.registerReceiver(mIntentReceiver, filter, null, getHandler())\n    }\n\n    private fun unregisterReceiver() {\n        context.unregisterReceiver(mIntentReceiver)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/CheckTextView.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.widget\n\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.Rect\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport android.view.Gravity\nimport android.view.View\nimport androidx.appcompat.widget.AppCompatCheckedTextView\nimport androidx.core.content.withStyledAttributes\nimport com.hippo.ehviewer.R\n\nopen class CheckTextView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : AppCompatCheckedTextView(context, attrs, defStyleAttr),\n    View.OnClickListener {\n    private val mSelfBounds = Rect()\n    private val mOverlayBounds = Rect()\n    private var mForegroundInPadding = true\n    private var mForegroundBoundsChanged = false\n    private var mForeground: Drawable? = null\n    private var mForegroundGravity = Gravity.FILL\n\n    init {\n        context.withStyledAttributes(\n            attrs,\n            R.styleable.CheckTextView,\n            defStyleAttr,\n            0,\n        ) {\n            mForegroundGravity = getInt(\n                R.styleable.CheckTextView_android_foregroundGravity,\n                mForegroundGravity,\n            )\n            getDrawable(R.styleable.CheckTextView_android_foreground)?.let {\n                foreground = it\n            }\n            mForegroundInPadding = getBoolean(\n                R.styleable.CheckTextView_foregroundInsidePadding,\n                true,\n            )\n        }\n        setOnClickListener(this)\n    }\n\n    /**\n     * Describes how the foreground is positioned.\n     *\n     * @return foreground gravity.\n     * @see .setForegroundGravity\n     */\n    override fun getForegroundGravity(): Int = mForegroundGravity\n\n    /**\n     * Describes how the foreground is positioned. Defaults to START and TOP.\n     *\n     * @param foregroundGravity See [android.view.Gravity]\n     * @see .getForegroundGravity\n     */\n    override fun setForegroundGravity(foregroundGravity: Int) {\n        var sForegroundGravity = foregroundGravity\n        if (mForegroundGravity != sForegroundGravity) {\n            if (sForegroundGravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK == 0) {\n                sForegroundGravity = sForegroundGravity or Gravity.START\n            }\n            if (sForegroundGravity and Gravity.VERTICAL_GRAVITY_MASK == 0) {\n                sForegroundGravity = sForegroundGravity or Gravity.TOP\n            }\n            mForegroundGravity = sForegroundGravity\n            if (mForegroundGravity == Gravity.FILL && mForeground != null) {\n                val padding = Rect()\n                mForeground!!.getPadding(padding)\n            }\n            requestLayout()\n        }\n    }\n\n    override fun verifyDrawable(who: Drawable): Boolean = super.verifyDrawable(who) || who === mForeground\n\n    override fun jumpDrawablesToCurrentState() {\n        super.jumpDrawablesToCurrentState()\n        if (mForeground != null) {\n            mForeground!!.jumpToCurrentState()\n        }\n    }\n\n    override fun drawableStateChanged() {\n        super.drawableStateChanged()\n        if (mForeground != null && mForeground!!.isStateful) {\n            mForeground!!.state = drawableState\n        }\n    }\n\n    /**\n     * Returns the drawable used as the foreground of this FrameLayout. The\n     * foreground drawable, if non-null, is always drawn on top of the children.\n     *\n     * @return A Drawable or null if no foreground was set.\n     */\n    override fun getForeground(): Drawable = mForeground!!\n\n    /**\n     * Supply a Drawable that is to be rendered on top of all of the child\n     * views in the frame layout.  Any padding in the Drawable will be taken\n     * into account by ensuring that the children are inset to be placed\n     * inside of the padding area.\n     *\n     * @param drawable The Drawable to be drawn on top of the children.\n     */\n    override fun setForeground(drawable: Drawable?) {\n        if (mForeground !== drawable) {\n            mForeground?.let {\n                it.callback = null\n                unscheduleDrawable(it)\n            }\n            mForeground = drawable\n            if (drawable != null) {\n                setWillNotDraw(false)\n                drawable.callback = this\n                if (drawable.isStateful) {\n                    drawable.state = drawableState\n                }\n                if (mForegroundGravity == Gravity.FILL) {\n                    val padding = Rect()\n                    drawable.getPadding(padding)\n                }\n            } else {\n                setWillNotDraw(true)\n            }\n            requestLayout()\n            invalidate()\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        mForegroundBoundsChanged = mForegroundBoundsChanged or changed\n    }\n\n    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {\n        super.onSizeChanged(w, h, oldw, oldh)\n        mForegroundBoundsChanged = true\n    }\n\n    override fun draw(canvas: Canvas) {\n        super.draw(canvas)\n        if (mForeground != null) {\n            val foreground: Drawable = mForeground!!\n            if (mForegroundBoundsChanged) {\n                mForegroundBoundsChanged = false\n                val selfBounds = mSelfBounds\n                val overlayBounds = mOverlayBounds\n                val w = right - left\n                val h = bottom - top\n                if (mForegroundInPadding) {\n                    selfBounds[0, 0, w] = h\n                } else {\n                    selfBounds[paddingLeft, paddingTop, w - paddingRight] = h - paddingBottom\n                }\n                Gravity.apply(\n                    mForegroundGravity,\n                    foreground.intrinsicWidth,\n                    foreground.intrinsicHeight,\n                    selfBounds,\n                    overlayBounds,\n                )\n                foreground.bounds = overlayBounds\n            }\n            foreground.draw(canvas)\n        }\n    }\n\n    override fun drawableHotspotChanged(x: Float, y: Float) {\n        super.drawableHotspotChanged(x, y)\n        if (mForeground != null) {\n            mForeground!!.setHotspot(x, y)\n        }\n    }\n\n    override fun onClick(v: View) {\n        isChecked = !isChecked\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/ColorView.java",
    "content": "/*\n * Copyright (C) 2014 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.util.AttributeSet;\nimport android.view.View;\n\npublic class ColorView extends View {\n    private int mColor;\n\n    public ColorView(Context context) {\n        super(context);\n    }\n\n    public ColorView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public ColorView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    public void setColor(int color) {\n        if (mColor != color) {\n            mColor = color;\n            invalidate();\n        }\n    }\n\n    @Override\n    protected void onDraw(Canvas canvas) {\n        canvas.drawColor(mColor);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/ContentLayout.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.widget\n\nimport android.app.Activity\nimport android.content.Context\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.util.Log\nimport android.view.View\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport android.widget.TextView\nimport android.widget.Toast\nimport androidx.appcompat.app.AlertDialog\nimport androidx.core.content.ContextCompat\nimport androidx.recyclerview.widget.RecyclerView\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout\nimport androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener\nimport com.google.android.material.progressindicator.CircularProgressIndicator\nimport com.google.android.material.progressindicator.LinearProgressIndicator\nimport com.hippo.easyrecyclerview.EasyRecyclerView\nimport com.hippo.easyrecyclerview.FastScroller\nimport com.hippo.easyrecyclerview.HandlerDrawable\nimport com.hippo.easyrecyclerview.LayoutManagerUtils\nimport com.hippo.easyrecyclerview.LayoutManagerUtils.OnScrollToPositionListener\nimport com.hippo.ehviewer.EhApplication\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhUrl\nimport com.hippo.ehviewer.client.exception.CloudflareBypassException\nimport com.hippo.ehviewer.ui.WebViewActivity\nimport com.hippo.util.ExceptionUtils\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.view.ViewTransition\nimport com.hippo.view.ViewTransition.OnShowViewListener\nimport com.hippo.yorozuya.IntIdGenerator\nimport com.hippo.yorozuya.LayoutUtils\nimport com.hippo.yorozuya.collect.IntList\nimport rikka.core.res.resolveColor\n\nclass ContentLayout @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n) : FrameLayout(context, attrs) {\n    private lateinit var mContentHelper: ContentHelper<*>\n    private val mProgressView: CircularProgressIndicator\n    private val mTipView: TextView\n    private val mContentView: ViewGroup\n    private val mRefreshLayout: SwipeRefreshLayout\n    private val mBottomProgress: LinearProgressIndicator\n    private val mRecyclerView: EasyRecyclerView\n    private val mFastScroller: FastScroller\n    private val mRecyclerViewOriginBottom: Int\n    private val mFastScrollerOriginBottom: Int\n\n    init {\n        (context as Activity).layoutInflater.inflate(R.layout.widget_content_layout, this)\n        clipChildren = false\n        clipToPadding = false\n        mProgressView = findViewById(R.id.progress)\n        mTipView = findViewById(R.id.tip)\n        mContentView = findViewById(R.id.content_view)\n        mRefreshLayout = mContentView.findViewById(R.id.refresh_layout)\n        mBottomProgress = mContentView.findViewById(R.id.bottom_progress)\n        mFastScroller = mContentView.findViewById(R.id.fast_scroller)\n        mRecyclerView = mRefreshLayout.findViewById(R.id.recycler_view)\n        mFastScroller.attachToRecyclerView(mRecyclerView)\n        val drawable = HandlerDrawable()\n        drawable.setColor(context.theme.resolveColor(R.attr.widgetColorThemeAccent))\n        mFastScroller.setHandlerDrawable(drawable)\n        mRefreshLayout.setColorSchemeResources(\n            R.color.loading_indicator_red,\n            R.color.loading_indicator_purple,\n            R.color.loading_indicator_blue,\n            R.color.loading_indicator_cyan,\n            R.color.loading_indicator_green,\n            R.color.loading_indicator_yellow,\n        )\n        mBottomProgress.setIndicatorColor(\n            context.getColor(R.color.loading_indicator_red),\n            context.getColor(R.color.loading_indicator_blue),\n            context.getColor(R.color.loading_indicator_green),\n            context.getColor(R.color.loading_indicator_orange),\n        )\n        mBottomProgress.indeterminateAnimationType = LinearProgressIndicator.INDETERMINATE_ANIMATION_TYPE_CONTIGUOUS\n        mRecyclerViewOriginBottom = mRecyclerView.paddingBottom\n        mFastScrollerOriginBottom = mFastScroller.paddingBottom\n    }\n\n    val recyclerView\n        get() = mRecyclerView\n\n    val fastScroller\n        get() = mFastScroller\n\n    fun setHelper(helper: ContentHelper<*>) {\n        mContentHelper = helper\n        helper.init(this)\n    }\n\n    fun hideFastScroll() {\n        mFastScroller.detachedFromRecyclerView()\n    }\n\n    override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {\n        super.setPadding(left, top, right, 0)\n        setFitPaddingBottom(bottom)\n    }\n\n    fun setFitPaddingTop(fitPaddingTop: Int) {\n        // RefreshLayout\n        mRefreshLayout.setProgressViewOffset(\n            true,\n            0,\n            fitPaddingTop + LayoutUtils.dp2pix(context, 32f),\n        ) // TODO\n    }\n\n    private fun setFitPaddingBottom(fitPaddingBottom: Int) {\n        // RecyclerView\n        mRecyclerView.setPadding(\n            mRecyclerView.paddingLeft,\n            mRecyclerView.paddingTop,\n            mRecyclerView.paddingRight,\n            mRecyclerViewOriginBottom + fitPaddingBottom,\n        )\n        mTipView.setPadding(\n            mTipView.paddingLeft,\n            mTipView.paddingTop,\n            mTipView.paddingRight,\n            fitPaddingBottom,\n        )\n        mProgressView.setPadding(\n            mProgressView.paddingLeft,\n            mProgressView.paddingTop,\n            mProgressView.paddingRight,\n            fitPaddingBottom,\n        )\n        mFastScroller.setPadding(\n            mFastScroller.paddingLeft,\n            mFastScroller.paddingTop,\n            mFastScroller.paddingRight,\n            mFastScrollerOriginBottom + fitPaddingBottom,\n        )\n        if (fitPaddingBottom > LayoutUtils.dp2pix(context, 16f)) {\n            mBottomProgress.setPadding(0, 0, 0, fitPaddingBottom)\n        } else {\n            mBottomProgress.setPadding(0, 0, 0, 0)\n        }\n    }\n\n    override fun onSaveInstanceState(): Parcelable = mContentHelper.saveInstanceState(super.onSaveInstanceState())\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        super.onRestoreInstanceState(mContentHelper.restoreInstanceState(state))\n    }\n\n    abstract class ContentHelper<E : Parcelable?> : OnShowViewListener {\n        /**\n         * Generate task id\n         */\n        private val mIdGenerator = IntIdGenerator()\n        private val mOnScrollToPositionListener =\n            OnScrollToPositionListener { position: Int -> onScrollToPosition(position) }\n        protected var mPrev: String? = null\n        protected var mNext: String? = null\n        private var mTipView: TextView? = null\n        private var mRefreshLayout: SwipeRefreshLayout? = null\n        private var mBottomProgress: LinearProgressIndicator? = null\n        private var mRecyclerView: EasyRecyclerView? = null\n        private var mViewTransition: ViewTransition? = null\n\n        /**\n         * Store data\n         */\n        private var mData = ArrayList<E>()\n\n        /**\n         * Store the page divider index\n         *\n         *\n         * For example, the data contain page 3, page 4, page 5,\n         * page 3 size is 7, page 4 size is 8, page 5 size is 9,\n         * so `mPageDivider` contain 7, 15, 24.\n         */\n        private var mPageDivider: IntList? = IntList()\n\n        /**\n         * The first page in `mData`\n         */\n        private var mStartPage = 0\n\n        /**\n         * The last page + 1 in `mData`\n         */\n        private var mEndPage = 0\n\n        /**\n         * The available page count.\n         */\n        var pages = 0\n            private set\n        private var mNextPage = 0\n        private var mCurrentTaskId = 0\n        private var mCurrentTaskType = 0\n        private var mCurrentTaskPage = 0\n        private val mOnRefreshListener = OnRefreshListener {\n            if (mPrev != null || mStartPage > 0) {\n                mCurrentTaskId = mIdGenerator.nextId()\n                mCurrentTaskType = TYPE_PRE_PAGE_KEEP_POS\n                mCurrentTaskPage = mStartPage - 1\n                getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, mPrev, false)\n            } else {\n                doRefresh()\n            }\n        }\n        private var mNextPageScrollSize = 0\n        private var mEmptyString = \"No hint\"\n        private val mOnScrollListener: RecyclerView.OnScrollListener =\n            object : RecyclerView.OnScrollListener() {\n                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {\n                    if (!mRefreshLayout!!.isRefreshing && !recyclerView.canScrollVertically(1)) {\n                        if (mNext != null || mEndPage < pages) {\n                            mBottomProgress!!.show()\n                            // Get next page\n                            // Fill pages before NextPage with empty list\n                            while (mNextPage > mEndPage && mEndPage < pages) {\n                                mCurrentTaskId = mIdGenerator.nextId()\n                                mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS\n                                mCurrentTaskPage = mEndPage\n                                onGetPageData(\n                                    mCurrentTaskId,\n                                    pages,\n                                    mNextPage,\n                                    null,\n                                    null,\n                                    emptyList(),\n                                )\n                            }\n                            mCurrentTaskId = mIdGenerator.nextId()\n                            mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS\n                            mCurrentTaskPage = mEndPage\n                            getPageData(\n                                mCurrentTaskId,\n                                mCurrentTaskType,\n                                mCurrentTaskPage,\n                                mNext,\n                                true,\n                            )\n                        } else if (mStartPage > 0 && mEndPage == pages) {\n                            mBottomProgress!!.show()\n                            // Refresh last page\n                            mCurrentTaskId = mIdGenerator.nextId()\n                            mCurrentTaskType = TYPE_REFRESH_PAGE\n                            mCurrentTaskPage = mEndPage - 1\n                            getPageData(\n                                mCurrentTaskId,\n                                mCurrentTaskType,\n                                mCurrentTaskPage,\n                                null,\n                                true,\n                            )\n                        }\n                    }\n                }\n            }\n        private var mSavedDataId = IntIdGenerator.INVALID_ID\n        fun init(contentLayout: ContentLayout) {\n            mNextPageScrollSize = LayoutUtils.dp2pix(contentLayout.context, 48f)\n            val mProgressView = contentLayout.mProgressView\n            mTipView = contentLayout.mTipView\n            val mContentView = contentLayout.mContentView\n            mRefreshLayout = contentLayout.mRefreshLayout\n            mBottomProgress = contentLayout.mBottomProgress\n            mRecyclerView = contentLayout.mRecyclerView\n            val drawable = ContextCompat.getDrawable(context, R.drawable.big_sad_pandroid)!!\n            drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)\n            mTipView!!.setCompoundDrawables(null, drawable, null, null)\n            mViewTransition = ViewTransition(mContentView, mProgressView, mTipView)\n            mViewTransition!!.setOnShowViewListener(this)\n            mRecyclerView!!.addOnScrollListener(mOnScrollListener)\n            mRefreshLayout!!.setOnRefreshListener(mOnRefreshListener)\n            mTipView!!.setOnClickListener { refresh() }\n        }\n\n        /**\n         * Call [.onGetPageData] when get data\n         *\n         * @param taskId task id\n         * @param page   the page to get\n         * @param index  the index to get\n         * @param isNext the index is next or prev\n         */\n        protected abstract fun getPageData(\n            taskId: Int,\n            type: Int,\n            page: Int,\n            index: String?,\n            isNext: Boolean,\n        )\n\n        protected abstract val context: Context\n        protected abstract fun notifyDataSetChanged()\n        protected abstract fun notifyItemRangeInserted(positionStart: Int, itemCount: Int)\n        protected open fun onScrollToPosition(position: Int) {}\n        override fun onShowView(hiddenView: View, shownView: View) {}\n        val shownViewIndex: Int\n            get() = mViewTransition!!.shownViewIndex\n\n        fun setRefreshLayoutEnable(enable: Boolean) {\n            mRefreshLayout!!.isEnabled = enable\n        }\n\n        fun setEnable(enable: Boolean) {\n            mRefreshLayout!!.isEnabled = enable\n        }\n\n        fun setEmptyString(str: String) {\n            mEmptyString = str\n        }\n\n        val data: List<E>\n            get() = mData\n\n        fun getDataAtEx(location: Int): E? = if (location >= 0 && location < mData.size) {\n            mData[location]\n        } else {\n            null\n        }\n\n        val firstVisibleItem: E?\n            get() = getDataAtEx(LayoutManagerUtils.getFirstVisibleItemPosition(mRecyclerView!!.layoutManager!!))\n\n        fun size(): Int = mData.size\n\n        fun isCurrentTask(taskId: Int): Boolean = mCurrentTaskId == taskId\n\n        protected abstract fun isDuplicate(d1: E, d2: E): Boolean\n        private fun removeDuplicateData(data: List<E>, start: Int, end: Int) {\n            val slicedData = mData.slice(start.coerceAtLeast(0) until end.coerceAtMost(mData.size))\n            data.dropWhile { d1 ->\n                slicedData.any { d2 ->\n                    isDuplicate(d1, d2)\n                }\n            }\n        }\n\n        protected open fun onAddData(data: List<E>) {}\n        protected open fun onRemoveData(data: List<E>) {}\n        protected open fun onClearData() {}\n        fun onGetPageData(\n            taskId: Int,\n            pages: Int,\n            nextPage: Int,\n            prev: String?,\n            next: String?,\n            data: List<E>,\n        ) {\n            if (mCurrentTaskId == taskId) {\n                val dataSize: Int\n                when (mCurrentTaskType) {\n                    TYPE_REFRESH -> {\n                        mStartPage = 0\n                        mEndPage = 1\n                        this.pages = pages\n                        mNextPage = nextPage\n                        mPrev = prev\n                        mNext = next\n                        mPageDivider!!.clear()\n                        mPageDivider!!.add(data.size)\n                        if (data.isEmpty()) {\n                            mData.clear()\n                            onClearData()\n                            notifyDataSetChanged()\n\n                            // Not found\n                            // Ui change, show empty string\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showEmptyString()\n                        } else {\n                            mData.clear()\n                            onClearData()\n                            mData.addAll(data)\n                            onAddData(data)\n                            notifyDataSetChanged()\n\n                            // Ui change, show content\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showContent()\n\n                            // RecyclerView scroll\n                            if (mRecyclerView!!.isAttachedToWindow) {\n                                mRecyclerView!!.stopScroll()\n                                LayoutManagerUtils.scrollToPositionWithOffset(\n                                    mRecyclerView!!.layoutManager!!,\n                                    0,\n                                    0,\n                                )\n                                onScrollToPosition(0)\n                            }\n                        }\n                    }\n                    TYPE_PRE_PAGE, TYPE_PRE_PAGE_KEEP_POS -> {\n                        removeDuplicateData(data, 0, CHECK_DUPLICATE_RANGE)\n                        dataSize = data.size\n                        var i = 0\n                        val n = mPageDivider!!.size\n                        while (i < n) {\n                            mPageDivider!![i] = mPageDivider!![i] + dataSize\n                            i++\n                        }\n                        mPageDivider!!.add(0, dataSize)\n                        mStartPage--\n                        this.pages = pages.coerceAtLeast(mEndPage)\n                        mPrev = prev\n                        // assert mStartPage >= 0\n                        if (data.isEmpty()) {\n                            // OK, that's all\n                            if (mData.isEmpty()) {\n                                // Ui change, show empty string\n                                mRefreshLayout!!.isRefreshing = false\n                                mBottomProgress!!.hide()\n                                showEmptyString()\n                            } else {\n                                // Ui change, show content\n                                mRefreshLayout!!.isRefreshing = false\n                                mBottomProgress!!.hide()\n                                showContent()\n                                if (mCurrentTaskType == TYPE_PRE_PAGE && mRecyclerView!!.isAttachedToWindow) {\n                                    // RecyclerView scroll, to top\n                                    mRecyclerView!!.stopScroll()\n                                    LayoutManagerUtils.scrollToPositionWithOffset(\n                                        mRecyclerView!!.layoutManager!!,\n                                        0,\n                                        0,\n                                    )\n                                    onScrollToPosition(0)\n                                }\n                            }\n                        } else {\n                            mData.addAll(0, data)\n                            onAddData(data)\n                            notifyItemRangeInserted(0, data.size)\n\n                            // Ui change, show content\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showContent()\n                            if (mRecyclerView!!.isAttachedToWindow) {\n                                // RecyclerView scroll\n                                if (mCurrentTaskType == TYPE_PRE_PAGE_KEEP_POS) {\n                                    mRecyclerView!!.stopScroll()\n                                    LayoutManagerUtils.scrollToPositionProperly(\n                                        mRecyclerView!!.layoutManager!!,\n                                        context,\n                                        dataSize - 1,\n                                        mOnScrollToPositionListener,\n                                    )\n                                } else {\n                                    mRecyclerView!!.stopScroll()\n                                    LayoutManagerUtils.scrollToPositionWithOffset(\n                                        mRecyclerView!!.layoutManager!!,\n                                        0,\n                                        0,\n                                    )\n                                    onScrollToPosition(0)\n                                }\n                            }\n                        }\n                    }\n                    TYPE_NEXT_PAGE, TYPE_NEXT_PAGE_KEEP_POS -> {\n                        removeDuplicateData(data, mData.size - CHECK_DUPLICATE_RANGE, mData.size)\n                        dataSize = data.size\n                        val oldDataSize = mData.size\n                        mPageDivider!!.add(oldDataSize + dataSize)\n                        mEndPage++\n                        mNextPage = nextPage\n                        this.pages = pages.coerceAtLeast(mEndPage)\n                        mNext = next\n                        if (data.isEmpty()) {\n                            // OK, that's all\n                            if (mData.isEmpty()) {\n                                // Ui change, show empty string\n                                mRefreshLayout!!.isRefreshing = false\n                                mBottomProgress!!.hide()\n                                showEmptyString()\n                            } else {\n                                // Ui change, show content\n                                mRefreshLayout!!.isRefreshing = false\n                                mBottomProgress!!.hide()\n                                showContent()\n                                if (mCurrentTaskType == TYPE_NEXT_PAGE && mRecyclerView!!.isAttachedToWindow) {\n                                    // RecyclerView scroll\n                                    mRecyclerView!!.stopScroll()\n                                    LayoutManagerUtils.scrollToPositionWithOffset(\n                                        mRecyclerView!!.layoutManager!!,\n                                        oldDataSize,\n                                        0,\n                                    )\n                                    onScrollToPosition(oldDataSize)\n                                }\n                            }\n                        } else {\n                            mData.addAll(data)\n                            onAddData(data)\n                            notifyItemRangeInserted(oldDataSize, dataSize)\n\n                            // Ui change, show content\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showContent()\n                            if (mRecyclerView!!.isAttachedToWindow) {\n                                if (mCurrentTaskType == TYPE_NEXT_PAGE_KEEP_POS) {\n                                    mRecyclerView!!.stopScroll()\n                                    mRecyclerView!!.smoothScrollBy(0, mNextPageScrollSize)\n                                } else {\n                                    mRecyclerView!!.stopScroll()\n                                    LayoutManagerUtils.scrollToPositionWithOffset(\n                                        mRecyclerView!!.layoutManager!!,\n                                        oldDataSize,\n                                        0,\n                                    )\n                                    onScrollToPosition(oldDataSize)\n                                }\n                            }\n                        }\n                    }\n                    TYPE_SOMEWHERE -> {\n                        mStartPage = mCurrentTaskPage\n                        mEndPage = mCurrentTaskPage + 1\n                        mNextPage = nextPage\n                        this.pages = pages\n                        mPrev = prev\n                        mNext = next\n                        mPageDivider!!.clear()\n                        mPageDivider!!.add(data.size)\n                        if (data.isEmpty()) {\n                            mData.clear()\n                            onClearData()\n                            notifyDataSetChanged()\n\n                            // Not found\n                            // Ui change, show empty string\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showEmptyString()\n                        } else {\n                            mData.clear()\n                            onClearData()\n                            mData.addAll(data)\n                            onAddData(data)\n                            notifyDataSetChanged()\n\n                            // Ui change, show content\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showContent()\n                            if (mRecyclerView!!.isAttachedToWindow) {\n                                // RecyclerView scroll\n                                mRecyclerView!!.stopScroll()\n                                LayoutManagerUtils.scrollToPositionWithOffset(\n                                    mRecyclerView!!.layoutManager!!,\n                                    0,\n                                    0,\n                                )\n                                onScrollToPosition(0)\n                            }\n                        }\n                    }\n                    TYPE_REFRESH_PAGE -> {\n                        if (mCurrentTaskPage < mStartPage || mCurrentTaskPage >= mEndPage) {\n                            Log.e(\n                                TAG,\n                                \"TYPE_REFRESH_PAGE, but mCurrentTaskPage = \" + mCurrentTaskPage +\n                                    \", mStartPage = \" + mStartPage + \", mEndPage = \" + mEndPage,\n                            )\n                            return\n                        }\n                        if (mCurrentTaskPage == mEndPage - 1) {\n                            mNextPage = nextPage\n                        }\n                        this.pages = pages.coerceAtLeast(mEndPage)\n                        val oldIndexStart =\n                            if (mCurrentTaskPage == mStartPage) 0 else mPageDivider!![mCurrentTaskPage - mStartPage - 1]\n                        val oldIndexEnd = mPageDivider!![mCurrentTaskPage - mStartPage]\n                        val toRemove = mData.subList(oldIndexStart, oldIndexEnd)\n                        onRemoveData(toRemove)\n                        toRemove.clear()\n                        removeDuplicateData(\n                            data,\n                            oldIndexStart - CHECK_DUPLICATE_RANGE,\n                            oldIndexStart + CHECK_DUPLICATE_RANGE,\n                        )\n                        val newIndexEnd = oldIndexStart + data.size\n                        mData.addAll(oldIndexStart, data)\n                        onAddData(data)\n                        notifyDataSetChanged()\n                        var i = mCurrentTaskPage - mStartPage\n                        val n = mPageDivider!!.size\n                        while (i < n) {\n                            mPageDivider!![i] = mPageDivider!![i] - oldIndexEnd + newIndexEnd\n                            i++\n                        }\n                        if (mData.isEmpty()) {\n                            // Ui change, show empty string\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showEmptyString()\n                        } else {\n                            // Ui change, show content\n                            mRefreshLayout!!.isRefreshing = false\n                            mBottomProgress!!.hide()\n                            showContent()\n\n                            // RecyclerView scroll\n                            if (newIndexEnd > oldIndexEnd && newIndexEnd > 0 && mRecyclerView!!.isAttachedToWindow) {\n                                mRecyclerView!!.stopScroll()\n                                LayoutManagerUtils.scrollToPositionWithOffset(\n                                    mRecyclerView!!.layoutManager!!,\n                                    newIndexEnd - 1,\n                                    0,\n                                )\n                                onScrollToPosition(newIndexEnd - 1)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        fun onGetException(taskId: Int, e: Exception?) {\n            if (mCurrentTaskId == taskId) {\n                mRefreshLayout!!.isRefreshing = false\n                mBottomProgress!!.hide()\n                val readableError = ExceptionUtils.getReadableString(e)\n                if (mViewTransition!!.shownViewIndex == 0) {\n                    Toast.makeText(context, readableError, Toast.LENGTH_SHORT).show()\n                } else {\n                    showText(readableError)\n                }\n                if (e?.cause is CloudflareBypassException) {\n                    val dialog = AlertDialog.Builder(context)\n                        .setTitle(R.string.cloudflare_bypass_failed)\n                        .setMessage(R.string.open_in_webview)\n                        .setNegativeButton(android.R.string.cancel, null)\n                        .setPositiveButton(android.R.string.ok) { _, _ ->\n                            context.startActivity(WebViewActivity.newIntent(context, EhUrl.host))\n                        }\n                    dialog.show()\n                }\n            }\n        }\n\n        private fun showContent() {\n            mViewTransition!!.showView(0)\n        }\n\n        private val isContentShowing: Boolean\n            get() = mViewTransition!!.shownViewIndex == 0\n\n        fun showProgressBar(animation: Boolean = true) {\n            mViewTransition!!.showView(1, animation)\n        }\n\n        private fun showText(text: CharSequence?) {\n            mTipView!!.text = text\n            mViewTransition!!.showView(2)\n        }\n\n        private fun showEmptyString() {\n            showText(mEmptyString)\n        }\n\n        private fun doRefresh() {\n            mCurrentTaskId = mIdGenerator.nextId()\n            mCurrentTaskType = TYPE_REFRESH\n            mCurrentTaskPage = 0\n            getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)\n        }\n\n        /**\n         * Like [.refresh], but no animation when show progress bar\n         */\n        fun firstRefresh() {\n            showProgressBar(false)\n            doRefresh()\n        }\n\n        /**\n         * Show progress bar first, than do refresh\n         */\n        fun refresh() {\n            showProgressBar()\n            doRefresh()\n        }\n\n        private fun cancelCurrentTask() {\n            mCurrentTaskId = mIdGenerator.nextId()\n            mRefreshLayout!!.isRefreshing = false\n            mBottomProgress!!.hide()\n        }\n\n        private fun getPageStart(page: Int): Int = if (mStartPage == page) {\n            0\n        } else {\n            mPageDivider!![page - mStartPage - 1]\n        }\n\n        private fun getPageForPosition(position: Int): Int {\n            if (position < 0) {\n                return -1\n            }\n            val pageDivider = mPageDivider\n            var i = 0\n            val n = pageDivider!!.size\n            while (i < n) {\n                if (position < pageDivider[i]) {\n                    return i + mStartPage\n                }\n                i++\n            }\n            return -1\n        }\n\n        val pageForTop: Int\n            get() = getPageForPosition(LayoutManagerUtils.getFirstVisibleItemPosition(mRecyclerView!!.layoutManager!!))\n        val pageForBottom: Int\n            get() = getPageForPosition(LayoutManagerUtils.getLastVisibleItemPosition(mRecyclerView!!.layoutManager!!))\n\n        fun canGoTo(): Boolean = isContentShowing\n\n        /**\n         * Check range first!\n         *\n         * @param page the target page\n         * @throws IndexOutOfBoundsException\n         */\n        @Throws(IndexOutOfBoundsException::class)\n        fun goTo(page: Int) {\n            if (page < 0 || (page >= pages && pages != 0)) {\n                throw IndexOutOfBoundsException(\"Page count is $pages, page is $page\")\n            } else if (page in mStartPage until mEndPage) {\n                cancelCurrentTask()\n                val position = getPageStart(page)\n                mRecyclerView!!.stopScroll()\n                LayoutManagerUtils.scrollToPositionWithOffset(\n                    mRecyclerView!!.layoutManager!!,\n                    position,\n                    0,\n                )\n                onScrollToPosition(position)\n            } else if (page == mStartPage - 1) {\n                mRefreshLayout!!.isRefreshing = true\n                mBottomProgress!!.hide()\n                mCurrentTaskId = mIdGenerator.nextId()\n                mCurrentTaskType = TYPE_PRE_PAGE\n                mCurrentTaskPage = page\n                getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)\n            } else if (page == mEndPage) {\n                mRefreshLayout!!.isRefreshing = false\n                mBottomProgress!!.show()\n                mCurrentTaskId = mIdGenerator.nextId()\n                mCurrentTaskType = TYPE_NEXT_PAGE\n                mCurrentTaskPage = page\n                getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)\n            } else {\n                mRefreshLayout!!.isRefreshing = true\n                mBottomProgress!!.hide()\n                mCurrentTaskId = mIdGenerator.nextId()\n                mCurrentTaskType = TYPE_SOMEWHERE\n                mCurrentTaskPage = page\n                getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, null, true)\n            }\n        }\n\n        fun goTo(index: String?, isNext: Boolean) {\n            mRefreshLayout!!.isRefreshing = true\n            mBottomProgress!!.hide()\n            mCurrentTaskId = mIdGenerator.nextId()\n            mCurrentTaskType = TYPE_SOMEWHERE\n            mCurrentTaskPage = 0\n            getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage, index, isNext)\n        }\n\n        fun scrollTo(position: Int) {\n            cancelCurrentTask()\n            mRecyclerView!!.stopScroll()\n            LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView!!.layoutManager!!, position, 0)\n            onScrollToPosition(position)\n        }\n\n        open fun saveInstanceState(superState: Parcelable?): Parcelable {\n            if (mData.isNotEmpty()) cancelCurrentTask()\n            val bundle = Bundle()\n            bundle.putParcelable(KEY_SUPER, superState)\n            val shownView = mViewTransition!!.shownViewIndex\n            bundle.putInt(KEY_SHOWN_VIEW, shownView)\n            bundle.putString(KEY_TIP, mTipView!!.text.toString())\n\n            // TODO It's a bad design\n            val app = context.applicationContext as EhApplication\n            if (mSavedDataId != IntIdGenerator.INVALID_ID) {\n                app.removeGlobalStuff(mSavedDataId)\n                mSavedDataId = IntIdGenerator.INVALID_ID\n            }\n            mSavedDataId = app.putGlobalStuff(mData)\n            bundle.putInt(KEY_DATA, mSavedDataId)\n            bundle.putInt(KEY_NEXT_ID, mIdGenerator.nextId())\n            bundle.putParcelable(KEY_PAGE_DIVIDER, mPageDivider)\n            bundle.putInt(KEY_START_PAGE, mStartPage)\n            bundle.putInt(KEY_END_PAGE, mEndPage)\n            bundle.putInt(KEY_PAGES, pages)\n            bundle.putString(KEY_PREV, mPrev)\n            bundle.putString(KEY_NEXT, mNext)\n            return bundle\n        }\n\n        @Suppress(\"UNCHECKED_CAST\")\n        open fun restoreInstanceState(state: Parcelable): Parcelable? = if (state is Bundle) {\n            mViewTransition!!.showView(state.getInt(KEY_SHOWN_VIEW), false)\n            mTipView!!.text = state.getString(KEY_TIP)\n            mSavedDataId = state.getInt(KEY_DATA)\n            var newData: ArrayList<E>? = null\n            val app = context.applicationContext as EhApplication\n            if (mSavedDataId != IntIdGenerator.INVALID_ID) {\n                newData = app.removeGlobalStuff(mSavedDataId) as ArrayList<E>?\n                mSavedDataId = IntIdGenerator.INVALID_ID\n                if (newData != null) {\n                    mData = newData\n                }\n            }\n            mIdGenerator.setNextId(state.getInt(KEY_NEXT_ID))\n            mPageDivider = state.getParcelableCompat(KEY_PAGE_DIVIDER)\n            mStartPage = state.getInt(KEY_START_PAGE)\n            mEndPage = state.getInt(KEY_END_PAGE)\n            pages = state.getInt(KEY_PAGES)\n            mPrev = state.getString(KEY_PREV)\n            mNext = state.getString(KEY_NEXT)\n            notifyDataSetChanged()\n            if (newData == null) {\n                mPageDivider!!.clear()\n                mStartPage = 0\n                mEndPage = 0\n                pages = 0\n                mPrev = null\n                mNext = null\n                firstRefresh()\n            }\n            state.getParcelableCompat(KEY_SUPER)\n        } else {\n            state\n        }\n\n        companion object {\n            const val TYPE_REFRESH = 0\n            const val TYPE_PRE_PAGE = 1\n            const val TYPE_PRE_PAGE_KEEP_POS = 2\n            const val TYPE_NEXT_PAGE = 3\n            const val TYPE_NEXT_PAGE_KEEP_POS = 4\n            const val TYPE_SOMEWHERE = 5\n            const val TYPE_REFRESH_PAGE = 6\n            private val TAG = ContentHelper::class.java.simpleName\n            private const val CHECK_DUPLICATE_RANGE = 50\n            private const val KEY_SUPER = \"super\"\n            private const val KEY_SHOWN_VIEW = \"shown_view\"\n            private const val KEY_TIP = \"tip\"\n            private const val KEY_DATA = \"data\"\n            private const val KEY_NEXT_ID = \"next_id\"\n            private const val KEY_PAGE_DIVIDER = \"page_divider\"\n            private const val KEY_START_PAGE = \"start_page\"\n            private const val KEY_END_PAGE = \"end_page\"\n            private const val KEY_PAGES = \"pages\"\n            private const val KEY_PREV = \"prev\"\n            private const val KEY_NEXT = \"next\"\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/CuteSpinner.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.widget.ArrayAdapter;\n\nimport androidx.appcompat.widget.AppCompatSpinner;\n\nimport com.hippo.ehviewer.R;\n\npublic class CuteSpinner extends AppCompatSpinner {\n    public CuteSpinner(Context context) {\n        super(context);\n        init(context, null, androidx.appcompat.R.attr.spinnerStyle);\n    }\n\n    public CuteSpinner(Context context, int mode) {\n        super(context, mode);\n        init(context, null, androidx.appcompat.R.attr.spinnerStyle);\n    }\n\n    public CuteSpinner(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs, androidx.appcompat.R.attr.spinnerStyle);\n    }\n\n    public CuteSpinner(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs, defStyleAttr);\n    }\n\n    public CuteSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {\n        super(context, attrs, defStyleAttr, mode);\n        init(context, attrs, defStyleAttr);\n    }\n\n    public CuteSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode, Resources.Theme popupTheme) {\n        super(context, attrs, defStyleAttr, mode, popupTheme);\n        init(context, attrs, defStyleAttr);\n    }\n\n    @SuppressLint(\"CustomViewStyleable\")\n    private void init(Context context, AttributeSet attrs, int defStyleAttr) {\n        //noinspection resource\n        TypedArray a = context.obtainStyledAttributes(attrs,\n                androidx.appcompat.R.styleable.Spinner, defStyleAttr, 0);\n        try {\n            final CharSequence[] entries = a.getTextArray(androidx.appcompat.R.styleable.Spinner_android_entries);\n            if (entries != null) {\n                final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<>(context,\n                        R.layout.item_cute_spinner_item, entries);\n                adapter.setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item);\n                setAdapter(adapter);\n            }\n        } finally {\n            a.recycle();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/DateUtils.java",
    "content": "/*\n * Copyright (C) 2014 Hippo Seven\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\npackage com.hippo.widget;\n\nfinal class DateUtils {\n    public static final char QUOTE = '\\'';\n    public static final char SECONDS = 's';\n\n    public static boolean hasSeconds(CharSequence inFormat) {\n        return hasDesignator(inFormat, SECONDS);\n    }\n\n    public static boolean hasDesignator(CharSequence inFormat, char designator) {\n        if (inFormat == null) return false;\n\n        final int length = inFormat.length();\n\n        int c;\n        int count;\n\n        for (int i = 0; i < length; i += count) {\n            count = 1;\n            c = inFormat.charAt(i);\n\n            if (c == QUOTE) {\n                count = skipQuotedText(inFormat, i, length);\n            } else if (c == designator) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    private static int skipQuotedText(CharSequence s, int i, int len) {\n        if (i + 1 < len && s.charAt(i + 1) == QUOTE) {\n            return 2;\n        }\n\n        int count = 1;\n        // skip leading quote\n        i++;\n\n        while (i < len) {\n            char c = s.charAt(i);\n\n            if (c == QUOTE) {\n                count++;\n                //  QUOTEQUOTE -> QUOTE\n                if (i + 1 < len && s.charAt(i + 1) == QUOTE) {\n                    i++;\n                } else {\n                    break;\n                }\n            } else {\n                i++;\n                count++;\n            }\n        }\n\n        return count;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/DrawerView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.widget.FrameLayout;\n\nimport com.hippo.yorozuya.LayoutUtils;\n\npublic class DrawerView extends FrameLayout {\n    private static final int DEFAULT_MAX_WIDTH = 280;\n\n    private static final int[] SIZE_ATTRS = new int[]{\n            android.R.attr.maxWidth\n    };\n\n    private int mMaxWidth;\n\n    public DrawerView(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public DrawerView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public DrawerView(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        //noinspection resource\n        TypedArray a = context.obtainStyledAttributes(attrs, SIZE_ATTRS);\n        try {\n            mMaxWidth = a.getDimensionPixelOffset(0, LayoutUtils.dp2pix(context, DEFAULT_MAX_WIDTH));\n        } finally {\n            a.recycle();\n        }\n    }\n\n    @Override\n    protected void onMeasure(int widthSpec, int heightSpec) {\n        switch (MeasureSpec.getMode(widthSpec)) {\n            case MeasureSpec.EXACTLY:\n                // Nothing to do\n                break;\n            case MeasureSpec.AT_MOST:\n                widthSpec = MeasureSpec.makeMeasureSpec(\n                        Math.min(MeasureSpec.getSize(widthSpec), mMaxWidth), MeasureSpec.EXACTLY);\n                break;\n            case MeasureSpec.UNSPECIFIED:\n                widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);\n                break;\n        }\n        // Let super sort out the height\n        super.onMeasure(widthSpec, heightSpec);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/FabLayout.kt",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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.hippo.widget\n\nimport android.animation.Animator\nimport android.content.Context\nimport android.os.Bundle\nimport android.os.Parcelable\nimport android.util.AttributeSet\nimport android.view.View\nimport android.view.ViewGroup\nimport android.view.animation.Interpolator\nimport androidx.core.view.isGone\nimport com.google.android.material.floatingactionbutton.FloatingActionButton\nimport com.hippo.ehviewer.R\nimport com.hippo.util.getParcelableCompat\nimport com.hippo.yorozuya.AnimationUtils\nimport com.hippo.yorozuya.SimpleAnimatorListener\n\nclass FabLayout @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : ViewGroup(context, attrs, defStyleAttr),\n    View.OnClickListener {\n    private var mFabSize = 0\n    private var mFabMiniSize = 0\n    private var mIntervalPrimary = 0\n    private var mIntervalSecondary = 0\n    private var mExpanded = true\n    private var mAutoCancel = true\n    private var mHidePrimaryFab = false\n    private var mMainFabCenterY = -1f\n    private var mOnExpandListener: OnExpandListener? = null\n    private var mOnClickFabListener: OnClickFabListener? = null\n\n    init {\n        isSoundEffectsEnabled = false\n        clipToPadding = false\n        mFabSize = context.resources.getDimensionPixelOffset(R.dimen.fab_size)\n        mFabMiniSize = context.resources.getDimensionPixelOffset(R.dimen.fab_min_size)\n        mIntervalPrimary =\n            context.resources.getDimensionPixelOffset(R.dimen.fab_layout_primary_margin)\n        mIntervalSecondary =\n            context.resources.getDimensionPixelOffset(R.dimen.fab_layout_secondary_margin)\n    }\n\n    override fun addView(child: View, index: Int, params: LayoutParams) {\n        check(child is FloatingActionButton) { \"FloatingActionBarLayout should only \" + \"contain FloatingActionButton, but try to add \" + child.javaClass.name }\n        super.addView(child, index, params)\n    }\n\n    val primaryFab: FloatingActionButton?\n        get() = getChildAt(childCount - 1) as? FloatingActionButton\n\n    private val secondaryFabCount: Int\n        get() = 0.coerceAtLeast(childCount - 1)\n\n    fun getSecondaryFabAt(index: Int): FloatingActionButton? = if (index < 0 || index >= secondaryFabCount) {\n        null\n    } else {\n        getChildAt(index) as FloatingActionButton\n    }\n\n    fun setSecondaryFabVisibilityAt(index: Int, visible: Boolean) {\n        getSecondaryFabAt(index)?.run {\n            if (visible && isGone) {\n                animate().cancel()\n                visibility = if (mExpanded) VISIBLE else INVISIBLE\n            } else if (!visible && visibility != GONE) {\n                animate().cancel()\n                visibility = GONE\n            }\n        }\n    }\n\n    private fun getChildMeasureSpec(parentMeasureSpec: Int): Int {\n        val parentMode = MeasureSpec.getMode(parentMeasureSpec)\n        val parentSize = MeasureSpec.getSize(parentMeasureSpec)\n        return MeasureSpec.makeMeasureSpec(\n            parentSize,\n            if (parentMode == MeasureSpec.UNSPECIFIED) MeasureSpec.UNSPECIFIED else MeasureSpec.AT_MOST,\n        )\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec)\n        val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec)\n        measureChildren(childWidthMeasureSpec, childHeightMeasureSpec)\n        var maxWidth = 0\n        var maxHeight = 0\n        val count = childCount\n        for (i in 0 until count) {\n            val child = getChildAt(i)\n            if (child.isGone) {\n                continue\n            }\n            maxWidth = maxWidth.coerceAtLeast(child.measuredWidth)\n            maxHeight += child.measuredHeight\n        }\n        maxWidth += paddingLeft + paddingRight\n        maxHeight += paddingTop + paddingBottom\n        maxHeight = maxHeight.coerceAtLeast(suggestedMinimumHeight)\n        maxWidth = maxWidth.coerceAtLeast(suggestedMinimumWidth)\n        setMeasuredDimension(\n            resolveSizeAndState(maxWidth, widthMeasureSpec, 0),\n            resolveSizeAndState(maxHeight, heightMeasureSpec, 0),\n        )\n    }\n\n    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {\n        var centerX = 0\n        var bottom = measuredHeight - paddingBottom\n        val count = childCount\n        var i = count\n        while (--i >= 0) {\n            val child = getChildAt(i)\n            if (child.isGone) {\n                continue\n            }\n            val childWidth = child.measuredWidth\n            val childHeight = child.measuredHeight\n            var layoutBottom: Int\n            var layoutRight: Int\n            if (i == count - 1) {\n                layoutBottom = bottom + (childHeight - mFabSize) / 2\n                layoutRight = measuredWidth - paddingRight + (childWidth - mFabSize) / 2\n                bottom -= mFabSize + mIntervalPrimary\n                centerX = layoutRight - childWidth / 2\n                mMainFabCenterY = layoutBottom - childHeight / 2f\n            } else {\n                layoutBottom = bottom + (childHeight - mFabMiniSize) / 2\n                layoutRight = centerX + childWidth / 2\n                bottom -= mFabMiniSize + mIntervalSecondary\n            }\n            child.layout(\n                layoutRight - childWidth,\n                layoutBottom - childHeight,\n                layoutRight,\n                layoutBottom,\n            )\n        }\n    }\n\n    fun setOnExpandListener(listener: OnExpandListener?) {\n        mOnExpandListener = listener\n    }\n\n    fun setOnClickFabListener(listener: OnClickFabListener?) {\n        mOnClickFabListener = listener\n        if (listener != null) {\n            var i = 0\n            val n = childCount\n            while (i < n) {\n                getChildAt(i).setOnClickListener(this)\n                i++\n            }\n        } else {\n            var i = 0\n            val n = childCount\n            while (i < n) {\n                getChildAt(i).isClickable = false\n                i++\n            }\n        }\n    }\n\n    fun setHidePrimaryFab(hidePrimaryFab: Boolean) {\n        if (mHidePrimaryFab != hidePrimaryFab) {\n            mHidePrimaryFab = hidePrimaryFab\n            val expanded = mExpanded\n            val count = childCount\n            if (!expanded && count > 0) {\n                getChildAt(count - 1).visibility = if (hidePrimaryFab) INVISIBLE else VISIBLE\n            }\n        }\n    }\n\n    fun setAutoCancel(autoCancel: Boolean) {\n        if (mAutoCancel != autoCancel) {\n            mAutoCancel = autoCancel\n            if (mExpanded) {\n                if (autoCancel) {\n                    setOnClickListener(this)\n                } else {\n                    isClickable = false\n                }\n            }\n        }\n    }\n\n    fun toggle() {\n        isExpanded = !mExpanded\n    }\n\n    var isExpanded: Boolean\n        get() = mExpanded\n        set(expanded) {\n            setExpanded(expanded, true)\n        }\n\n    fun setExpanded(expanded: Boolean, animation: Boolean) {\n        if (mExpanded != expanded) {\n            mExpanded = expanded\n            if (mAutoCancel) {\n                if (expanded) {\n                    setOnClickListener(this)\n                } else {\n                    isClickable = false\n                }\n            }\n            val count = childCount\n            if (count > 0) {\n                if (mMainFabCenterY == -1f || !animation) {\n                    // It is before first onLayout\n                    val checkCount = if (mHidePrimaryFab) count else count - 1\n                    for (i in 0 until checkCount) {\n                        val child = getChildAt(i)\n                        if (child.isGone) {\n                            continue\n                        }\n                        child.visibility = if (expanded) VISIBLE else INVISIBLE\n                        if (expanded) {\n                            child.alpha = 1f\n                        }\n                    }\n                } else {\n                    if (mHidePrimaryFab) {\n                        setPrimaryFabAnimation(getChildAt(count - 1), expanded, !expanded)\n                    }\n                    for (i in 0 until count - 1) {\n                        val child = getChildAt(i)\n                        if (child.isGone) {\n                            continue\n                        }\n                        setSecondaryFabAnimation(child, expanded, expanded)\n                    }\n                }\n            }\n            mOnExpandListener?.onExpand(expanded)\n        }\n    }\n\n    private fun setPrimaryFabAnimation(child: View, expanded: Boolean, delay: Boolean) {\n        val startRotation: Float\n        val endRotation: Float\n        val startScale: Float\n        val endScale: Float\n        val interpolator: Interpolator\n        if (expanded) {\n            startRotation = -45.0f\n            endRotation = 0.0f\n            startScale = 0.0f\n            endScale = 1.0f\n            interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR\n        } else {\n            startRotation = 0.0f\n            endRotation = 0.0f\n            startScale = 1.0f\n            endScale = 0.0f\n            interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR\n        }\n        child.scaleX = startScale\n        child.scaleY = startScale\n        child.rotation = startRotation\n        child.animate()\n            .scaleX(endScale)\n            .scaleY(endScale)\n            .rotation(endRotation)\n            .setStartDelay(if (delay) ANIMATE_TIME else 0L)\n            .setDuration(ANIMATE_TIME)\n            .setInterpolator(interpolator)\n            .setListener(object : SimpleAnimatorListener() {\n                override fun onAnimationStart(animation: Animator) {\n                    if (expanded) {\n                        child.visibility = VISIBLE\n                    }\n                }\n\n                override fun onAnimationEnd(animation: Animator) {\n                    if (!expanded) {\n                        child.visibility = INVISIBLE\n                    }\n                }\n            }).start()\n    }\n\n    private fun setSecondaryFabAnimation(child: View, expanded: Boolean, delay: Boolean) {\n        val startTranslationY: Float\n        val endTranslationY: Float\n        val startAlpha: Float\n        val endAlpha: Float\n        val interpolator: Interpolator\n        if (expanded) {\n            startTranslationY = mMainFabCenterY - child.top - child.height / 2\n            endTranslationY = 0f\n            startAlpha = 0f\n            endAlpha = 1f\n            interpolator = AnimationUtils.FAST_SLOW_INTERPOLATOR\n        } else {\n            startTranslationY = 0f\n            endTranslationY = mMainFabCenterY - child.top - child.height / 2\n            startAlpha = 1f\n            endAlpha = 0f\n            interpolator = AnimationUtils.SLOW_FAST_INTERPOLATOR\n        }\n        child.alpha = startAlpha\n        child.translationY = startTranslationY\n        child.animate()\n            .alpha(endAlpha)\n            .translationY(endTranslationY)\n            .setStartDelay(if (delay) ANIMATE_TIME else 0L)\n            .setDuration(ANIMATE_TIME)\n            .setInterpolator(interpolator)\n            .setListener(object : SimpleAnimatorListener() {\n                override fun onAnimationStart(animation: Animator) {\n                    if (expanded) {\n                        child.visibility = VISIBLE\n                    }\n                }\n\n                override fun onAnimationEnd(animation: Animator) {\n                    if (!expanded) {\n                        child.visibility = INVISIBLE\n                    }\n                }\n            }).start()\n    }\n\n    override fun onClick(v: View) {\n        if (this === v) {\n            isExpanded = false\n        } else {\n            mOnClickFabListener?.let {\n                val position = indexOfChild(v)\n                if (position == childCount - 1) {\n                    it.onClickPrimaryFab(this, v as FloatingActionButton)\n                } else if (position >= 0 && mExpanded) {\n                    it.onClickSecondaryFab(this, v as FloatingActionButton, position)\n                }\n            }\n        }\n    }\n\n    override fun dispatchSetPressed(pressed: Boolean) {\n        // Don't dispatch it to children\n    }\n\n    override fun onSaveInstanceState(): Parcelable {\n        val state = Bundle()\n        state.putParcelable(STATE_KEY_SUPER, super.onSaveInstanceState())\n        state.putBoolean(STATE_KEY_AUTO_CANCEL, mAutoCancel)\n        state.putBoolean(STATE_KEY_EXPANDED, mExpanded)\n        return state\n    }\n\n    override fun onRestoreInstanceState(state: Parcelable) {\n        if (state is Bundle) {\n            super.onRestoreInstanceState(state.getParcelableCompat(STATE_KEY_SUPER))\n            setAutoCancel(state.getBoolean(STATE_KEY_AUTO_CANCEL))\n            setExpanded(state.getBoolean(STATE_KEY_EXPANDED), false)\n        }\n    }\n\n    interface OnExpandListener {\n        fun onExpand(expanded: Boolean)\n    }\n\n    interface OnClickFabListener {\n        fun onClickPrimaryFab(view: FabLayout, fab: FloatingActionButton)\n        fun onClickSecondaryFab(view: FabLayout, fab: FloatingActionButton, position: Int)\n    }\n\n    companion object {\n        private const val ANIMATE_TIME = 300L\n        private const val STATE_KEY_SUPER = \"super\"\n        private const val STATE_KEY_AUTO_CANCEL = \"auto_cancel\"\n        private const val STATE_KEY_EXPANDED = \"expanded\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/FixedAspectImageView.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.widget\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.util.AttributeSet\nimport androidx.core.content.withStyledAttributes\nimport com.google.android.material.imageview.ShapeableImageView\nimport com.hippo.ehviewer.R\nimport com.hippo.yorozuya.MathUtils\nimport kotlin.math.abs\nimport kotlin.math.max\nimport kotlin.math.min\n\nopen class FixedAspectImageView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0,\n) : ShapeableImageView(\n    context,\n    attrs,\n    defStyle,\n) {\n    private var mMinWidth = 0\n    private var mMinHeight = 0\n    private var mMaxWidth = Int.MAX_VALUE\n    private var mMaxHeight = Int.MAX_VALUE\n    private var mAdjustViewBounds = false\n\n    // width / height\n    private var mAspect = -1f\n\n    @SuppressLint(\"ResourceType\")\n    private fun init(context: Context, attrs: AttributeSet?, defStyle: Int) {\n        // Make sure we get value from xml\n        context.withStyledAttributes(attrs, MIN_ATTRS, defStyle, 0) {\n            setMinimumWidth(getDimensionPixelSize(0, 0))\n            setMinimumHeight(getDimensionPixelSize(1, 0))\n        }\n        context.withStyledAttributes(attrs, ATTRS, defStyle, 0) {\n            setAdjustViewBounds(getBoolean(0, false))\n            setMaxWidth(getDimensionPixelSize(1, Int.MAX_VALUE))\n            setMaxHeight(getDimensionPixelSize(2, Int.MAX_VALUE))\n        }\n        context.withStyledAttributes(\n            attrs,\n            R.styleable.FixedAspectImageView,\n            defStyle,\n            0,\n        ) {\n            aspect = getFloat(R.styleable.FixedAspectImageView_aspect, -1f)\n        }\n    }\n\n    override fun setMinimumWidth(minWidth: Int) {\n        super.setMinimumWidth(minWidth)\n        mMinWidth = minWidth\n    }\n\n    override fun setMinimumHeight(minHeight: Int) {\n        super.setMinimumHeight(minHeight)\n        mMinHeight = minHeight\n    }\n\n    override fun setMaxWidth(maxWidth: Int) {\n        super.setMaxWidth(maxWidth)\n        mMaxWidth = maxWidth\n    }\n\n    override fun setMaxHeight(maxHeight: Int) {\n        super.setMaxHeight(maxHeight)\n        mMaxHeight = maxHeight\n    }\n\n    override fun setAdjustViewBounds(adjustViewBounds: Boolean) {\n        super.setAdjustViewBounds(adjustViewBounds)\n        mAdjustViewBounds = adjustViewBounds\n    }\n\n    /**\n     * Enable aspect will set AdjustViewBounds true.\n     * Any negative float to disable it,\n     * disable Aspect will not disable AdjustViewBounds.\n     *\n     * @param aspect width/height\n     */\n    var aspect: Float\n        get() = mAspect\n        set(aspect) {\n            mAspect = if (aspect > 0) {\n                aspect\n            } else {\n                -1f\n            }\n            requestLayout()\n        }\n\n    private fun resolveAdjustedSize(\n        desiredSize: Int,\n        minSize: Int,\n        maxSize: Int,\n        measureSpec: Int,\n    ): Int {\n        var result = desiredSize\n        val specMode = MeasureSpec.getMode(measureSpec)\n        val specSize = MeasureSpec.getSize(measureSpec)\n        when (specMode) {\n            MeasureSpec.UNSPECIFIED ->\n                // Parent says we can be as big as we want. Just don't be smaller\n                // than min size, and don't be larger than max size.\n                result = MathUtils.clamp(desiredSize, minSize, maxSize)\n            MeasureSpec.AT_MOST ->\n                // Parent says we can be as big as we want, up to specSize.\n                // Don't be larger than specSize, and don't be smaller\n                // than min size, and don't be larger than max size.\n                result = min(\n                    MathUtils.clamp(desiredSize, minSize, maxSize).toDouble(),\n                    specSize.toDouble(),\n                ).toInt()\n            MeasureSpec.EXACTLY ->\n                // No choice. Do what we are told.\n                result = specSize\n        }\n        return result\n    }\n\n    private fun isSizeAcceptable(size: Int, minSize: Int, maxSize: Int, measureSpec: Int): Boolean {\n        val specMode = MeasureSpec.getMode(measureSpec)\n        val specSize = MeasureSpec.getSize(measureSpec)\n        return when (specMode) {\n            MeasureSpec.UNSPECIFIED ->\n                // Parent says we can be as big as we want. Just don't be smaller\n                // than min size, and don't be larger than max size.\n                size in minSize..maxSize\n            MeasureSpec.AT_MOST ->\n                // Parent says we can be as big as we want, up to specSize.\n                // Don't be larger than specSize, and don't be smaller\n                // than min size, and don't be larger than max size.\n                size in minSize..specSize && size <= maxSize\n            MeasureSpec.EXACTLY ->\n                // No choice.\n                size == specSize\n            else -> // WTF? Return true to make you happy. (´・ω・`)\n                true\n        }\n    }\n\n    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {\n        var w: Int\n        var h: Int\n\n        // Desired aspect ratio of the view's contents (not including padding)\n        var desiredAspect = 0.0f\n\n        // We are allowed to change the view's width\n        var resizeWidth = false\n\n        // We are allowed to change the view's height\n        var resizeHeight = false\n        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)\n        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)\n        val drawable = getDrawable()\n        if (drawable == null) {\n            // If no drawable, its intrinsic size is 0.\n            h = 0\n            w = 0\n\n            // Aspect is forced set.\n            if (mAspect > 0.0f) {\n                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY\n                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY\n                desiredAspect = mAspect\n            }\n        } else {\n            w = drawable.intrinsicWidth\n            h = drawable.intrinsicHeight\n            if (w <= 0) w = 1\n            if (h <= 0) h = 1\n            if (mAdjustViewBounds) {\n                // We are supposed to adjust view bounds to match the aspect\n                // ratio of our drawable. See if that is possible.\n                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY\n                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY\n                desiredAspect = w.toFloat() / h.toFloat()\n            } else if (mAspect > 0.0f) {\n                // Aspect is forced set.\n                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY\n                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY\n                desiredAspect = mAspect\n            }\n        }\n        val pLeft = paddingLeft\n        val pRight = paddingRight\n        val pTop = paddingTop\n        val pBottom = paddingBottom\n        var widthSize: Int\n        var heightSize: Int\n        if (resizeWidth || resizeHeight) {\n            // If we get here, it means we want to resize to match the\n            // drawables aspect ratio, and we have the freedom to change at\n            // least one dimension.\n\n            // Get the max possible width given our constraints\n            widthSize =\n                resolveAdjustedSize(w + pLeft + pRight, mMinWidth, mMaxWidth, widthMeasureSpec)\n\n            // Get the max possible height given our constraints\n            heightSize =\n                resolveAdjustedSize(h + pTop + pBottom, mMinHeight, mMaxHeight, heightMeasureSpec)\n            if (desiredAspect != 0.0f) {\n                // See what our actual aspect ratio is\n                val actualAspect = (widthSize - pLeft - pRight).toFloat() /\n                    (heightSize - pTop - pBottom)\n                if (abs((actualAspect - desiredAspect).toDouble()) > 0.0000001) {\n                    var done = false\n\n                    // Try adjusting width to be proportional to height\n                    if (resizeWidth) {\n                        val newWidth = (desiredAspect * (heightSize - pTop - pBottom)).toInt() +\n                            pLeft + pRight\n\n                        // Allow the width to outgrow its original estimate if height is fixed.\n                        // if (!resizeHeight) {\n                        // widthSize = resolveAdjustedSize(newWidth, mMinWidth, mMaxWidth, widthMeasureSpec);\n                        // }\n                        if (isSizeAcceptable(newWidth, mMinWidth, mMaxWidth, widthMeasureSpec)) {\n                            widthSize = newWidth\n                            done = true\n                        }\n                    }\n\n                    // Try adjusting height to be proportional to width\n                    if (!done && resizeHeight) {\n                        val newHeight = ((widthSize - pLeft - pRight) / desiredAspect).toInt() +\n                            pTop + pBottom\n\n                        // Allow the height to outgrow its original estimate if width is fixed.\n                        if (!resizeWidth) {\n                            heightSize = resolveAdjustedSize(\n                                newHeight,\n                                mMinHeight,\n                                mMaxHeight,\n                                heightMeasureSpec,\n                            )\n                        }\n                        if (isSizeAcceptable(\n                                newHeight,\n                                mMinHeight,\n                                mMaxHeight,\n                                heightMeasureSpec,\n                            )\n                        ) {\n                            heightSize = newHeight\n                        }\n                    }\n                }\n            }\n        } else {\n            // We are either don't want to preserve the drawables aspect ratio,\n            // or we are not allowed to change view dimensions. Just measure in\n            // the normal way.\n            w += pLeft + pRight\n            h += pTop + pBottom\n            w = max(w.toDouble(), suggestedMinimumWidth.toDouble()).toInt()\n            h = max(h.toDouble(), suggestedMinimumHeight.toDouble()).toInt()\n            widthSize = resolveSizeAndState(w, widthMeasureSpec, 0)\n            heightSize = resolveSizeAndState(h, heightMeasureSpec, 0)\n        }\n        setMeasuredDimension(widthSize, heightSize)\n    }\n\n    companion object {\n        private val MIN_ATTRS = intArrayOf(\n            android.R.attr.minWidth,\n            android.R.attr.minHeight,\n        )\n        private val ATTRS = intArrayOf(\n            android.R.attr.adjustViewBounds,\n            android.R.attr.maxWidth,\n            android.R.attr.maxHeight,\n        )\n    }\n\n    init {\n        init(context, attrs, defStyle)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout.kt",
    "content": "/*\n * Copyright 2022 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.widget\n\nimport android.content.Context\nimport android.util.AttributeSet\nimport android.view.WindowInsets\nimport com.drakeet.drawer.FullDraggableContainer\n\nclass IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyle: Int = 0,\n) : FullDraggableContainer(\n    context,\n    attrs,\n    defStyle,\n) {\n    override fun onApplyWindowInsets(insets: WindowInsets?): WindowInsets? = insets\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/IndicatingListView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.util.AttributeSet;\nimport android.widget.ListView;\n\nimport androidx.annotation.NonNull;\n\nimport com.hippo.ehviewer.R;\n\npublic class IndicatingListView extends ListView {\n    private final Paint mPaint = new Paint();\n    private final Rect mTemp = new Rect();\n    private int mIndicatorHeight;\n\n    public IndicatingListView(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public IndicatingListView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public IndicatingListView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    @SuppressLint(\"CustomViewStyleable\")\n    private void init(Context context, AttributeSet attrs) {\n        //noinspection resource\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Indicating);\n        try {\n            mIndicatorHeight = a.getDimensionPixelOffset(R.styleable.Indicating_indicatorHeight, 1);\n            mPaint.setColor(a.getColor(R.styleable.Indicating_indicatorColor, Color.BLACK));\n            mPaint.setStyle(Paint.Style.FILL);\n        } finally {\n            a.recycle();\n        }\n    }\n\n    private void fillTopIndicatorDrawRect() {\n        mTemp.set(0, 0, getWidth(), mIndicatorHeight);\n    }\n\n    private void fillBottomIndicatorDrawRect() {\n        mTemp.set(0, getHeight() - mIndicatorHeight, getWidth(), getHeight());\n    }\n\n    private boolean needShowTopIndicator() {\n        return canScrollVertically(-1);\n    }\n\n    private boolean needShowBottomIndicator() {\n        return canScrollVertically(1);\n    }\n\n    @Override\n    public void draw(@NonNull Canvas canvas) {\n        super.draw(canvas);\n\n        final int restoreCount = canvas.save();\n        canvas.translate(getScrollX(), getScrollY());\n\n        // Draw top indicator\n        if (needShowTopIndicator()) {\n            fillTopIndicatorDrawRect();\n            canvas.drawRect(mTemp, mPaint);\n        }\n        // Draw bottom indicator\n        if (needShowBottomIndicator()) {\n            fillBottomIndicatorDrawRect();\n            canvas.drawRect(mTemp, mPaint);\n        }\n\n        canvas.restoreToCount(restoreCount);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/LinkifyTextView.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.text.Layout;\nimport android.text.Spanned;\nimport android.text.style.ClickableSpan;\nimport android.util.AttributeSet;\nimport android.view.MotionEvent;\n\nimport androidx.annotation.NonNull;\n\npublic class LinkifyTextView extends ObservedTextView {\n    private ClickableSpan mCurrentSpan;\n\n    public LinkifyTextView(Context context) {\n        super(context);\n    }\n\n    public LinkifyTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public LinkifyTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    public ClickableSpan getCurrentSpan() {\n        return mCurrentSpan;\n    }\n\n    public void clearCurrentSpan() {\n        mCurrentSpan = null;\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(@NonNull MotionEvent event) {\n        // Let the parent or grandparent of TextView to handles click aciton.\n        // Otherwise click effect like ripple will not work, and if touch area\n        // do not contain a url, the TextView will still get MotionEvent.\n        // onTouchEven must be called with MotionEvent.ACTION_DOWN for each touch\n        // action on it, so we analyze touched url here.\n        if (event.getAction() == MotionEvent.ACTION_DOWN) {\n            mCurrentSpan = null;\n\n            if (getText() instanceof Spanned) {\n                // Get this code from android.text.method.LinkMovementMethod.\n                // Work fine !\n                int x = (int) event.getX();\n                int y = (int) event.getY();\n\n                x -= getTotalPaddingLeft();\n                y -= getTotalPaddingTop();\n\n                x += getScrollX();\n                y += getScrollY();\n\n                Layout layout = getLayout();\n                if (null != layout) {\n                    int line = layout.getLineForVertical(y);\n                    int off = layout.getOffsetForHorizontal(line, x);\n\n                    ClickableSpan[] spans = ((Spanned) getText()).getSpans(off, off, ClickableSpan.class);\n\n                    if (spans.length > 0) {\n                        mCurrentSpan = spans[0];\n                    }\n                }\n            }\n        }\n\n        return super.onTouchEvent(event);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/LoadImageView.kt",
    "content": "/*\n * Copyright 2015-2016 Hippo Seven\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.hippo.widget\n\nimport android.content.Context\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport android.view.View\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.IntDef\nimport androidx.core.content.ContextCompat\nimport coil3.load\nimport coil3.request.allowHardware\nimport coil3.request.crossfade\nimport coil3.size.Size\nimport com.hippo.drawable.PreciselyClipDrawable\nimport com.hippo.ehviewer.R\n\nopen class LoadImageView @JvmOverloads constructor(\n    context: Context,\n    attrs: AttributeSet? = null,\n    defStyleAttr: Int = 0,\n) : FixedAspectImageView(context, attrs, defStyleAttr),\n    View.OnClickListener,\n    View.OnLongClickListener {\n    private var mOffsetX = Int.MIN_VALUE\n    private var mOffsetY = Int.MIN_VALUE\n    private var mClipWidth = Int.MIN_VALUE\n    private var mClipHeight = Int.MIN_VALUE\n    private var mKey: String? = null\n    private var mUrl: String? = null\n    private var mCrossfade = true\n    private var mHardware = true\n\n    @RetryType\n    private val mRetryType: Int =\n        context.obtainStyledAttributes(attrs, R.styleable.LoadImageView, defStyleAttr, 0).run {\n            getInt(R.styleable.LoadImageView_retryType, 0).also { recycle() }\n        }\n\n    private fun setRetry(canRetry: Boolean) {\n        when (mRetryType) {\n            RETRY_TYPE_CLICK -> {\n                setOnClickListener(if (canRetry) this else null)\n                isClickable = canRetry\n            }\n            RETRY_TYPE_LONG_CLICK -> {\n                setOnLongClickListener(if (canRetry) this else null)\n                isLongClickable = canRetry\n            }\n            RETRY_TYPE_NONE -> {}\n        }\n    }\n\n    fun setClip(offsetX: Int, offsetY: Int, clipWidth: Int, clipHeight: Int) {\n        mOffsetX = offsetX\n        mOffsetY = offsetY\n        mClipWidth = clipWidth\n        mClipHeight = clipHeight\n    }\n\n    fun resetClip() {\n        mOffsetX = Int.MIN_VALUE\n        mOffsetY = Int.MIN_VALUE\n        mClipWidth = Int.MIN_VALUE\n        mClipHeight = Int.MIN_VALUE\n    }\n\n    fun load(\n        key: String,\n        url: String,\n        crossfade: Boolean = true,\n        hardware: Boolean = true,\n    ) {\n        mKey = key\n        mUrl = url\n        mCrossfade = crossfade\n        mHardware = hardware\n        load(url) {\n            // https://coil-kt.github.io/coil/recipes/#shared-element-transitions\n            allowHardware(hardware)\n            placeholderMemoryCacheKey(key)\n            memoryCacheKey(key)\n            diskCacheKey(key)\n            size(Size.ORIGINAL)\n            if (!crossfade) crossfade(false)\n            listener(\n                { setRetry(false) },\n                { setRetry(true) },\n                { _, _ ->\n                    val errorDrawable = ContextCompat.getDrawable(context, R.drawable.image_failed)\n                    onPreSetImageDrawable(errorDrawable, true)\n                    super.setImageDrawable(errorDrawable)\n                    setRetry(true)\n                },\n                { _, _ -> setRetry(false) },\n            )\n        }\n    }\n\n    fun load(@DrawableRes id: Int) {\n        onPreSetImageResource(id, true)\n        setImageResource(id)\n    }\n\n    private fun reload() {\n        mKey?.let { this.load(it, mUrl!!, mCrossfade, mHardware) }\n    }\n\n    override fun setImageDrawable(drawable: Drawable?) {\n        var newDrawable = drawable\n        if (newDrawable != null) {\n            if (Int.MIN_VALUE != mOffsetX) {\n                newDrawable =\n                    PreciselyClipDrawable(newDrawable, mOffsetX, mOffsetY, mClipWidth, mClipHeight)\n            }\n            onPreSetImageDrawable(newDrawable, true)\n        }\n        super.setImageDrawable(newDrawable)\n    }\n\n    override fun getDrawable(): Drawable? {\n        var newDrawable = super.getDrawable()\n        if (newDrawable is PreciselyClipDrawable) {\n            newDrawable = newDrawable.drawable\n        }\n        return newDrawable\n    }\n\n    override fun onClick(v: View) {\n        reload()\n    }\n\n    override fun onLongClick(v: View): Boolean {\n        reload()\n        return true\n    }\n\n    open fun onPreSetImageDrawable(drawable: Drawable?, isTarget: Boolean) {}\n\n    open fun onPreSetImageResource(resId: Int, isTarget: Boolean) {}\n\n    @IntDef(RETRY_TYPE_NONE, RETRY_TYPE_CLICK, RETRY_TYPE_LONG_CLICK)\n    @Retention(AnnotationRetention.SOURCE)\n    private annotation class RetryType\n\n    companion object {\n        const val RETRY_TYPE_NONE = 0\n        const val RETRY_TYPE_CLICK = 1\n        const val RETRY_TYPE_LONG_CLICK = 2\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/MaxSizeContainer.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport com.hippo.ehviewer.R;\nimport com.hippo.yorozuya.AssertUtils;\n\npublic class MaxSizeContainer extends ViewGroup {\n    private int mMaxWidth;\n    private int mMaxHeight;\n\n    public MaxSizeContainer(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public MaxSizeContainer(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        //noinspection resource\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MaxSizeContainer);\n        try {\n            mMaxWidth = a.getDimensionPixelOffset(R.styleable.MaxSizeContainer_maxWidth, -1);\n            mMaxHeight = a.getDimensionPixelOffset(R.styleable.MaxSizeContainer_maxHeight, -1);\n        } finally {\n            a.recycle();\n        }\n    }\n\n    private int getMeasureSpec(int measureSpec, int max) {\n        if (max < 0) {\n            return measureSpec;\n        }\n\n        int size = MeasureSpec.getSize(measureSpec);\n        int mode = MeasureSpec.getMode(measureSpec);\n\n        switch (mode) {\n            case MeasureSpec.AT_MOST -> size = Math.min(size, max);\n            case MeasureSpec.UNSPECIFIED -> {\n                size = max;\n                mode = MeasureSpec.AT_MOST;\n            }\n            case MeasureSpec.EXACTLY -> {}\n        }\n        return MeasureSpec.makeMeasureSpec(size, mode);\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        AssertUtils.assertEquals(\"getChildCount() must be 1\", 1, getChildCount());\n\n        View child = getChildAt(0);\n        if (child.getVisibility() != GONE) {\n            child.measure(getMeasureSpec(widthMeasureSpec, mMaxWidth),\n                    getMeasureSpec(heightMeasureSpec, mMaxHeight));\n            setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());\n        } else {\n            setMeasuredDimension(0, 0);\n        }\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n        AssertUtils.assertEquals(\"getChildCount() must be 1\", 1, getChildCount());\n\n        View child = getChildAt(0);\n        if (child.getVisibility() != GONE) {\n            child.layout(0, 0, r - l, b - t);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/ObservedTextView.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\npublic class ObservedTextView extends AppCompatTextView {\n    private OnWindowAttachListener mOnWindowAttachListener;\n\n    public ObservedTextView(Context context) {\n        super(context);\n    }\n\n    public ObservedTextView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public ObservedTextView(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n\n        if (mOnWindowAttachListener != null) {\n            mOnWindowAttachListener.onAttachedToWindow();\n        }\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n\n        if (mOnWindowAttachListener != null) {\n            mOnWindowAttachListener.onDetachedFromWindow();\n        }\n    }\n\n    public void setOnWindowAttachListener(OnWindowAttachListener onWindowAttachListener) {\n        mOnWindowAttachListener = onWindowAttachListener;\n    }\n\n    public interface OnWindowAttachListener {\n        void onAttachedToWindow();\n\n        void onDetachedFromWindow();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/RadioGridGroup.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.CompoundButton;\nimport android.widget.RadioButton;\n\nimport androidx.annotation.IdRes;\nimport androidx.annotation.NonNull;\n\nimport com.hippo.yorozuya.ViewUtils;\n\npublic class RadioGridGroup extends SimpleGridLayout {\n    private static final int[] RADIO_ATTRS = new int[]{\n            android.R.attr.checkedButton\n    };\n\n    // holds the checked id; the selection is empty by default\n    private int mCheckedId = -1;\n    // tracks children radio buttons checked state\n    private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;\n    // when true, mOnCheckedChangeListener discards events\n    private boolean mProtectFromCheckedChange = false;\n    private OnCheckedChangeListener mOnCheckedChangeListener;\n    private PassThroughHierarchyChangeListener mPassThroughListener;\n\n    public RadioGridGroup(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public RadioGridGroup(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        //noinspection resource\n        final TypedArray a = context.obtainStyledAttributes(attrs, RADIO_ATTRS);\n        try {\n            int value = a.getResourceId(0, View.NO_ID);\n            if (value != View.NO_ID) {\n                mCheckedId = value;\n            }\n        } finally {\n            a.recycle();\n        }\n\n        mChildOnCheckedChangeListener = new CheckedStateTracker();\n        mPassThroughListener = new PassThroughHierarchyChangeListener();\n        super.setOnHierarchyChangeListener(mPassThroughListener);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {\n        // the user listener is delegated to our pass-through listener\n        mPassThroughListener.mOnHierarchyChangeListener = listener;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    protected void onFinishInflate() {\n        super.onFinishInflate();\n\n        // checks the appropriate radio button as requested in the XML file\n        if (mCheckedId != -1) {\n            mProtectFromCheckedChange = true;\n            setCheckedStateForView(mCheckedId, true);\n            mProtectFromCheckedChange = false;\n            setCheckedId(mCheckedId);\n        }\n    }\n\n    @Override\n    public void addView(View child, int index, ViewGroup.LayoutParams params) {\n        if (child instanceof final RadioButton button) {\n            if (button.isChecked()) {\n                mProtectFromCheckedChange = true;\n                if (mCheckedId != -1) {\n                    setCheckedStateForView(mCheckedId, false);\n                }\n                mProtectFromCheckedChange = false;\n                setCheckedId(button.getId());\n            }\n        }\n\n        super.addView(child, index, params);\n    }\n\n    /**\n     * <p>Sets the selection to the radio button whose identifier is passed in\n     * parameter. Using -1 as the selection identifier clears the selection;\n     * such an operation is equivalent to invoking {@link #clearCheck()}.</p>\n     *\n     * @param id the unique id of the radio button to select in this group\n     * @see #getCheckedRadioButtonId()\n     * @see #clearCheck()\n     */\n    public void check(@IdRes int id) {\n        // don't even bother\n        if (id != -1 && (id == mCheckedId)) {\n            return;\n        }\n\n        if (mCheckedId != -1) {\n            setCheckedStateForView(mCheckedId, false);\n        }\n\n        if (id != -1) {\n            setCheckedStateForView(id, true);\n        }\n\n        setCheckedId(id);\n    }\n\n    private void setCheckedId(@IdRes int id) {\n        mCheckedId = id;\n        if (mOnCheckedChangeListener != null) {\n            mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);\n        }\n    }\n\n    private void setCheckedStateForView(int viewId, boolean checked) {\n        View checkedView = findViewById(viewId);\n        if (checkedView instanceof RadioButton) {\n            ((RadioButton) checkedView).setChecked(checked);\n        }\n    }\n\n    /**\n     * <p>Returns the identifier of the selected radio button in this group.\n     * Upon empty selection, the returned value is -1.</p>\n     *\n     * @return the unique id of the selected radio button in this group\n     * @see #check(int)\n     * @see #clearCheck()\n     */\n    @IdRes\n    public int getCheckedRadioButtonId() {\n        return mCheckedId;\n    }\n\n    /**\n     * <p>Clears the selection. When the selection is cleared, no radio button\n     * in this group is selected and {@link #getCheckedRadioButtonId()} returns\n     * null.</p>\n     *\n     * @see #check(int)\n     * @see #getCheckedRadioButtonId()\n     */\n    public void clearCheck() {\n        check(-1);\n    }\n\n    /**\n     * <p>Register a callback to be invoked when the checked radio button\n     * changes in this group.</p>\n     *\n     * @param listener the callback to call on checked state change\n     */\n    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {\n        mOnCheckedChangeListener = listener;\n    }\n\n    /**\n     * <p>Interface definition for a callback to be invoked when the checked\n     * radio button changed in this group.</p>\n     */\n    public interface OnCheckedChangeListener {\n        /**\n         * <p>Called when the checked radio button has changed. When the\n         * selection is cleared, checkedId is -1.</p>\n         *\n         * @param group     the group in which the checked radio button has changed\n         * @param checkedId the unique identifier of the newly checked radio button\n         */\n        void onCheckedChanged(RadioGridGroup group, @IdRes int checkedId);\n    }\n\n    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {\n        @Override\n        public void onCheckedChanged(@NonNull CompoundButton buttonView, boolean isChecked) {\n            // prevents from infinite recursion\n            if (mProtectFromCheckedChange) {\n                return;\n            }\n\n            mProtectFromCheckedChange = true;\n            if (mCheckedId != -1) {\n                setCheckedStateForView(mCheckedId, false);\n            }\n            mProtectFromCheckedChange = false;\n\n            int id = buttonView.getId();\n            setCheckedId(id);\n        }\n    }\n\n    /**\n     * <p>A pass-through listener acts upon the events and dispatches them\n     * to another listener. This allows the table layout to set its own internal\n     * hierarchy change listener without preventing the user to setup his.</p>\n     */\n    private class PassThroughHierarchyChangeListener implements\n            ViewGroup.OnHierarchyChangeListener {\n        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;\n\n        /**\n         * {@inheritDoc}\n         */\n        @Override\n        public void onChildViewAdded(View parent, View child) {\n            if (parent == RadioGridGroup.this && child instanceof RadioButton) {\n                int id = child.getId();\n                // generates an id if it's missing\n                if (id == View.NO_ID) {\n                    id = ViewUtils.generateViewId();\n                    child.setId(id);\n                }\n                ((RadioButton) child).setOnCheckedChangeListener(\n                        mChildOnCheckedChangeListener);\n            }\n\n            if (mOnHierarchyChangeListener != null) {\n                mOnHierarchyChangeListener.onChildViewAdded(parent, child);\n            }\n        }\n\n        /**\n         * {@inheritDoc}\n         */\n        @Override\n        public void onChildViewRemoved(View parent, View child) {\n            if (parent == RadioGridGroup.this && child instanceof RadioButton) {\n                ((RadioButton) child).setOnCheckedChangeListener(null);\n            }\n\n            if (mOnHierarchyChangeListener != null) {\n                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/SearchBarMover.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.animation.Animator;\nimport android.animation.ValueAnimator;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport com.hippo.yorozuya.MathUtils;\nimport com.hippo.yorozuya.SimpleAnimatorListener;\nimport com.hippo.yorozuya.ViewUtils;\n\npublic class SearchBarMover extends RecyclerView.OnScrollListener {\n    private static final long ANIMATE_TIME = 300L;\n    private final Helper mHelper;\n    private final View mSearchBar;\n    private boolean mShow;\n    private ValueAnimator mSearchBarMoveAnimator;\n\n    public SearchBarMover(Helper helper, View searchBar, RecyclerView... recyclerViews) {\n        mHelper = helper;\n        mSearchBar = searchBar;\n        for (RecyclerView recyclerView : recyclerViews) {\n            recyclerView.addOnScrollListener(this);\n        }\n    }\n\n    public void cancelAnimation() {\n        if (mSearchBarMoveAnimator != null) {\n            mSearchBarMoveAnimator.cancel();\n        }\n    }\n\n    @Override\n    public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {\n        if (newState == RecyclerView.SCROLL_STATE_IDLE && mHelper.isValidView(recyclerView)) {\n            returnSearchBarPosition();\n        }\n    }\n\n    @Override\n    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {\n        if (mHelper.isValidView(recyclerView)) {\n            int oldBottom = (int) ViewUtils.getY2(mSearchBar);\n            int offsetYStep = MathUtils.clamp(-dy, -oldBottom, -(int) mSearchBar.getTranslationY());\n            if (offsetYStep != 0) {\n                ViewUtils.translationYBy(mSearchBar, offsetYStep);\n            }\n        }\n    }\n\n    public void returnSearchBarPosition() {\n        returnSearchBarPosition(true);\n    }\n\n    @SuppressWarnings(\"SimplifiableIfStatement\")\n    public void returnSearchBarPosition(boolean animation) {\n        if (mSearchBar.getHeight() == 0) {\n            // Layout not called\n            return;\n        }\n\n        boolean show;\n        if (mHelper.forceShowSearchBar()) {\n            show = true;\n        } else {\n            RecyclerView recyclerView = mHelper.getValidRecyclerView();\n            if (recyclerView == null) {\n                return;\n            }\n            if (!recyclerView.isShown()) {\n                show = true;\n            } else if (recyclerView.computeVerticalScrollOffset() < mSearchBar.getBottom()) {\n                show = true;\n            } else {\n                show = (int) ViewUtils.getY2(mSearchBar) > (mSearchBar.getHeight()) / 2;\n            }\n        }\n\n        int offset;\n        if (show) {\n            offset = -(int) mSearchBar.getTranslationY();\n        } else {\n            offset = -(int) ViewUtils.getY2(mSearchBar);\n        }\n\n        if (offset == 0) {\n            // No need to scroll\n            return;\n        }\n\n        if (animation) {\n            if (mSearchBarMoveAnimator != null) {\n                if (mShow == show) {\n                    // The same target, no need to do animation\n                    return;\n                } else {\n                    // Cancel it\n                    mSearchBarMoveAnimator.cancel();\n                    mSearchBarMoveAnimator = null;\n                }\n            }\n\n            mShow = show;\n            final ValueAnimator va = ValueAnimator.ofInt(0, offset);\n            va.setDuration(ANIMATE_TIME);\n            va.addListener(new SimpleAnimatorListener() {\n                @Override\n                public void onAnimationEnd(@NonNull Animator animation) {\n                    mSearchBarMoveAnimator = null;\n                }\n            });\n            va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n                int lastValue;\n\n                @Override\n                public void onAnimationUpdate(@NonNull ValueAnimator animation) {\n                    int value = (Integer) animation.getAnimatedValue();\n                    int offsetStep = value - lastValue;\n                    lastValue = value;\n                    ViewUtils.translationYBy(mSearchBar, offsetStep);\n                }\n            });\n            mSearchBarMoveAnimator = va;\n            va.start();\n        } else {\n            if (mSearchBarMoveAnimator != null) {\n                mSearchBarMoveAnimator.cancel();\n            }\n            ViewUtils.translationYBy(mSearchBar, offset);\n        }\n    }\n\n    public void showSearchBar() {\n        showSearchBar(true);\n    }\n\n    public void showSearchBar(boolean animation) {\n        if (mSearchBar.getHeight() == 0) {\n            // Layout not called\n            return;\n        }\n\n        final int offset = -(int) mSearchBar.getTranslationY();\n\n        if (offset == 0) {\n            // No need to scroll\n            return;\n        }\n\n        if (animation) {\n            if (mSearchBarMoveAnimator != null) {\n                if (mShow) {\n                    // The same target, no need to do animation\n                    return;\n                } else {\n                    // Cancel it\n                    mSearchBarMoveAnimator.cancel();\n                    mSearchBarMoveAnimator = null;\n                }\n            }\n\n            mShow = true;\n            final ValueAnimator va = ValueAnimator.ofInt(0, offset);\n            va.setDuration(ANIMATE_TIME);\n            va.addListener(new SimpleAnimatorListener() {\n                @Override\n                public void onAnimationEnd(@NonNull Animator animation) {\n                    mSearchBarMoveAnimator = null;\n                }\n            });\n            va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n                int lastValue;\n\n                @Override\n                public void onAnimationUpdate(@NonNull ValueAnimator animation) {\n                    int value = (Integer) animation.getAnimatedValue();\n                    int offsetStep = value - lastValue;\n                    lastValue = value;\n                    ViewUtils.translationYBy(mSearchBar, offsetStep);\n                }\n            });\n            mSearchBarMoveAnimator = va;\n            va.start();\n        } else {\n            if (mSearchBarMoveAnimator != null) {\n                mSearchBarMoveAnimator.cancel();\n            }\n            ViewUtils.translationYBy(mSearchBar, offset);\n        }\n    }\n\n    public interface Helper {\n        boolean isValidView(RecyclerView recyclerView);\n\n        @Nullable\n        RecyclerView getValidRecyclerView();\n\n        boolean forceShowSearchBar();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/SimpleGridAutoSpanLayout.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\npublic class SimpleGridAutoSpanLayout extends SimpleGridLayout {\n    public static final int STRATEGY_SUITABLE_SIZE = 1;\n\n    private int mColumnSize = -1;\n    private boolean mColumnSizeChanged = true;\n    private int mStrategy;\n\n    public SimpleGridAutoSpanLayout(Context context) {\n        super(context);\n    }\n\n    public SimpleGridAutoSpanLayout(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public SimpleGridAutoSpanLayout(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n    }\n\n    public static int getSpanCountForSuitableSize(int total, int single) {\n        int span = total / single;\n        if (span <= 0) {\n            return 1;\n        }\n        int span2 = span + 1;\n        float deviation = Math.abs(1 - ((float) total / span / (float) single));\n        float deviation2 = Math.abs(1 - ((float) total / span2 / (float) single));\n        return deviation < deviation2 ? span : span2;\n    }\n\n    public static int getSpanCountForMinSize(int total, int single) {\n        return Math.max(1, total / single);\n    }\n\n    public void setColumnSize(int columnSize) {\n        if (columnSize == mColumnSize) {\n            return;\n        }\n        mColumnSize = columnSize;\n        mColumnSizeChanged = true;\n    }\n\n    public void setStrategy(int strategy) {\n        if (strategy == mStrategy) {\n            return;\n        }\n        mStrategy = strategy;\n        mColumnSizeChanged = true;\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int widthMode = MeasureSpec.getMode(widthMeasureSpec);\n        int widthSize = MeasureSpec.getSize(widthMeasureSpec);\n\n        if (mColumnSizeChanged && mColumnSize > 0 && widthMode == MeasureSpec.EXACTLY) {\n            int totalSpace = widthSize - getPaddingRight() - getPaddingLeft();\n            int spanCount;\n            if (mStrategy == STRATEGY_SUITABLE_SIZE) {\n                spanCount = getSpanCountForSuitableSize(totalSpace, mColumnSize);\n            } else {\n                spanCount = getSpanCountForMinSize(totalSpace, mColumnSize);\n            }\n            setColumnCount(spanCount);\n            mColumnSizeChanged = false;\n        }\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/SimpleGridLayout.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport com.hippo.ehviewer.R;\nimport com.hippo.yorozuya.MathUtils;\nimport com.hippo.yorozuya.ViewUtils;\n\n/**\n * not scrollable\n *\n * @author Hippo\n */\npublic class SimpleGridLayout extends ViewGroup {\n    private static final int DEFAULT_COLUMN_COUNT = 3;\n\n    private int mColumnCount;\n    private int mItemMargin;\n\n    private int[] mRowHeights;\n    private int mItemWidth;\n\n    public SimpleGridLayout(Context context) {\n        super(context);\n        init(context, null);\n    }\n\n    public SimpleGridLayout(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public SimpleGridLayout(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        init(context, attrs);\n    }\n\n    private void init(Context context, AttributeSet attrs) {\n        //noinspection resource\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SimpleGridLayout);\n        try {\n            mColumnCount = a.getInteger(R.styleable.SimpleGridLayout_columnCount, DEFAULT_COLUMN_COUNT);\n            mItemMargin = a.getDimensionPixelOffset(R.styleable.SimpleGridLayout_itemMargin, 0);\n        } finally {\n            a.recycle();\n        }\n    }\n\n    public void setItemMargin(int itemMargin) {\n        if (mItemMargin != itemMargin) {\n            mItemMargin = itemMargin;\n            requestLayout();\n        }\n    }\n\n    public void setColumnCount(int columnCount) {\n        if (columnCount <= 0) {\n            throw new IllegalStateException(\"Column count can't be \" + columnCount);\n        }\n\n        if (mColumnCount != columnCount) {\n            mColumnCount = columnCount;\n            requestLayout();\n        }\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        int maxRowCount = MathUtils.ceilDivide(getChildCount(), mColumnCount);\n        if (mRowHeights == null || mRowHeights.length != maxRowCount) {\n            mRowHeights = new int[maxRowCount];\n        }\n\n        int widthMode = MeasureSpec.getMode(widthMeasureSpec);\n        int heightMode = MeasureSpec.getMode(heightMeasureSpec);\n        int maxWidth = MeasureSpec.getSize(widthMeasureSpec);\n        int maxHeight = MeasureSpec.getSize(heightMeasureSpec);\n\n        if (widthMode == MeasureSpec.UNSPECIFIED) {\n            maxWidth = 300;\n        }\n        if (heightMode == MeasureSpec.UNSPECIFIED) {\n            maxHeight = ViewUtils.MAX_SIZE;\n        }\n\n        // Get item width MeasureSpec\n        mItemWidth = Math.max(\n                (maxWidth - getPaddingLeft() - getPaddingRight() - ((mColumnCount - 1) * mItemMargin)) / mColumnCount, 1);\n        int itemWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mItemWidth, MeasureSpec.EXACTLY);\n        int itemHeightMeasureSpec = MeasureSpec.UNSPECIFIED;\n\n        int measuredWidth = maxWidth;\n        int measuredHeight = 0;\n        int rowHeight = 0;\n        int row = 0;\n        int count = getChildCount();\n        for (int index = 0, indexInRow = 0; index < count; index++, indexInRow++) {\n            final View child = getChildAt(index);\n            if (child.getVisibility() == View.GONE) {\n                indexInRow--;\n                continue;\n            }\n\n            child.measure(itemWidthMeasureSpec, itemHeightMeasureSpec);\n\n            if (indexInRow == mColumnCount) {\n                // New row\n                indexInRow = 0;\n                rowHeight = 0;\n                row++;\n            }\n\n            rowHeight = Math.max(rowHeight, child.getMeasuredHeight());\n\n            if (indexInRow == mColumnCount - 1 || index == count - 1) {\n                mRowHeights[row] = rowHeight;\n                measuredHeight += rowHeight + mItemMargin;\n            }\n        }\n        measuredHeight -= mItemMargin;\n        measuredHeight = Math.max(0, Math.min(measuredHeight + getPaddingTop() + getPaddingBottom(), maxHeight));\n\n        setMeasuredDimension(measuredWidth, measuredHeight);\n    }\n\n    @Override\n    protected void onLayout(boolean changed, int l, int t, int r, int b) {\n        int itemWidth = mItemWidth;\n        int itemMargin = mItemMargin;\n        int paddingLeft = getPaddingLeft();\n        int left = paddingLeft;\n        int top = getPaddingTop();\n        int row = 0;\n        int count = getChildCount();\n        for (int index = 0, indexInRow = 0; index < count; index++, indexInRow++) {\n            final View child = getChildAt(index);\n            if (child.getVisibility() == View.GONE) {\n                indexInRow--;\n                continue;\n            }\n\n            if (indexInRow == mColumnCount) {\n                // New row\n                left = paddingLeft;\n                top += mRowHeights[row] + itemMargin;\n\n                indexInRow = 0;\n                row++;\n            }\n\n            child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());\n\n            left += itemWidth + itemMargin;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/Slider.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.animation.ValueAnimator;\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Rect;\nimport android.graphics.RectF;\nimport android.graphics.drawable.Drawable;\nimport android.util.AttributeSet;\nimport android.view.Gravity;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.widget.PopupWindow;\nimport android.widget.RelativeLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.widget.AppCompatImageView;\nimport androidx.core.graphics.drawable.DrawableCompat;\n\nimport com.hippo.ehviewer.R;\nimport com.hippo.yorozuya.AnimationUtils;\nimport com.hippo.yorozuya.LayoutUtils;\nimport com.hippo.yorozuya.MathUtils;\nimport com.hippo.yorozuya.SimpleHandler;\n\npublic class Slider extends View {\n    private static final char[] CHARACTERS = {\n            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'\n    };\n\n    private static final int BUBBLE_WIDTH = 26;\n    private static final int BUBBLE_HEIGHT = 32;\n    private final RectF mLeftRectF = new RectF();\n    private final RectF mRightRectF = new RectF();\n    private final int[] mTemp = new int[2];\n    private Context mContext;\n    private Paint mPaint;\n    private Paint mBgPaint;\n    private PopupWindow mPopup;\n    private BubbleView mBubble;\n    private int mStart;\n    private int mEnd;\n    private int mProgress;\n    private float mPercent;\n    private int mDrawProgress;\n    private float mDrawPercent;\n    private int mTargetProgress;\n    private float mThickness;\n    private float mRadius;\n    private float mCharWidth;\n    private float mCharHeight;\n    private int mBubbleWidth;\n    private int mBubbleHeight;\n    private int mBubbleMinWidth;\n    private int mBubbleMinHeight;\n    private int mPopupX;\n    private int mPopupY;\n    private int mPopupWidth;\n    private boolean mReverse = false;\n\n    private boolean mShowBubble;\n\n    private float mDrawBubbleScale = 0f;\n\n    private ValueAnimator mProgressAnimation;\n    private ValueAnimator mBubbleScaleAnimation;\n\n    private OnSetProgressListener mListener;\n\n    private CheckForShowBubble mCheckForShowBubble;\n\n    public Slider(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs);\n    }\n\n    public Slider(Context context, AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        init(context, attrs);\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    private void init(Context context, AttributeSet attrs) {\n        mContext = context;\n        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n        textPaint.setTextAlign(Paint.Align.CENTER);\n\n        Resources resources = context.getResources();\n        mBubbleMinWidth = resources.getDimensionPixelOffset(R.dimen.slider_bubble_width);\n        mBubbleMinHeight = resources.getDimensionPixelOffset(R.dimen.slider_bubble_height);\n\n        mBubble = new BubbleView(context, textPaint);\n        mBubble.setScaleX(0.0f);\n        mBubble.setScaleY(0.0f);\n        RelativeLayout relativeLayout = new RelativeLayout(context);\n        relativeLayout.addView(mBubble);\n        relativeLayout.setBackgroundDrawable(null);\n        mPopup = new PopupWindow(relativeLayout);\n        mPopup.setOutsideTouchable(false);\n        mPopup.setTouchable(false);\n        mPopup.setFocusable(false);\n\n        //noinspection resource\n        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider);\n        try {\n            textPaint.setColor(a.getColor(R.styleable.Slider_textColor, Color.WHITE));\n            textPaint.setTextSize(a.getDimensionPixelSize(R.styleable.Slider_textSize, 12));\n\n            updateTextSize();\n\n            setRange(a.getInteger(R.styleable.Slider_start, 0), a.getInteger(R.styleable.Slider_end, 0));\n            setProgress(a.getInteger(R.styleable.Slider_slider_progress, 0));\n            mThickness = a.getDimension(R.styleable.Slider_thickness, 2);\n            mRadius = a.getDimension(R.styleable.Slider_radius, 6);\n            setColor(a.getColor(R.styleable.Slider_color, Color.BLACK));\n\n            mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\n            mBgPaint.setColor(a.getBoolean(R.styleable.Slider_dark, false) ? 0x4dffffff : 0x42000000);\n        } finally {\n            a.recycle();\n        }\n\n        mProgressAnimation = new ValueAnimator();\n        mProgressAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);\n        mProgressAnimation.addUpdateListener(animation -> {\n            float value = (Float) animation.getAnimatedValue();\n            mDrawPercent = value;\n            mDrawProgress = Math.round(MathUtils.lerp((float) mStart, mEnd, value));\n            updateBubblePosition();\n            mBubble.setProgress(mDrawProgress);\n            invalidate();\n        });\n\n        mBubbleScaleAnimation = new ValueAnimator();\n        mBubbleScaleAnimation.addUpdateListener(animation -> {\n            float value = (Float) animation.getAnimatedValue();\n            mDrawBubbleScale = value;\n            mBubble.setScaleX(value);\n            mBubble.setScaleY(value);\n            invalidate();\n        });\n    }\n\n    private void updateTextSize() {\n        int length = CHARACTERS.length;\n        float[] widths = new float[length];\n        mPaint.getTextWidths(CHARACTERS, 0, length, widths);\n        mCharWidth = 0.0f;\n        for (float f : widths) {\n            mCharWidth = Math.max(mCharWidth, f);\n        }\n\n        Paint.FontMetrics fm = mPaint.getFontMetrics();\n        mCharHeight = fm.bottom - fm.top;\n    }\n\n    private void updateBubbleSize() {\n        int oldWidth = mBubbleWidth;\n        int oldHeight = mBubbleHeight;\n        mBubbleWidth = (int) Math.max(mBubbleMinWidth,\n                Integer.toString(mEnd).length() * mCharWidth + LayoutUtils.dp2pix(mContext, 8));\n        mBubbleHeight = (int) Math.max(mBubbleMinHeight,\n                mCharHeight + LayoutUtils.dp2pix(mContext, 8));\n\n        if (oldWidth != mBubbleWidth && oldHeight != mBubbleHeight) {\n            RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mBubble.getLayoutParams();\n            lp.width = mBubbleWidth;\n            lp.height = mBubbleHeight;\n            mBubble.setLayoutParams(lp);\n        }\n    }\n\n    private void updatePopup() {\n        int width = getWidth();\n        int paddingTop = getPaddingTop();\n        int paddingBottom = getPaddingBottom();\n\n        getLocationInWindow(mTemp);\n\n        mPopupWidth = (int) (width - mRadius - mRadius + mBubbleWidth);\n        int popupHeight = mBubbleHeight;\n        mPopupX = (int) (mTemp[0] + mRadius - ((float) mBubbleWidth / 2));\n        mPopupY = (int) (mTemp[1] - popupHeight + paddingTop +\n                ((float) (getHeight() - paddingTop - paddingBottom) / 2) -\n                mRadius - LayoutUtils.dp2pix(mContext, 2));\n\n        mPopup.update(mPopupX, mPopupY, mPopupWidth, popupHeight, false);\n    }\n\n    private void updateBubblePosition() {\n        float x = ((mPopupWidth - mBubbleWidth) * (mReverse ? (1.0f - mDrawPercent) : mDrawPercent));\n        mBubble.setX(x);\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        updatePopup();\n        updateBubblePosition();\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n\n        mPopup.showAtLocation(this, Gravity.TOP | Gravity.START, mPopupX, mPopupY);\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n\n        mPopup.dismiss();\n    }\n\n    private void startProgressAnimation(float percent) {\n        float currentValue;\n        if (mProgressAnimation.isRunning()) {\n            mProgressAnimation.setCurrentPlayTime(mProgressAnimation.getCurrentPlayTime());\n            Object value = mProgressAnimation.getAnimatedValue();\n            if (value instanceof Float) {\n                currentValue = (float) value;\n            } else {\n                currentValue = mDrawPercent;\n            }\n        } else {\n            currentValue = mDrawPercent;\n        }\n        mProgressAnimation.cancel();\n        mProgressAnimation.setFloatValues(currentValue, percent);\n        mProgressAnimation.setDuration(Math.min(500, (long) (50 * getWidth() * Math.abs(currentValue - percent))));\n        mProgressAnimation.start();\n    }\n\n    private void startShowBubbleAnimation() {\n        mBubbleScaleAnimation.cancel();\n        mBubbleScaleAnimation.setFloatValues(mDrawBubbleScale, 1.0f);\n        mBubbleScaleAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR);\n        mBubbleScaleAnimation.setDuration((long) (300 * Math.abs(mDrawBubbleScale - 1.0f)));\n        mBubbleScaleAnimation.start();\n    }\n\n    private void startHideBubbleAnimation() {\n        mBubbleScaleAnimation.cancel();\n        mBubbleScaleAnimation.setFloatValues(mDrawBubbleScale, 0.0f);\n        mBubbleScaleAnimation.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR);\n        mBubbleScaleAnimation.setDuration((long) (300 * Math.abs(mDrawBubbleScale - 0.0f)));\n        mBubbleScaleAnimation.start();\n    }\n\n    public void setColor(int color) {\n        mPaint.setColor(color);\n        mBubble.setColor(color);\n        invalidate();\n    }\n\n    public void setRange(int start, int end) {\n        mStart = start;\n        mEnd = end;\n        setProgress(mProgress);\n\n        updateBubbleSize();\n    }\n\n    public int getProgress() {\n        return mProgress;\n    }\n\n    public void setProgress(int progress) {\n        progress = MathUtils.clamp(progress, mStart, mEnd);\n        int oldProgress = mProgress;\n        if (mProgress != progress) {\n            mProgress = progress;\n            mPercent = MathUtils.delerp(mStart, mEnd, mProgress);\n            mTargetProgress = progress;\n\n            if (mProgressAnimation == null) {\n                // For init\n                mDrawPercent = mPercent;\n                mDrawProgress = mProgress;\n                updateBubblePosition();\n                mBubble.setProgress(mDrawProgress);\n            } else {\n                startProgressAnimation(mPercent);\n            }\n            invalidate();\n        }\n        if (mListener != null) {\n            mListener.onSetProgress(this, progress, oldProgress, false, true);\n        }\n    }\n\n    public void setReverse(boolean reverse) {\n        if (mReverse != reverse) {\n            mReverse = reverse;\n            invalidate();\n        }\n    }\n\n    public void setOnSetProgressListener(OnSetProgressListener listener) {\n        mListener = listener;\n    }\n\n    @Override\n    protected void onDraw(@NonNull Canvas canvas) {\n        int width = getWidth();\n        int height = getHeight();\n        if (width < LayoutUtils.dp2pix(mContext, 24)) {\n            canvas.drawRect(0, 0, width, getHeight(), mPaint);\n        } else {\n            int paddingLeft = getPaddingLeft();\n            int paddingTop = getPaddingTop();\n            int paddingRight = getPaddingRight();\n            int paddingBottom = getPaddingBottom();\n            float thickness = mThickness;\n            float radius = mRadius;\n            float halfThickness = thickness / 2;\n\n            int saved = canvas.save();\n\n            canvas.translate(0, paddingTop + ((float) (height - paddingTop - paddingBottom) / 2));\n\n            float currentX = paddingLeft + radius + (width - radius - radius - paddingLeft - paddingRight) *\n                    (mReverse ? (1.0f - mDrawPercent) : mDrawPercent);\n\n            mLeftRectF.set(paddingLeft + radius, -halfThickness, currentX, halfThickness);\n            mRightRectF.set(currentX, -halfThickness, width - paddingRight - radius, halfThickness);\n\n            // Draw bar\n            if (mReverse) {\n                canvas.drawRect(mRightRectF, mPaint);\n                canvas.drawRect(mLeftRectF, mBgPaint);\n            } else {\n                canvas.drawRect(mLeftRectF, mPaint);\n                canvas.drawRect(mRightRectF, mBgPaint);\n            }\n\n            // Draw controller\n            float scale = 1.0f - mDrawBubbleScale;\n            if (scale != 0.0f) {\n                canvas.scale(scale, scale, currentX, 0);\n                canvas.drawCircle(currentX, 0, radius, mPaint);\n            }\n\n            canvas.restoreToCount(saved);\n        }\n    }\n\n    private void setShowBubble(boolean showBubble) {\n        if (mShowBubble != showBubble) {\n            mShowBubble = showBubble;\n            if (showBubble) {\n                startShowBubbleAnimation();\n            } else {\n                startHideBubbleAnimation();\n            }\n        }\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(@NonNull MotionEvent event) {\n        int action = event.getAction();\n        switch (action) {\n            case MotionEvent.ACTION_DOWN:\n            case MotionEvent.ACTION_MOVE:\n            case MotionEvent.ACTION_UP:\n            case MotionEvent.ACTION_CANCEL:\n\n                if (mListener != null) {\n                    if (action == MotionEvent.ACTION_DOWN) {\n                        mListener.onFingerDown();\n                    } else if (action == MotionEvent.ACTION_UP) {\n                        mListener.onFingerUp();\n                    }\n                }\n\n                int paddingLeft = getPaddingLeft();\n                int paddingRight = getPaddingRight();\n                float radius = mRadius;\n                float x = event.getX();\n                int progress = Math.round(MathUtils.lerp((float) mStart, (float) mEnd,\n                        MathUtils.clamp((mReverse ? (getWidth() - paddingLeft - radius - x) : (x - radius - paddingLeft)) /\n                                (getWidth() - radius - radius - paddingLeft - paddingRight), 0.0f, 1.0f)));\n                float percent = MathUtils.delerp(mStart, mEnd, progress);\n\n                // ACTION_CANCEL not changed\n                if (action == MotionEvent.ACTION_CANCEL) {\n                    progress = mProgress;\n                    percent = mPercent;\n                }\n\n                if (mTargetProgress != progress) {\n                    mTargetProgress = progress;\n                    startProgressAnimation(percent);\n\n                    if (mListener != null) {\n                        mListener.onSetProgress(this, mProgress, progress, true, false);\n                    }\n                }\n\n                if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {\n                    SimpleHandler.getInstance().removeCallbacks(mCheckForShowBubble);\n                    setShowBubble(false);\n                } else if (action == MotionEvent.ACTION_DOWN) {\n                    if (mCheckForShowBubble == null) {\n                        mCheckForShowBubble = new CheckForShowBubble();\n                    }\n                    SimpleHandler.getInstance().postDelayed(mCheckForShowBubble, ViewConfiguration.getTapTimeout());\n                }\n\n                if (action == MotionEvent.ACTION_UP) {\n                    int oldProgress = mProgress;\n                    if (mProgress != progress) {\n                        mProgress = progress;\n                        mPercent = mDrawPercent;\n                    }\n                    if (mListener != null) {\n                        mListener.onSetProgress(this, progress, oldProgress, true, true);\n                    }\n                }\n                break;\n        }\n\n        return true;\n    }\n\n    public interface OnSetProgressListener {\n        void onSetProgress(Slider slider, int newProgress, int oldProgress, boolean byUser, boolean confirm);\n\n        void onFingerDown();\n\n        void onFingerUp();\n    }\n\n    @SuppressLint(\"ViewConstructor\")\n    private static class BubbleView extends AppCompatImageView {\n        private static final float TEXT_CENTER = (float) BUBBLE_WIDTH / 2.0f / BUBBLE_HEIGHT;\n\n        private final Drawable mDrawable;\n\n        private final Paint mTextPaint;\n        private final Rect mRect = new Rect();\n        private String mProgressStr = \"\";\n\n        public BubbleView(Context context, Paint paint) {\n            super(context);\n            setImageResource(R.drawable.v_slider_bubble);\n            mDrawable = DrawableCompat.wrap(getDrawable());\n            setImageDrawable(mDrawable);\n            mTextPaint = paint;\n        }\n\n        public void setColor(int color) {\n            DrawableCompat.setTint(mDrawable, color);\n        }\n\n        public void setProgress(int progress) {\n            String str = Integer.toString(progress);\n            if (!str.equals(mProgressStr)) {\n                mProgressStr = str;\n                mTextPaint.getTextBounds(str, 0, str.length(), mRect);\n                invalidate();\n            }\n        }\n\n        @Override\n        protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n            super.onSizeChanged(w, h, oldw, oldh);\n            setPivotX((float) w / 2);\n            setPivotY(h);\n        }\n\n        @Override\n        protected void onDraw(@NonNull Canvas canvas) {\n            super.onDraw(canvas);\n\n            int width = getWidth();\n            int height = getHeight();\n            int x = width / 2;\n            int y = (int) ((height * TEXT_CENTER) + ((float) mRect.height() / 2));\n            canvas.drawText(mProgressStr, x, y, mTextPaint);\n        }\n    }\n\n    private final class CheckForShowBubble implements Runnable {\n        @Override\n        public void run() {\n            setShowBubble(true);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/TextClock.java",
    "content": "/*\n * Copyright (C) 2014 Hippo Seven\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\npackage com.hippo.widget;\n\nimport android.content.BroadcastReceiver;\nimport android.content.ContentResolver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.database.ContentObserver;\nimport android.net.Uri;\nimport android.os.Handler;\nimport android.os.SystemClock;\nimport android.provider.Settings;\nimport android.text.format.DateFormat;\nimport android.util.AttributeSet;\nimport android.view.ViewDebug.ExportedProperty;\n\nimport androidx.appcompat.widget.AppCompatTextView;\n\nimport java.util.Calendar;\nimport java.util.TimeZone;\n\npublic class TextClock extends AppCompatTextView {\n    public static final CharSequence DEFAULT_FORMAT_12_HOUR = \"hh:mm a\";\n    public static final CharSequence DEFAULT_FORMAT_24_HOUR;\n\n    static {\n        DEFAULT_FORMAT_24_HOUR = \"HH:mm\";\n    }\n\n    private CharSequence mFormat12;\n    private CharSequence mFormat24;\n\n    @ExportedProperty\n    private CharSequence mFormat;\n    @ExportedProperty\n    private boolean mHasSeconds;\n\n    private boolean mAttached;\n\n    private Calendar mTime;\n    private String mTimeZone;\n    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {\n                final String timeZone = intent.getStringExtra(\"time-zone\");\n                createTime(timeZone);\n            }\n            onTimeChanged();\n        }\n    };\n\n    public TextClock(Context context) {\n        this(context, null);\n    }\n\n    public TextClock(Context context, AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n\n    public TextClock(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n\n        mFormat12 = DEFAULT_FORMAT_12_HOUR;\n        mFormat24 = DEFAULT_FORMAT_24_HOUR;\n        mTimeZone = TimeZone.getDefault().getID();\n\n        init();\n    }\n\n    private void init() {\n        createTime(mTimeZone);\n        // Wait until onAttachedToWindow() to handle the ticker\n        chooseFormat(false);\n    }\n\n    private void createTime(String timeZone) {\n        if (timeZone != null) {\n            mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));\n        } else {\n            mTime = Calendar.getInstance();\n        }\n    }\n\n    public CharSequence getFormat12Hour() {\n        return mFormat12;\n    }\n\n    public void setFormat12Hour(CharSequence format) {\n        mFormat12 = format;\n\n        chooseFormat();\n        onTimeChanged();\n    }\n\n    public CharSequence getFormat24Hour() {\n        return mFormat24;\n    }    private final Runnable mTicker = new Runnable() {\n        @Override\n        public void run() {\n            onTimeChanged();\n\n            long now = SystemClock.uptimeMillis();\n            long next = now + (1000 - now % 1000);\n\n            getHandler().postAtTime(mTicker, next);\n        }\n    };\n\n    public void setFormat24Hour(CharSequence format) {\n        mFormat24 = format;\n\n        chooseFormat();\n        onTimeChanged();\n    }\n\n    public boolean is24HourModeEnabled() {\n        return DateFormat.is24HourFormat(getContext());\n    }\n\n    public String getTimeZone() {\n        return mTimeZone;\n    }\n\n    public void setTimeZone(String timeZone) {\n        mTimeZone = timeZone;\n\n        createTime(timeZone);\n        onTimeChanged();\n    }\n\n    /**\n     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}\n     * depending on whether the user has selected 24-hour format.\n     * <p>\n     * Calling this method does not schedule or unschedule the time ticker.\n     */\n    private void chooseFormat() {\n        chooseFormat(true);\n    }\n\n    public CharSequence getFormat() {\n        return mFormat;\n    }\n\n    /**\n     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}\n     * depending on whether the user has selected 24-hour format.\n     *\n     * @param handleTicker true if calling this method should schedule/unschedule the\n     *                     time ticker, false otherwise\n     */\n    private void chooseFormat(boolean handleTicker) {\n        final boolean format24Requested = is24HourModeEnabled();\n\n        if (format24Requested) {\n            mFormat = mFormat24;\n        } else {\n            mFormat = mFormat12;\n        }\n\n        boolean hadSeconds = mHasSeconds;\n        mHasSeconds = DateUtils.hasSeconds(mFormat);\n\n        if (handleTicker && mAttached && hadSeconds != mHasSeconds) {\n            if (hadSeconds) getHandler().removeCallbacks(mTicker);\n            else mTicker.run();\n        }\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n\n        if (!mAttached) {\n            mAttached = true;\n\n            registerReceiver();\n            registerObserver();\n\n            createTime(mTimeZone);\n\n            if (mHasSeconds) {\n                mTicker.run();\n            } else {\n                onTimeChanged();\n            }\n        }\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n\n        if (mAttached) {\n            unregisterReceiver();\n            unregisterObserver();\n\n            getHandler().removeCallbacks(mTicker);\n\n            mAttached = false;\n        }\n    }\n\n    private void registerReceiver() {\n        final IntentFilter filter = new IntentFilter();\n\n        filter.addAction(Intent.ACTION_TIME_TICK);\n        filter.addAction(Intent.ACTION_TIME_CHANGED);\n        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);\n\n        getContext().registerReceiver(mIntentReceiver, filter, null, getHandler());\n    }    private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {\n        @Override\n        public void onChange(boolean selfChange) {\n            chooseFormat();\n            onTimeChanged();\n        }\n\n        @Override\n        public void onChange(boolean selfChange, Uri uri) {\n            chooseFormat();\n            onTimeChanged();\n        }\n    };\n\n    private void registerObserver() {\n        final ContentResolver resolver = getContext().getContentResolver();\n        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);\n    }\n\n    private void unregisterReceiver() {\n        getContext().unregisterReceiver(mIntentReceiver);\n    }\n\n    private void unregisterObserver() {\n        final ContentResolver resolver = getContext().getContentResolver();\n        resolver.unregisterContentObserver(mFormatChangeObserver);\n    }\n\n    private void onTimeChanged() {\n        mTime.setTimeInMillis(System.currentTimeMillis());\n        setText(DateFormat.format(mFormat, mTime));\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/lockpattern/LockPatternUtils.java",
    "content": "/*\n * Copyright (C) 2007 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\npackage com.hippo.widget.lockpattern;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Utilities for the lock pattern and its settings.\n */\npublic class LockPatternUtils {\n    /**\n     * Deserialize a pattern.\n     *\n     * @param string The pattern serialized with {@link #patternToString}\n     * @return The pattern.\n     */\n    public static List<LockPatternView.Cell> stringToPattern(String string) {\n        List<LockPatternView.Cell> result = new ArrayList<>();\n\n        final byte[] bytes = string.getBytes();\n        for (byte b : bytes) {\n            result.add(LockPatternView.Cell.of(b / 3, b % 3));\n        }\n        return result;\n    }\n\n    /**\n     * Serialize a pattern.\n     *\n     * @param pattern The pattern.\n     * @return The pattern in string form.\n     */\n    public static String patternToString(List<LockPatternView.Cell> pattern) {\n        if (pattern == null) {\n            return \"\";\n        }\n        final int patternSize = pattern.size();\n\n        byte[] res = new byte[patternSize];\n        for (int i = 0; i < patternSize; i++) {\n            LockPatternView.Cell cell = pattern.get(i);\n            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());\n        }\n        return new String(res);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/lockpattern/LockPatternView.java",
    "content": "/*\n * Copyright (C) 2007 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\npackage com.hippo.widget.lockpattern;\n\nimport android.animation.Animator;\nimport android.animation.AnimatorListenerAdapter;\nimport android.animation.ValueAnimator;\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.Rect;\nimport android.os.Debug;\nimport android.os.Parcel;\nimport android.os.Parcelable;\nimport android.os.SystemClock;\nimport android.util.AttributeSet;\nimport android.view.HapticFeedbackConstants;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.accessibility.AccessibilityManager;\nimport android.view.animation.Interpolator;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.content.ContextCompat;\nimport androidx.interpolator.view.animation.FastOutSlowInInterpolator;\nimport androidx.interpolator.view.animation.LinearOutSlowInInterpolator;\n\nimport com.hippo.ehviewer.R;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Displays and detects the user's unlock attempt, which is a drag of a finger\n * across 9 regions of the screen.\n * <p>\n * Is also capable of displaying a static pattern in \"in progress\", \"wrong\" or\n * \"correct\" states.\n */\n@SuppressWarnings(\"IntegerDivisionInFloatingPointContext\")\npublic class LockPatternView extends View {\n    // Aspect to use when rendering this view\n    private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height\n    private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h)\n    private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h)\n    private static final boolean PROFILE_DRAWING = false;\n    /**\n     * How many milliseconds we spend animating each circle of a lock pattern\n     * if the animating mode is set.  The entire animation should take this\n     * constant * the length of the pattern to complete.\n     */\n    private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;\n    /**\n     * This can be used to avoid updating the display for very small motions or noisy panels.\n     * It didn't seem to have much impact on the devices tested, so currently set to 0.\n     */\n    private static final float DRAG_THRESHOLD = 0.0f;\n    private final Cell[][] mCells;\n    private final CellState[][] mCellStates;\n    private final int mDotSize;\n    private final int mDotSizeActivated;\n    private final int mPathWidth;\n    private final Paint mPaint = new Paint();\n    private final Paint mPathPaint = new Paint();\n    private final ArrayList<Cell> mPattern = new ArrayList<>(9);\n    /**\n     * Lookup table for the circles of the pattern we are currently drawing.\n     * This will be the cells of the complete pattern unless we are animating,\n     * in which case we use this to hold the cells we are drawing for the in\n     * progress animation.\n     */\n    private final boolean[][] mPatternDrawLookup = new boolean[3][3];\n    private final float mHitFactor = 0.6f;\n    private final Path mCurrentPath = new Path();\n    private final Rect mInvalidate = new Rect();\n    private final Rect mTmpInvalidateRect = new Rect();\n    private final int mAspect;\n    private final int mRegularColor;\n    private final int mErrorColor;\n    private final int mSuccessColor;\n    private final Interpolator mFastOutSlowInInterpolator;\n    private final Interpolator mLinearOutSlowInInterpolator;\n    private boolean mDrawingProfilingStarted = false;\n    private OnPatternListener mOnPatternListener;\n    /**\n     * the in progress point:\n     * - during interaction: where the user's finger is\n     * - during animation: the current tip of the animating line\n     */\n    private float mInProgressX = -1;\n    private float mInProgressY = -1;\n    private long mAnimatingPeriodStart;\n    private DisplayMode mPatternDisplayMode = DisplayMode.Correct;\n    private boolean mInputEnabled = true;\n    private boolean mInStealthMode = false;\n    private boolean mPatternInProgress = false;\n    private float mSquareWidth;\n    private float mSquareHeight;\n\n    public LockPatternView(Context context) {\n        this(context, null);\n    }\n\n    public LockPatternView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n\n        //TypedArray a = context.obtainStyledAttributes(attrs, LockPatternView);\n\n        //final String aspect = a.getString(R.styleable.LockPatternView_aspect);\n\n        //if (\"square\".equals(aspect)) {\n        //    mAspect = ASPECT_SQUARE;\n        //} else if (\"lock_width\".equals(aspect)) {\n        //    mAspect = ASPECT_LOCK_WIDTH;\n        //} else if (\"lock_height\".equals(aspect)) {\n        //    mAspect = ASPECT_LOCK_HEIGHT;\n        //} else {\n        mAspect = ASPECT_SQUARE;\n        //}\n\n        setClickable(true);\n\n        mPathPaint.setAntiAlias(true);\n        mPathPaint.setDither(true);\n\n        mRegularColor = ContextCompat.getColor(context, R.color.lock_pattern_view_regular_color);\n        mErrorColor = ContextCompat.getColor(context, R.color.lock_pattern_view_error_color);\n        mSuccessColor = ContextCompat.getColor(context, R.color.lock_pattern_view_success_color);\n\n        mPathPaint.setColor(mRegularColor);\n\n        mPathPaint.setStyle(Paint.Style.STROKE);\n        mPathPaint.setStrokeJoin(Paint.Join.ROUND);\n        mPathPaint.setStrokeCap(Paint.Cap.ROUND);\n\n        mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);\n        mPathPaint.setStrokeWidth(mPathWidth);\n\n        mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);\n        mDotSizeActivated = getResources().getDimensionPixelSize(\n                R.dimen.lock_pattern_dot_size_activated);\n\n        mPaint.setAntiAlias(true);\n        mPaint.setDither(true);\n\n        mCellStates = new CellState[3][3];\n        for (int i = 0; i < 3; i++) {\n            for (int j = 0; j < 3; j++) {\n                mCellStates[i][j] = new CellState();\n                mCellStates[i][j].radius = mDotSize / 2;\n                mCellStates[i][j].row = i;\n                mCellStates[i][j].col = j;\n            }\n        }\n        mCells = new Cell[3][3];\n        for (int i = 0; i < 3; i++) {\n            for (int j = 0; j < 3; j++) {\n                mCells[i][j] = Cell.of(i, j);\n            }\n        }\n\n        mFastOutSlowInInterpolator = new FastOutSlowInInterpolator();\n        mLinearOutSlowInInterpolator = new LinearOutSlowInInterpolator();\n    }\n\n    /**\n     * Returns the greatest common divisor of {@code a, b}. Returns {@code 0} if\n     * {@code a == 0 && b == 0}.\n     *\n     * @throws IllegalArgumentException if {@code a < 0} or {@code b < 0}\n     */\n    private static int gcd(int a, int b) {\n        /*\n         * The reason we require both arguments to be >= 0 is because otherwise, what do you return\n         * on gcd(0, Integer.MIN_VALUE)? BigInteger.gcd would return positive 2^31, but positive\n         * 2^31 isn't an int.\n         */\n        //checkNonNegative(\"a\", a);\n        if (a < 0) {\n            throw new IllegalArgumentException(\"a (\" + a + \") must be >= 0\");\n        }\n        //checkNonNegative(\"b\", b);\n        if (b < 0) {\n            throw new IllegalArgumentException(\"b (\" + b + \") must be >= 0\");\n        }\n\n        if (a == 0) {\n            // 0 % b == 0, so b divides a, but the converse doesn't hold.\n            // BigInteger.gcd is consistent with this decision.\n            return b;\n        } else if (b == 0) {\n            return a; // similar logic\n        }\n        /*\n         * Uses the binary GCD algorithm; see http://en.wikipedia.org/wiki/Binary_GCD_algorithm.\n         * This is >40% faster than the Euclidean algorithm in benchmarks.\n         */\n        int aTwos = Integer.numberOfTrailingZeros(a);\n        a >>= aTwos; // divide out all 2s\n        int bTwos = Integer.numberOfTrailingZeros(b);\n        b >>= bTwos; // divide out all 2s\n        while (a != b) { // both a, b are odd\n\n            // The key to the binary GCD algorithm is as follows:\n            // Both a and b are odd. Assume a > b; then gcd(a - b, b) = gcd(a, b).\n            // But in gcd(a - b, b), a - b is even and b is odd, so we can divide out powers of two.\n\n            // We bend over backwards to avoid branching, adapting a technique from\n            // http://graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax\n\n            int delta = a - b; // can't overflow, since a and b are nonnegative\n\n            int minDeltaOrZero = delta & (delta >> (Integer.SIZE - 1));\n            // equivalent to Math.min(delta, 0)\n\n            a = delta - minDeltaOrZero - minDeltaOrZero; // sets a to Math.abs(a - b)\n            // a is now nonnegative and even\n\n            b += minDeltaOrZero; // sets b to min(old a, b)\n            a >>= Integer.numberOfTrailingZeros(a); // divide out all 2s, since 2 doesn't divide b\n        }\n        return a << Math.min(aTwos, bTwos);\n    }\n\n    public int getCellSize() {\n        return mPattern.size();\n    }\n\n    public String getPatternString() {\n        return LockPatternUtils.patternToString(mPattern);\n    }\n\n    /**\n     * Set the call back for pattern detection.\n     *\n     * @param onPatternListener The call back.\n     */\n    public void setOnPatternListener(\n            OnPatternListener onPatternListener) {\n        mOnPatternListener = onPatternListener;\n    }\n\n    /**\n     * Set the pattern explicitly (rather than waiting for the user to input\n     * a pattern).\n     *\n     * @param displayMode How to display the pattern.\n     * @param pattern     The pattern.\n     */\n    public void setPattern(DisplayMode displayMode, List<Cell> pattern) {\n        mPattern.clear();\n        mPattern.addAll(pattern);\n        clearPatternDrawLookup();\n        for (Cell cell : pattern) {\n            mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;\n        }\n\n        setDisplayMode(displayMode);\n    }\n\n    /**\n     * Set the display mode of the current pattern.  This can be useful, for\n     * instance, after detecting a pattern to tell this view whether change the\n     * in progress result to correct or wrong.\n     *\n     * @param displayMode The display mode.\n     */\n    public void setDisplayMode(DisplayMode displayMode) {\n        mPatternDisplayMode = displayMode;\n        if (displayMode == DisplayMode.Animate) {\n            if (mPattern.isEmpty()) {\n                throw new IllegalStateException(\"you must have a pattern to \"\n                        + \"animate if you want to set the display mode to animate\");\n            }\n            mAnimatingPeriodStart = SystemClock.elapsedRealtime();\n            //noinspection SequencedCollectionMethodCanBeUsed\n            final Cell first = mPattern.get(0);\n            mInProgressX = getCenterXForColumn(first.getColumn());\n            mInProgressY = getCenterYForRow(first.getRow());\n            clearPatternDrawLookup();\n        }\n        invalidate();\n    }\n\n    private void notifyCellAdded() {\n        if (mOnPatternListener != null) {\n            mOnPatternListener.onPatternCellAdded(mPattern);\n        }\n    }\n\n    private void notifyPatternStarted() {\n        if (mOnPatternListener != null) {\n            mOnPatternListener.onPatternStart();\n        }\n    }\n\n    private void notifyPatternDetected() {\n        if (mOnPatternListener != null) {\n            mOnPatternListener.onPatternDetected(mPattern);\n        }\n    }\n\n    private void notifyPatternCleared() {\n        if (mOnPatternListener != null) {\n            mOnPatternListener.onPatternCleared();\n        }\n    }\n\n    /**\n     * Reset all pattern state.\n     */\n    private void resetPattern() {\n        mPattern.clear();\n        clearPatternDrawLookup();\n        mPatternDisplayMode = DisplayMode.Correct;\n        invalidate();\n    }\n\n    /**\n     * Clear the pattern lookup table.\n     */\n    private void clearPatternDrawLookup() {\n        for (int i = 0; i < 3; i++) {\n            for (int j = 0; j < 3; j++) {\n                mPatternDrawLookup[i][j] = false;\n            }\n        }\n    }\n\n    @Override\n    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\n        final int width = w - getPaddingLeft() - getPaddingRight();\n        mSquareWidth = width / 3.0f;\n\n        final int height = h - getPaddingTop() - getPaddingBottom();\n        mSquareHeight = height / 3.0f;\n    }\n\n    @SuppressLint(\"SwitchIntDef\")\n    private int resolveMeasured(int measureSpec, int desired) {\n        int specSize = MeasureSpec.getSize(measureSpec);\n        return switch (MeasureSpec.getMode(measureSpec)) {\n            case MeasureSpec.UNSPECIFIED -> desired;\n            case MeasureSpec.AT_MOST -> Math.max(specSize, desired);\n            default -> specSize;\n        };\n    }\n\n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        final int minimumWidth = getSuggestedMinimumWidth();\n        final int minimumHeight = getSuggestedMinimumHeight();\n        int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);\n        int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);\n\n        switch (mAspect) {\n            case ASPECT_SQUARE:\n                viewWidth = viewHeight = Math.min(viewWidth, viewHeight);\n                break;\n            case ASPECT_LOCK_WIDTH:\n                viewHeight = Math.min(viewWidth, viewHeight);\n                break;\n            case ASPECT_LOCK_HEIGHT:\n                viewWidth = Math.min(viewWidth, viewHeight);\n                break;\n        }\n        // Log.v(TAG, \"LockPatternView dimensions: \" + viewWidth + \"x\" + viewHeight);\n        setMeasuredDimension(viewWidth, viewHeight);\n    }\n\n    // From\n    // https://github.com/google/guava/blob/c462d69329709f72a17a64cb229d15e76e72199c/guava/src/com/google/common/math/IntMath.java\n\n    /**\n     * Determines whether the point x, y will add a new point to the current\n     * pattern (in addition to finding the cell, also makes heuristic choices\n     * such as filling in gaps based on current pattern).\n     *\n     * @param x The x coordinate.\n     * @param y The y coordinate.\n     */\n    private Cell detectAndAddHit(float x, float y) {\n        final Cell cell = checkForNewHit(x, y);\n        if (cell != null) {\n            // check for gaps in existing pattern\n            final ArrayList<Cell> pattern = mPattern;\n            if (!pattern.isEmpty()) {\n                //noinspection SequencedCollectionMethodCanBeUsed\n                final Cell lastCell = pattern.get(pattern.size() - 1);\n                int dRow = cell.row - lastCell.row;\n                int dColumn = cell.column - lastCell.column;\n                int dGcd = gcd(Math.abs(dRow), Math.abs(dColumn));\n                if (dGcd > 0) {\n                    int fillInRow = lastCell.row;\n                    int fillInColumn = lastCell.column;\n                    int fillInRowStep = dRow / dGcd;\n                    int fillInColumnStep = dColumn / dGcd;\n                    for (int i = 1; i < dGcd; ++i) {\n                        fillInRow += fillInRowStep;\n                        fillInColumn += fillInColumnStep;\n                        if (!mPatternDrawLookup[fillInRow][fillInColumn]) {\n                            addCellToPattern(mCells[fillInRow][fillInColumn]);\n                        }\n                    }\n                }\n            }\n            addCellToPattern(cell);\n            performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,\n                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING\n                            | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);\n\n            return cell;\n        }\n        return null;\n    }\n\n    private void addCellToPattern(Cell newCell) {\n        mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;\n        mPattern.add(newCell);\n        if (!mInStealthMode) {\n            startCellActivatedAnimation(newCell);\n        }\n        notifyCellAdded();\n    }\n\n    private void startCellActivatedAnimation(Cell cell) {\n        final CellState cellState = mCellStates[cell.row][cell.column];\n        startRadiusAnimation(mDotSize / 2, mDotSizeActivated / 2, 96, mLinearOutSlowInInterpolator,\n                cellState, () -> startRadiusAnimation(mDotSizeActivated / 2, mDotSize / 2, 192,\n                        mFastOutSlowInInterpolator,\n                        cellState, null));\n        startLineEndAnimation(cellState, mInProgressX, mInProgressY,\n                getCenterXForColumn(cell.column), getCenterYForRow(cell.row));\n    }\n\n    private void startLineEndAnimation(final CellState state,\n                                       final float startX, final float startY, final float targetX, final float targetY) {\n        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);\n        valueAnimator.addUpdateListener(animation -> {\n            float t = (float) animation.getAnimatedValue();\n            state.lineEndX = (1 - t) * startX + t * targetX;\n            state.lineEndY = (1 - t) * startY + t * targetY;\n            invalidate();\n        });\n        valueAnimator.addListener(new AnimatorListenerAdapter() {\n            @Override\n            public void onAnimationEnd(Animator animation) {\n                state.lineAnimator = null;\n            }\n        });\n        valueAnimator.setInterpolator(mFastOutSlowInInterpolator);\n        valueAnimator.setDuration(100);\n        valueAnimator.start();\n        state.lineAnimator = valueAnimator;\n    }\n\n    private void startRadiusAnimation(float start, float end, long duration,\n                                      Interpolator interpolator, final CellState state,\n                                      final Runnable endRunnable) {\n        ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);\n        valueAnimator.addUpdateListener(animation -> {\n            state.radius = (float) animation.getAnimatedValue();\n            invalidate();\n        });\n        if (endRunnable != null) {\n            valueAnimator.addListener(new AnimatorListenerAdapter() {\n                @Override\n                public void onAnimationEnd(Animator animation) {\n                    endRunnable.run();\n                }\n            });\n        }\n        valueAnimator.setInterpolator(interpolator);\n        valueAnimator.setDuration(duration);\n        valueAnimator.start();\n    }\n\n    // helper method to find which cell a point maps to\n    private Cell checkForNewHit(float x, float y) {\n        final int rowHit = getRowHit(y);\n        if (rowHit < 0) {\n            return null;\n        }\n        final int columnHit = getColumnHit(x);\n        if (columnHit < 0) {\n            return null;\n        }\n\n        if (mPatternDrawLookup[rowHit][columnHit]) {\n            return null;\n        }\n        return mCells[rowHit][columnHit];\n    }\n\n    /**\n     * Helper method to find the row that y falls into.\n     *\n     * @param y The y coordinate\n     * @return The row that y falls in, or -1 if it falls in no row.\n     */\n    private int getRowHit(float y) {\n        final float squareHeight = mSquareHeight;\n        float hitSize = squareHeight * mHitFactor;\n\n        float offset = getPaddingTop() + (squareHeight - hitSize) / 2f;\n        for (int i = 0; i < 3; i++) {\n            final float hitTop = offset + squareHeight * i;\n            if (y >= hitTop && y <= hitTop + hitSize) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    /**\n     * Helper method to find the column x fallis into.\n     *\n     * @param x The x coordinate.\n     * @return The column that x falls in, or -1 if it falls in no column.\n     */\n    private int getColumnHit(float x) {\n        final float squareWidth = mSquareWidth;\n        float hitSize = squareWidth * mHitFactor;\n\n        float offset = getPaddingLeft() + (squareWidth - hitSize) / 2f;\n        for (int i = 0; i < 3; i++) {\n            final float hitLeft = offset + squareWidth * i;\n            if (x >= hitLeft && x <= hitLeft + hitSize) {\n                return i;\n            }\n        }\n        return -1;\n    }\n\n    @Override\n    public boolean onHoverEvent(@NonNull MotionEvent event) {\n        AccessibilityManager accessibilityManager =\n                (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);\n        if (accessibilityManager.isTouchExplorationEnabled()) {\n            final int action = event.getAction();\n            switch (action) {\n                case MotionEvent.ACTION_HOVER_ENTER:\n                    event.setAction(MotionEvent.ACTION_DOWN);\n                    break;\n                case MotionEvent.ACTION_HOVER_MOVE:\n                    event.setAction(MotionEvent.ACTION_MOVE);\n                    break;\n                case MotionEvent.ACTION_HOVER_EXIT:\n                    event.setAction(MotionEvent.ACTION_UP);\n                    break;\n            }\n            onTouchEvent(event);\n            event.setAction(action);\n        }\n        return super.onHoverEvent(event);\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    @Override\n    public boolean onTouchEvent(@NonNull MotionEvent event) {\n        if (!mInputEnabled || !isEnabled()) {\n            return false;\n        }\n\n        switch (event.getAction()) {\n            case MotionEvent.ACTION_DOWN:\n                handleActionDown(event);\n                return true;\n            case MotionEvent.ACTION_UP:\n                handleActionUp();\n                return true;\n            case MotionEvent.ACTION_MOVE:\n                handleActionMove(event);\n                return true;\n            case MotionEvent.ACTION_CANCEL:\n                if (mPatternInProgress) {\n                    setPatternInProgress(false);\n                    resetPattern();\n                    notifyPatternCleared();\n                }\n                if (PROFILE_DRAWING) {\n                    if (mDrawingProfilingStarted) {\n                        Debug.stopMethodTracing();\n                        mDrawingProfilingStarted = false;\n                    }\n                }\n                return true;\n        }\n        return false;\n    }\n\n    private void setPatternInProgress(boolean progress) {\n        mPatternInProgress = progress;\n    }\n\n    private void handleActionMove(MotionEvent event) {\n        // Handle all recent motion events so we don't skip any cells even when the device\n        // is busy...\n        final float radius = mPathWidth;\n        final int historySize = event.getHistorySize();\n        mTmpInvalidateRect.setEmpty();\n        boolean invalidateNow = false;\n        for (int i = 0; i < historySize + 1; i++) {\n            final float x = i < historySize ? event.getHistoricalX(i) : event.getX();\n            final float y = i < historySize ? event.getHistoricalY(i) : event.getY();\n            Cell hitCell = detectAndAddHit(x, y);\n            final int patternSize = mPattern.size();\n            if (hitCell != null && patternSize == 1) {\n                setPatternInProgress(true);\n                notifyPatternStarted();\n            }\n            // note current x and y for rubber banding of in progress patterns\n            final float dx = Math.abs(x - mInProgressX);\n            final float dy = Math.abs(y - mInProgressY);\n            if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {\n                invalidateNow = true;\n            }\n\n            if (mPatternInProgress && patternSize > 0) {\n                final Cell lastCell = mPattern.get(patternSize - 1);\n                float lastCellCenterX = getCenterXForColumn(lastCell.column);\n                float lastCellCenterY = getCenterYForRow(lastCell.row);\n\n                // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.\n                float left = Math.min(lastCellCenterX, x) - radius;\n                float right = Math.max(lastCellCenterX, x) + radius;\n                float top = Math.min(lastCellCenterY, y) - radius;\n                float bottom = Math.max(lastCellCenterY, y) + radius;\n\n                // Invalidate between the pattern's new cell and the pattern's previous cell\n                if (hitCell != null) {\n                    final float width = mSquareWidth * 0.5f;\n                    final float height = mSquareHeight * 0.5f;\n                    final float hitCellCenterX = getCenterXForColumn(hitCell.column);\n                    final float hitCellCenterY = getCenterYForRow(hitCell.row);\n\n                    left = Math.min(hitCellCenterX - width, left);\n                    right = Math.max(hitCellCenterX + width, right);\n                    top = Math.min(hitCellCenterY - height, top);\n                    bottom = Math.max(hitCellCenterY + height, bottom);\n                }\n\n                // Invalidate between the pattern's last cell and the previous location\n                mTmpInvalidateRect.union(Math.round(left), Math.round(top),\n                        Math.round(right), Math.round(bottom));\n            }\n        }\n        mInProgressX = event.getX();\n        mInProgressY = event.getY();\n\n        // To save updates, we only invalidate if the user moved beyond a certain amount.\n        if (invalidateNow) {\n            mInvalidate.union(mTmpInvalidateRect);\n            invalidate(mInvalidate);\n            mInvalidate.set(mTmpInvalidateRect);\n        }\n    }\n\n    private void handleActionUp() {\n        // report pattern detected\n        if (!mPattern.isEmpty()) {\n            setPatternInProgress(false);\n            cancelLineAnimations();\n            notifyPatternDetected();\n            invalidate();\n        }\n        if (PROFILE_DRAWING) {\n            if (mDrawingProfilingStarted) {\n                Debug.stopMethodTracing();\n                mDrawingProfilingStarted = false;\n            }\n        }\n    }\n\n    private void cancelLineAnimations() {\n        for (int i = 0; i < 3; i++) {\n            for (int j = 0; j < 3; j++) {\n                CellState state = mCellStates[i][j];\n                if (state.lineAnimator != null) {\n                    state.lineAnimator.cancel();\n                    state.lineEndX = Float.MIN_VALUE;\n                    state.lineEndY = Float.MIN_VALUE;\n                }\n            }\n        }\n    }\n\n    private void handleActionDown(MotionEvent event) {\n        resetPattern();\n        final float x = event.getX();\n        final float y = event.getY();\n        final Cell hitCell = detectAndAddHit(x, y);\n        if (hitCell != null) {\n            setPatternInProgress(true);\n            mPatternDisplayMode = DisplayMode.Correct;\n            notifyPatternStarted();\n        } else if (mPatternInProgress) {\n            setPatternInProgress(false);\n            notifyPatternCleared();\n        }\n        if (hitCell != null) {\n            final float startX = getCenterXForColumn(hitCell.column);\n            final float startY = getCenterYForRow(hitCell.row);\n\n            final float widthOffset = mSquareWidth / 2f;\n            final float heightOffset = mSquareHeight / 2f;\n\n            invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),\n                    (int) (startX + widthOffset), (int) (startY + heightOffset));\n        }\n        mInProgressX = x;\n        mInProgressY = y;\n        if (PROFILE_DRAWING) {\n            if (!mDrawingProfilingStarted) {\n                Debug.startMethodTracing(\"LockPatternDrawing\");\n                mDrawingProfilingStarted = true;\n            }\n        }\n    }\n\n    private float getCenterXForColumn(int column) {\n        return getPaddingLeft() + column * mSquareWidth + mSquareWidth / 2f;\n    }\n\n    private float getCenterYForRow(int row) {\n        return getPaddingTop() + row * mSquareHeight + mSquareHeight / 2f;\n    }\n\n    @Override\n    protected void onDraw(@NonNull Canvas canvas) {\n        final ArrayList<Cell> pattern = mPattern;\n        final int count = pattern.size();\n        final boolean[][] drawLookup = mPatternDrawLookup;\n\n        if (mPatternDisplayMode == DisplayMode.Animate) {\n            // figure out which circles to draw\n\n            // + 1 so we pause on complete pattern\n            final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;\n            final int spotInCycle = (int) (SystemClock.elapsedRealtime() -\n                    mAnimatingPeriodStart) % oneCycle;\n            final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;\n\n            clearPatternDrawLookup();\n            for (int i = 0; i < numCircles; i++) {\n                final Cell cell = pattern.get(i);\n                drawLookup[cell.getRow()][cell.getColumn()] = true;\n            }\n\n            // figure out in progress portion of ghosting line\n\n            final boolean needToUpdateInProgressPoint = numCircles > 0\n                    && numCircles < count;\n\n            if (needToUpdateInProgressPoint) {\n                final float percentageOfNextCircle =\n                        ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /\n                                MILLIS_PER_CIRCLE_ANIMATING;\n\n                final Cell currentCell = pattern.get(numCircles - 1);\n                final float centerX = getCenterXForColumn(currentCell.column);\n                final float centerY = getCenterYForRow(currentCell.row);\n\n                final Cell nextCell = pattern.get(numCircles);\n                final float dx = percentageOfNextCircle *\n                        (getCenterXForColumn(nextCell.column) - centerX);\n                final float dy = percentageOfNextCircle *\n                        (getCenterYForRow(nextCell.row) - centerY);\n                mInProgressX = centerX + dx;\n                mInProgressY = centerY + dy;\n            }\n            // TODO: Infinite loop here...\n            invalidate();\n        }\n\n        final Path currentPath = mCurrentPath;\n        currentPath.rewind();\n\n        // draw the circles\n        for (int i = 0; i < 3; i++) {\n            float centerY = getCenterYForRow(i);\n            for (int j = 0; j < 3; j++) {\n                CellState cellState = mCellStates[i][j];\n                float centerX = getCenterXForColumn(j);\n                float translationY = cellState.translationY;\n                drawCircle(canvas, (int) centerX, (int) centerY + translationY, cellState.radius,\n                        drawLookup[i][j], cellState.alpha);\n            }\n        }\n\n        // TODO: the path should be created and cached every time we hit-detect a cell\n        // only the last segment of the path should be computed here\n        // draw the path of the pattern (unless we are in stealth mode)\n        final boolean drawPath = !mInStealthMode;\n\n        if (drawPath) {\n            mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));\n            // Anyway other drawing sets their own alpha ignoring the original; And in this way we\n            // can use ?colorControlNormal better.\n            mPathPaint.setAlpha(255);\n\n            boolean anyCircles = false;\n            float lastX = 0f;\n            float lastY = 0f;\n            for (int i = 0; i < count; i++) {\n                Cell cell = pattern.get(i);\n\n                // only draw the part of the pattern stored in\n                // the lookup table (this is only different in the case\n                // of animation).\n                if (!drawLookup[cell.row][cell.column]) {\n                    break;\n                }\n                anyCircles = true;\n\n                float centerX = getCenterXForColumn(cell.column);\n                float centerY = getCenterYForRow(cell.row);\n                if (i != 0) {\n                    CellState state = mCellStates[cell.row][cell.column];\n                    currentPath.rewind();\n                    currentPath.moveTo(lastX, lastY);\n                    if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {\n                        currentPath.lineTo(state.lineEndX, state.lineEndY);\n                    } else {\n                        currentPath.lineTo(centerX, centerY);\n                    }\n                    canvas.drawPath(currentPath, mPathPaint);\n                }\n                lastX = centerX;\n                lastY = centerY;\n            }\n\n            // draw last in progress section\n            if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)\n                    && anyCircles) {\n                currentPath.rewind();\n                currentPath.moveTo(lastX, lastY);\n                currentPath.lineTo(mInProgressX, mInProgressY);\n\n                mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(\n                        mInProgressX, mInProgressY, lastX, lastY) * 255f));\n                canvas.drawPath(currentPath, mPathPaint);\n            }\n        }\n    }\n\n    private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {\n        float diffX = x - lastX;\n        float diffY = y - lastY;\n        float dist = (float) Math.sqrt(diffX * diffX + diffY * diffY);\n        float frac = dist / mSquareWidth;\n        return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));\n    }\n\n    private int getCurrentColor(boolean partOfPattern) {\n        if (!partOfPattern || mInStealthMode || mPatternInProgress) {\n            // unselected circle\n            return mRegularColor;\n        } else if (mPatternDisplayMode == DisplayMode.Wrong) {\n            // the pattern is wrong\n            return mErrorColor;\n        } else if (mPatternDisplayMode == DisplayMode.Correct ||\n                mPatternDisplayMode == DisplayMode.Animate) {\n            return mSuccessColor;\n        } else {\n            throw new IllegalStateException(\"unknown display mode \" + mPatternDisplayMode);\n        }\n    }\n\n    /**\n     * @param partOfPattern Whether this circle is part of the pattern.\n     */\n    private void drawCircle(Canvas canvas, float centerX, float centerY, float radius,\n                            boolean partOfPattern, float alpha) {\n        mPaint.setColor(getCurrentColor(partOfPattern));\n        mPaint.setAlpha((int) (alpha * 255));\n        canvas.drawCircle(centerX, centerY, radius, mPaint);\n    }\n\n    @Override\n    protected Parcelable onSaveInstanceState() {\n        Parcelable superState = super.onSaveInstanceState();\n        return new SavedState(superState,\n                LockPatternUtils.patternToString(mPattern),\n                mPatternDisplayMode.ordinal(),\n                mInputEnabled, mInStealthMode);\n    }\n\n    @Override\n    protected void onRestoreInstanceState(Parcelable state) {\n        final SavedState ss = (SavedState) state;\n        super.onRestoreInstanceState(ss.getSuperState());\n        setPattern(\n                DisplayMode.Correct,\n                LockPatternUtils.stringToPattern(ss.getSerializedPattern()));\n        mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];\n        mInputEnabled = ss.isInputEnabled();\n        mInStealthMode = ss.isInStealthMode();\n    }\n\n    /**\n     * How to display the current pattern.\n     */\n    public enum DisplayMode {\n        /**\n         * The pattern drawn is correct (i.e draw it in a friendly color)\n         */\n        Correct,\n\n        /**\n         * Animate the pattern (for demo, and help).\n         */\n        Animate,\n\n        /**\n         * The pattern is wrong (i.e draw a foreboding color)\n         */\n        Wrong\n    }\n\n    /**\n     * The call back interface for detecting patterns entered by the user.\n     */\n    public interface OnPatternListener {\n        /**\n         * A new pattern has begun.\n         */\n        void onPatternStart();\n\n        /**\n         * The pattern was cleared.\n         */\n        void onPatternCleared();\n\n        /**\n         * The user extended the pattern currently being drawn by one cell.\n         *\n         * @param pattern The pattern with newly added cell.\n         */\n        void onPatternCellAdded(List<Cell> pattern);\n\n        /**\n         * A pattern was detected from the user.\n         *\n         * @param pattern The pattern.\n         */\n        void onPatternDetected(List<Cell> pattern);\n    }\n\n    /**\n     * Represents a cell in the 3 X 3 matrix of the unlock pattern view.\n     */\n    public static class Cell {\n        // keep # objects limited to 9\n        static Cell[][] sCells = new Cell[3][3];\n\n        static {\n            for (int i = 0; i < 3; i++) {\n                for (int j = 0; j < 3; j++) {\n                    sCells[i][j] = new Cell(i, j);\n                }\n            }\n        }\n\n        int row;\n        int column;\n\n        /**\n         * @param row    The row of the cell.\n         * @param column The column of the cell.\n         */\n        private Cell(int row, int column) {\n            checkRange(row, column);\n            this.row = row;\n            this.column = column;\n        }\n\n        /**\n         * @param row    The row of the cell.\n         * @param column The column of the cell.\n         */\n        public static synchronized Cell of(int row, int column) {\n            checkRange(row, column);\n            return sCells[row][column];\n        }\n\n        private static void checkRange(int row, int column) {\n            if (row < 0 || row > 2) {\n                throw new IllegalArgumentException(\"row must be in range 0-2\");\n            }\n            if (column < 0 || column > 2) {\n                throw new IllegalArgumentException(\"column must be in range 0-2\");\n            }\n        }\n\n        public int getRow() {\n            return row;\n        }\n\n        public int getColumn() {\n            return column;\n        }\n\n        @NonNull\n        public String toString() {\n            return \"(row=\" + row + \",clmn=\" + column + \")\";\n        }\n    }\n\n    public static class CellState {\n        public float lineEndX = Float.MIN_VALUE;\n        public float lineEndY = Float.MIN_VALUE;\n        public ValueAnimator lineAnimator;\n        int row;\n        int col;\n        float radius;\n        float translationY;\n        float alpha = 1f;\n    }\n\n    /**\n     * The parecelable for saving and restoring a lock pattern view.\n     */\n    private static class SavedState extends BaseSavedState {\n        public static final Creator<SavedState> CREATOR =\n                new Creator<>() {\n                    @Override\n                    public SavedState createFromParcel(Parcel in) {\n                        return new SavedState(in);\n                    }\n\n                    @Override\n                    public SavedState[] newArray(int size) {\n                        return new SavedState[size];\n                    }\n                };\n        private final String mSerializedPattern;\n        private final int mDisplayMode;\n        private final boolean mInputEnabled;\n        private final boolean mInStealthMode;\n\n        /**\n         * Constructor called from {@link LockPatternView#onSaveInstanceState()}\n         */\n        private SavedState(Parcelable superState, String serializedPattern, int displayMode,\n                           boolean inputEnabled, boolean inStealthMode) {\n            super(superState);\n            mSerializedPattern = serializedPattern;\n            mDisplayMode = displayMode;\n            mInputEnabled = inputEnabled;\n            mInStealthMode = inStealthMode;\n        }\n\n        /**\n         * Constructor called from {@link #CREATOR}\n         */\n        @SuppressLint(\"ParcelClassLoader\")\n        private SavedState(Parcel in) {\n            super(in);\n            mSerializedPattern = in.readString();\n            mDisplayMode = in.readInt();\n            mInputEnabled = (Boolean) in.readValue(null);\n            mInStealthMode = (Boolean) in.readValue(null);\n        }\n\n        public String getSerializedPattern() {\n            return mSerializedPattern;\n        }\n\n        public int getDisplayMode() {\n            return mDisplayMode;\n        }\n\n        public boolean isInputEnabled() {\n            return mInputEnabled;\n        }\n\n        public boolean isInStealthMode() {\n            return mInStealthMode;\n        }\n\n        @Override\n        public void writeToParcel(@NonNull Parcel dest, int flags) {\n            super.writeToParcel(dest, flags);\n            dest.writeString(mSerializedPattern);\n            dest.writeInt(mDisplayMode);\n            dest.writeValue(mInputEnabled);\n            dest.writeValue(mInStealthMode);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/recyclerview/AutoGridLayoutManager.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget.recyclerview;\n\nimport android.content.Context;\n\nimport androidx.recyclerview.widget.GridLayoutManager;\nimport androidx.recyclerview.widget.RecyclerView;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class AutoGridLayoutManager extends GridLayoutManager {\n    public static final int STRATEGY_SUITABLE_SIZE = 1;\n\n    private int mColumnSize = -1;\n    private boolean mColumnSizeChanged = true;\n    private int mStrategy;\n    private int fakePadding;\n\n    private List<OnUpdateSpanCountListener> mListeners;\n\n    public AutoGridLayoutManager(Context context, int columnSize, int fakePadding) {\n        super(context, 1);\n        this.fakePadding = fakePadding;\n        setColumnSize(columnSize);\n    }\n\n    public AutoGridLayoutManager(Context context, int columnSize, int orientation, boolean reverseLayout) {\n        super(context, 1, orientation, reverseLayout);\n        setColumnSize(columnSize);\n    }\n\n    public static int getSpanCountForSuitableSize(int total, int single) {\n        int span = total / single;\n        if (span <= 0) {\n            return 1;\n        }\n        int span2 = span + 1;\n        float deviation = Math.abs(1 - ((float) total / span / (float) single));\n        float deviation2 = Math.abs(1 - ((float) total / span2 / (float) single));\n        return deviation < deviation2 ? span : span2;\n    }\n\n    public static int getSpanCountForMinSize(int total, int single) {\n        return Math.max(1, total / single);\n    }\n\n    public void setColumnSize(int columnSize) {\n        if (columnSize == mColumnSize) {\n            return;\n        }\n        mColumnSize = columnSize;\n        mColumnSizeChanged = true;\n    }\n\n    public void setStrategy(int strategy) {\n        if (strategy == mStrategy) {\n            return;\n        }\n        mStrategy = strategy;\n        mColumnSizeChanged = true;\n    }\n\n    @Override\n    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {\n        if (mColumnSizeChanged && mColumnSize > 0) {\n            int totalSpace;\n            if (getOrientation() == RecyclerView.VERTICAL) {\n                totalSpace = getWidth() - getPaddingRight() - getPaddingLeft() - fakePadding;\n            } else {\n                totalSpace = getHeight() - getPaddingTop() - getPaddingBottom() - fakePadding;\n            }\n\n            int spanCount;\n            if (mStrategy == STRATEGY_SUITABLE_SIZE) {\n                spanCount = getSpanCountForSuitableSize(totalSpace, mColumnSize);\n            } else {\n                spanCount = getSpanCountForMinSize(totalSpace, mColumnSize);\n            }\n            setSpanCount(spanCount);\n            mColumnSizeChanged = false;\n\n            if (null != mListeners) {\n                for (int i = 0, n = mListeners.size(); i < n; i++) {\n                    mListeners.get(i).onUpdateSpanCount(spanCount);\n                }\n            }\n        }\n        super.onLayoutChildren(recycler, state);\n    }\n\n    public void addOnUpdateSpanCountListener(OnUpdateSpanCountListener listener) {\n        if (null == mListeners) {\n            mListeners = new ArrayList<>();\n        }\n        mListeners.add(listener);\n    }\n\n    public void removeOnUpdateSpanCountListener(OnUpdateSpanCountListener listener) {\n        if (null != mListeners) {\n            mListeners.remove(listener);\n        }\n    }\n\n    public interface OnUpdateSpanCountListener {\n        void onUpdateSpanCount(int spanCount);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/widget/recyclerview/AutoStaggeredGridLayoutManager.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.widget.recyclerview;\n\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.recyclerview.widget.RecyclerView;\nimport androidx.recyclerview.widget.StaggeredGridLayoutManager;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class AutoStaggeredGridLayoutManager extends StaggeredGridLayoutManager {\n    public static final int STRATEGY_MIN_SIZE = 0;\n    public static final int STRATEGY_SUITABLE_SIZE = 1;\n\n    private int mColumnSize = -1;\n    private boolean mColumnSizeChanged = true;\n    private int mStrategy;\n\n    private List<OnUpdateSpanCountListener> mListeners;\n\n    public AutoStaggeredGridLayoutManager(int columnSize, int orientation) {\n        super(1, orientation);\n        setColumnSize(columnSize);\n    }\n\n    public static int getSpanCountForSuitableSize(int total, int single) {\n        int span = total / single;\n        if (span <= 0) {\n            return 1;\n        }\n        int span2 = span + 1;\n        float deviation = Math.abs(1 - ((float) total / span / (float) single));\n        float deviation2 = Math.abs(1 - ((float) total / span2 / (float) single));\n        return deviation < deviation2 ? span : span2;\n    }\n\n    public static int getSpanCountForMinSize(int total, int single) {\n        return Math.max(1, total / single);\n    }\n\n    public void setColumnSize(int columnSize) {\n        if (columnSize == mColumnSize) {\n            return;\n        }\n        mColumnSize = columnSize;\n        mColumnSizeChanged = true;\n    }\n\n    public void setStrategy(int strategy) {\n        if (strategy == mStrategy) {\n            return;\n        }\n        mStrategy = strategy;\n        mColumnSizeChanged = true;\n    }\n\n    @Override\n    public void onMeasure(@NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state, int widthSpec, int heightSpec) {\n        if (mColumnSizeChanged && mColumnSize > 0) {\n            int totalSpace;\n            if (getOrientation() == VERTICAL) {\n                if (View.MeasureSpec.EXACTLY != View.MeasureSpec.getMode(widthSpec)) {\n                    throw new IllegalStateException(\"RecyclerView need a fixed width for AutoStaggeredGridLayoutManager\");\n                }\n                totalSpace = View.MeasureSpec.getSize(widthSpec) - getPaddingRight() - getPaddingLeft();\n            } else {\n                if (View.MeasureSpec.EXACTLY != View.MeasureSpec.getMode(heightSpec)) {\n                    throw new IllegalStateException(\"RecyclerView need a fixed height for AutoStaggeredGridLayoutManager\");\n                }\n                totalSpace = View.MeasureSpec.getSize(heightSpec) - getPaddingTop() - getPaddingBottom();\n            }\n\n            int spanCount;\n            if (mStrategy == STRATEGY_SUITABLE_SIZE) {\n                spanCount = getSpanCountForSuitableSize(totalSpace, mColumnSize);\n            } else {\n                spanCount = getSpanCountForMinSize(totalSpace, mColumnSize);\n            }\n            setSpanCount(spanCount);\n            mColumnSizeChanged = false;\n\n            if (null != mListeners) {\n                for (int i = 0, n = mListeners.size(); i < n; i++) {\n                    mListeners.get(i).onUpdateSpanCount(spanCount);\n                }\n            }\n        }\n        super.onMeasure(recycler, state, widthSpec, heightSpec);\n    }\n\n    public void addOnUpdateSpanCountListener(OnUpdateSpanCountListener listener) {\n        if (null == mListeners) {\n            mListeners = new ArrayList<>();\n        }\n        mListeners.add(listener);\n    }\n\n    public void removeOnUpdateSpanCountListener(OnUpdateSpanCountListener listener) {\n        if (null != mListeners) {\n            mListeners.remove(listener);\n        }\n    }\n\n    public interface OnUpdateSpanCountListener {\n        void onUpdateSpanCount(int spanCount);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/AnimationUtils.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.view.animation.Interpolator;\n\nimport androidx.interpolator.view.animation.FastOutLinearInInterpolator;\nimport androidx.interpolator.view.animation.FastOutSlowInInterpolator;\nimport androidx.interpolator.view.animation.LinearOutSlowInInterpolator;\n\npublic final class AnimationUtils {\n    public static final Interpolator FAST_SLOW_INTERPOLATOR = new LinearOutSlowInInterpolator();\n    public static final Interpolator SLOW_FAST_INTERPOLATOR = new FastOutLinearInInterpolator();\n    public static final Interpolator SLOW_FAST_SLOW_INTERPOLATOR = new FastOutSlowInInterpolator();\n\n    private AnimationUtils() {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/AssertError.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\npublic class AssertError extends Error {\n    public AssertError(String detailMessage) {\n        super(detailMessage);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/AssertException.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\npublic class AssertException extends Exception {\n    public AssertException(String detailMessage) {\n        super(detailMessage);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/AssertUtils.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\npublic final class AssertUtils {\n    private AssertUtils() {\n    }\n\n    public static void assertTrue(boolean cond) {\n        assertTrue(\"Condition has to be true.\", cond);\n    }\n\n    public static void assertTrueEx(boolean cond) throws AssertException {\n        assertTrueEx(\"Condition has to be true.\", cond);\n    }\n\n    public static void assertTrue(String message, boolean cond) {\n        if (!cond) {\n            throw new AssertError(message);\n        }\n    }\n\n    public static void assertTrueEx(String message, boolean cond) throws AssertException {\n        if (!cond) {\n            throw new AssertException(message);\n        }\n    }\n\n    public static void assertNull(Object object) {\n        assertNull(\"Should be null\", object);\n    }\n\n    public static void assertNullEx(Object object) throws AssertException {\n        assertNullEx(\"Should be null\", object);\n    }\n\n    public static void assertNull(String message, Object object) {\n        if (object != null) {\n            throw new AssertError(message);\n        }\n    }\n\n    public static void assertNullEx(String message, Object object) throws AssertException {\n        if (object != null) {\n            throw new AssertException(message);\n        }\n    }\n\n    public static void assertNotNull(Object object) {\n        assertNotNull(\"Should not be null\", object);\n    }\n\n    public static void assertNotNullEx(Object object) throws AssertException {\n        assertNotNullEx(\"Should not be null\", object);\n    }\n\n    public static void assertNotNull(String message, Object object) {\n        if (object == null) {\n            throw new AssertError(message);\n        }\n    }\n\n    public static void assertNotNullEx(String message, Object object) throws AssertException {\n        if (object == null) {\n            throw new AssertException(message);\n        }\n    }\n\n    public static void assertEquals(int expected, int actual) {\n        assertEquals(\"Should be \" + expected + \", but it is \" + actual, expected, actual);\n    }\n\n    public static void assertEqualsEx(int expected, int actual) throws AssertException {\n        assertEqualsEx(\"Should be \" + expected + \", but it is \" + actual, expected, actual);\n    }\n\n    public static void assertEquals(String message, int expected, int actual) {\n        if (expected != actual) {\n            throw new AssertError(message);\n        }\n    }\n\n    public static void assertEqualsEx(String message, int expected, int actual) throws AssertException {\n        if (expected != actual) {\n            throw new AssertException(message);\n        }\n    }\n\n    public static void assertInstanceOf(Object obj, Class<?> clazz) {\n        assertInstanceOf(\"The object should be instance of \" + clazz.getName(), obj, clazz);\n    }\n\n    public static void assertInstanceOfEx(Object obj, Class<?> clazz) throws AssertException {\n        assertInstanceOfEx(\"The object should be instance of \" + clazz.getName(), obj, clazz);\n    }\n\n    public static void assertInstanceOf(String message, Object obj, Class<?> clazz) {\n        if (!clazz.isInstance(obj)) {\n            throw new AssertError(message);\n        }\n    }\n\n    public static void assertInstanceOfEx(String message, Object obj, Class<?> clazz) throws AssertException {\n        if (!clazz.isInstance(obj)) {\n            throw new AssertException(message);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/ConcurrentPool.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.yorozuya;\n\npublic class ConcurrentPool<T> {\n    private final T[] mArray;\n    private final int mMaxSize;\n    private int mSize;\n\n    @SuppressWarnings(\"unchecked\")\n    public ConcurrentPool(int size) {\n        if (size <= 0) {\n            throw new IllegalStateException(\"Pool size must > 0, it is \" + size);\n        }\n        mArray = (T[]) new Object[size];\n        mMaxSize = size;\n        mSize = 0;\n    }\n\n    public synchronized void push(T t) {\n        if (t != null && mSize < mMaxSize) {\n            mArray[mSize++] = t;\n        }\n    }\n\n    public synchronized T pop() {\n        if (mSize > 0) {\n            T t = mArray[--mSize];\n            mArray[mSize] = null;\n            return t;\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/FileUtils.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\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.nio.charset.StandardCharsets;\nimport java.util.Locale;\n\npublic final class FileUtils {\n    private static final char[] FORBIDDEN_FILENAME_CHARACTERS = {\n            '\\\\',\n            '/',\n            ':',\n            '*',\n            '?',\n            '\"',\n            '<',\n            '>',\n            '|',\n    };\n\n    private FileUtils() {\n    }\n\n    public static boolean ensureFile(File file) {\n        return file != null && (!file.exists() || file.isFile());\n    }\n\n    public static boolean ensureDirectory(File file) {\n        if (file != null) {\n            if (file.exists()) {\n                return file.isDirectory();\n            } else {\n                return file.mkdirs();\n            }\n        } else {\n            return false;\n        }\n    }\n\n    /**\n     * Convert byte to human readable string.<br/>\n     * <a href=\"http://stackoverflow.com/questions/3758606/\">...</a>\n     *\n     * @param bytes the bytes to convert\n     * @param si    si units\n     * @return the human readable string\n     */\n    public static String humanReadableByteCount(long bytes, boolean si) {\n        int unit = si ? 1000 : 1024;\n        if (bytes < unit) return bytes + \" B\";\n        int exp = (int) (Math.log(bytes) / Math.log(unit));\n        String pre = (si ? \"kMGTPE\" : \"KMGTPE\").charAt(exp - 1) + (si ? \"\" : \"i\");\n        return String.format(Locale.US, \"%.1f %sB\", bytes / Math.pow(unit, exp), pre);\n    }\n\n    /**\n     * Try to delete file, dir and it's children\n     *\n     * @param file the file to delete\n     *             The dir to deleted\n     */\n    public static boolean delete(File file) {\n        if (file == null) {\n            return false;\n        }\n        boolean success = true;\n        File[] files = file.listFiles();\n        if (files != null) {\n            for (File f : files) {\n                success &= delete(f);\n            }\n        }\n        /*\n        final File to = new File(file.getAbsolutePath()\n                + System.currentTimeMillis());\n        if (file.renameTo(to)) {\n            success &= to.delete();\n        } else\n            success &= file.delete();\n        }\n        */\n        success &= file.delete();\n        return success;\n    }\n\n    public static boolean deleteContent(File file) {\n        if (file == null) {\n            return false;\n        }\n\n        boolean success = true;\n        File[] files = file.listFiles();\n        if (files != null) {\n            for (File f : files) {\n                success &= delete(f);\n            }\n        }\n        return success;\n    }\n\n    /**\n     * @return {@code null} for get exception\n     */\n    @Nullable\n    public static String read(File file) {\n        if (file == null) {\n            return null;\n        }\n\n        InputStream is = null;\n        try {\n            is = new FileInputStream(file);\n            return IOUtils.readString(is, \"utf-8\");\n        } catch (IOException e) {\n            Log.e(\"FileUtils\", \"Error reading file\", e);\n            return null;\n        } finally {\n            IOUtils.closeQuietly(is);\n        }\n    }\n\n    public static boolean write(File file, String str) {\n        if (file == null) {\n            return false;\n        }\n        if (str == null) {\n            return true;\n        }\n\n        OutputStream os = null;\n        try {\n            os = new FileOutputStream(file);\n            os.write(str.getBytes(StandardCharsets.UTF_8));\n            return true;\n        } catch (IOException e) {\n            Log.e(\"FileUtils\", \"Error writing file\", e);\n            return false;\n        } finally {\n            IOUtils.closeQuietly(os);\n        }\n    }\n\n    public static String sanitizeFilename(@NonNull String filename) {\n        // Remove forbidden_filename_characters\n        filename = StringUtils.remove(filename, FORBIDDEN_FILENAME_CHARACTERS);\n\n        // Ensure utf-8 byte count <= 255\n        int byteCount = 0;\n        int length = 0;\n        for (int len = filename.length(); length < len; length++) {\n            char ch = filename.charAt(length);\n            if (ch <= 0x7F) {\n                byteCount++;\n            } else if (ch <= 0x7FF) {\n                byteCount += 2;\n            } else if (Character.isHighSurrogate(ch)) {\n                byteCount += 4;\n                ++length;\n            } else {\n                byteCount += 3;\n            }\n            // Meet max byte count\n            if (byteCount > 255) {\n                filename = filename.substring(0, length);\n                break;\n            }\n        }\n\n        // Trim\n        return StringUtils.trim(filename);\n    }\n\n    /**\n     * Get extension from filename\n     *\n     * @param filename the complete filename\n     * @return null for can't find extension, \"\" empty String for ending with . dot\n     */\n    public static String getExtensionFromFilename(@Nullable String filename) {\n        if (null == filename) {\n            return null;\n        }\n\n        int index = filename.lastIndexOf('.');\n        if (index != -1) {\n            return filename.substring(index + 1);\n        } else {\n            return null;\n        }\n    }\n\n    /**\n     * Get name from filename\n     *\n     * @param filename the complete filename\n     * @return null for start with . dot\n     */\n    public static String getNameFromFilename(@Nullable String filename) {\n        if (null == filename) {\n            return null;\n        }\n\n        int index = filename.lastIndexOf('.');\n        if (index != -1) {\n            String name = filename.substring(0, index);\n            return TextUtils.isEmpty(name) ? null : name;\n        } else {\n            return filename;\n        }\n    }\n\n    /**\n     * Create a temp file, you need to delete it by you self.\n     *\n     * @param parent    The temp file's parent\n     * @param extension The extension of temp file\n     * @return The temp file or null\n     */\n    @Nullable\n    public static File createTempFile(@Nullable File parent, @Nullable String extension) {\n        if (parent == null) {\n            return null;\n        }\n\n        long now = System.currentTimeMillis();\n        for (int i = 0; i < 100; i++) {\n            String filename = Long.toString(now + i);\n            if (extension != null) {\n                filename = filename + '.' + extension;\n            }\n            File tempFile = new File(parent, filename);\n            if (!tempFile.exists()) {\n                return tempFile;\n            }\n        }\n\n        // Unbelievable\n        return null;\n    }\n\n    /**\n     * Create a temp dir, you need to delete it by you self.\n     *\n     * @param parent The temp file's parent\n     * @return The temp dir or null\n     */\n    @Nullable\n    public static File createTempDir(@Nullable File parent) {\n        if (parent == null) {\n            return null;\n        }\n\n        long now = System.currentTimeMillis();\n        for (int i = 0; i < 100; i++) {\n            String filename = Long.toString(now + i);\n            File tempFile = new File(parent, filename);\n            if (!tempFile.exists() && tempFile.mkdirs()) {\n                return tempFile;\n            }\n        }\n\n        // Unbelievable\n        return null;\n    }\n\n    /**\n     * Only support file now\n     * @noinspection ResultOfMethodCallIgnored\n     */\n    public static boolean rename(@NonNull File from, @NonNull File to) {\n        if (!from.isFile() || to.exists()) {\n            return false;\n        }\n\n        boolean ok = from.renameTo(to);\n        if (ok && !from.exists() && to.isFile()) {\n            return true;\n        }\n\n        // Copy content\n        InputStream is = null;\n        OutputStream os = null;\n        try {\n            is = new FileInputStream(from);\n            os = new FileOutputStream(to);\n            IOUtils.copy(is, os);\n            ok = true;\n        } catch (IOException e) {\n            IOUtils.closeQuietly(is);\n            IOUtils.closeQuietly(os);\n            ok = false;\n        }\n\n        if (!ok) {\n            to.delete();\n            return false;\n        }\n\n        // delete old one\n        from.delete();\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/IOUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.Closeable;\nimport java.io.EOFException;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\n\npublic final class IOUtils {\n    private static final int EOF = -1;\n    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;\n\n    private IOUtils() {\n    }\n\n    /**\n     * Close the closeable stuff. Don't worry about anything.\n     *\n     * @param is the closeable stuff\n     */\n    public static void closeQuietly(Closeable is) {\n        try {\n            if (is != null) {\n                is.close();\n            }\n        } catch (IOException e) {\n            // Ignore\n        }\n    }\n\n    /**\n     * Copy bytes from an <code>InputStream</code> to an\n     * <code>OutputStream</code>.\n     *\n     * @param input  the InputStream\n     * @param output the OutputStream\n     * @return the number of bytes copied\n     * @throws IOException the exception\n     */\n    public static long copy(InputStream input, OutputStream output)\n            throws IOException {\n        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];\n        long count = 0;\n        int n;\n        while (EOF != (n = input.read(buffer))) {\n            output.write(buffer, 0, n);\n            count += n;\n        }\n        return count;\n    }\n\n    /**\n     * Returns the ASCII characters up to but not including the next \"\\r\\n\", or\n     * \"\\n\".\n     *\n     * @throws java.io.EOFException if the stream is exhausted before the next\n     *                              newline character.\n     */\n    public static String readAsciiLine(final InputStream in) throws IOException {\n        final StringBuilder result = new StringBuilder(80);\n        while (true) {\n            final int c = in.read();\n            if (c == -1) {\n                throw new EOFException();\n            } else if (c == '\\n') {\n                break;\n            }\n\n            result.append((char) c);\n        }\n        final int length = result.length();\n        if (length > 0 && result.charAt(length - 1) == '\\r') {\n            result.setLength(length - 1);\n        }\n        return result.toString();\n    }\n\n    public static String readString(final InputStream is, String encoding) throws IOException {\n        InputStreamReader reader = new InputStreamReader(is, encoding);\n        StringBuilder sb = new StringBuilder();\n\n        char[] buffer = new char[DEFAULT_BUFFER_SIZE];\n        int n;\n        while (EOF != (n = reader.read(buffer))) {\n            sb.append(buffer, 0, n);\n        }\n\n        return sb.toString();\n    }\n\n    public static byte[] getAllByte(InputStream is) throws IOException {\n        ByteArrayOutputStream baos = new ByteArrayOutputStream();\n        copy(is, baos);\n        return baos.toByteArray();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/IOUtils.kt",
    "content": "/*\n * Copyright 2023 Tarsin Norbin\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.yorozuya\n\nimport com.hippo.util.isAtLeastN\nimport java.io.File\nimport okhttp3.ResponseBody\nimport okio.buffer\nimport okio.sink\n\nfun ResponseBody.copyToFile(file: File) {\n    file.outputStream().use { os ->\n        source().use {\n            // Prior to the adoption of OpenJDK, transferFrom will call ByteBuffer.allocate((int) count)\n            if (isAtLeastN) {\n                os.channel.transferFrom(it, 0, Long.MAX_VALUE)\n            } else {\n                os.sink().buffer().use { buffer -> buffer.writeAll(source()) }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/IntIdGenerator.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic final class IntIdGenerator {\n    public static final int INVALID_ID = -1;\n\n    private final AtomicInteger mId = new AtomicInteger();\n\n    public IntIdGenerator() {\n    }\n\n    public IntIdGenerator(int init) {\n        setNextId(init);\n    }\n\n    @SuppressWarnings(\"StatementWithEmptyBody\")\n    public int nextId() {\n        int id;\n        while ((id = mId.getAndIncrement()) == INVALID_ID) ;\n        return id;\n    }\n\n    public void setNextId(int id) {\n        checkInValidId(id);\n        mId.set(id);\n    }\n\n    private void checkInValidId(int id) {\n        if (INVALID_ID == id) {\n            throw new IllegalStateException(\"Can't set INVALID_ID\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/LayoutUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.content.Context;\n\npublic final class LayoutUtils {\n    private LayoutUtils() {\n    }\n\n    /**\n     * dp conversion to pix\n     *\n     * @param context The context\n     * @param dp      The value you want to conversion\n     * @return value in pix\n     */\n    public static int dp2pix(Context context, float dp) {\n        return (int) (dp * context.getResources().getDisplayMetrics().density + 0.5f);\n    }\n\n    /**\n     * pix conversion to dp\n     *\n     * @param context The context\n     * @param pix     The value you want to conversion\n     * @return value in dp\n     */\n    public static float pix2dp(Context context, int pix) {\n        return pix / context.getResources().getDisplayMetrics().density;\n    }\n\n    /**\n     * sp conversion to pix\n     *\n     * @param sp The value you want to conversion\n     * @return value in pix\n     */\n    public static int sp2pix(Context context, float sp) {\n        return (int) (sp * context.getResources().getDisplayMetrics().scaledDensity + 0.5f);\n    }\n\n    /**\n     * pix conversion to sp\n     *\n     * @param pix The value you want to conversion\n     * @return value in sp\n     */\n    public static float pix2sp(Context context, float pix) {\n        return pix / context.getResources().getDisplayMetrics().scaledDensity;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/MathUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport java.util.Random;\n\n// Get most code from android.util.MathUtils\npublic final class MathUtils {\n    private static final Random sRandom = new Random();\n    private static final float DEG_TO_RAD = 3.1415926f / 180.0f;\n    private static final float RAD_TO_DEG = 180.0f / 3.1415926f;\n\n    private MathUtils() {\n    }\n\n    public static float abs(float v) {\n        return v > 0 ? v : -v;\n    }\n\n    public static float log(float a) {\n        return (float) Math.log(a);\n    }\n\n    public static float exp(float a) {\n        return (float) Math.exp(a);\n    }\n\n    public static float pow(float a, float b) {\n        return (float) Math.pow(a, b);\n    }\n\n    public static float max(float a, float b) {\n        return Math.max(a, b);\n    }\n\n    public static float max(int a, int b) {\n        return Math.max(a, b);\n    }\n\n    public static float max(float a, float b, float c) {\n        return a > b ? Math.max(a, c) : Math.max(b, c);\n    }\n\n    public static float max(int a, int b, int c) {\n        return a > b ? Math.max(a, c) : Math.max(b, c);\n    }\n\n    public static float max(float... arg) {\n        int length = arg.length;\n        if (length == 0) {\n            throw new IllegalArgumentException(\"Empty argument\");\n        } else {\n            float n = arg[0];\n            float m;\n            for (int i = 1; i < length; i++) {\n                m = arg[i];\n                if (m > n)\n                    n = m;\n            }\n            return n;\n        }\n    }\n\n    public static int max(int... arg) {\n        int length = arg.length;\n        if (length == 0) {\n            throw new IllegalArgumentException(\"Empty argument\");\n        } else {\n            int n = arg[0];\n            int m;\n            for (int i = 1; i < length; i++) {\n                m = arg[i];\n                if (m > n)\n                    n = m;\n            }\n            return n;\n        }\n    }\n\n    public static float min(float a, float b) {\n        return Math.min(a, b);\n    }\n\n    public static float min(int a, int b) {\n        return Math.min(a, b);\n    }\n\n    public static float min(float a, float b, float c) {\n        return a < b ? Math.min(a, c) : Math.min(b, c);\n    }\n\n    public static float min(int a, int b, int c) {\n        return a < b ? Math.min(a, c) : Math.min(b, c);\n    }\n\n    public static float min(float... args) {\n        int length = args.length;\n        if (length == 0) {\n            throw new IllegalArgumentException(\"Empty argument\");\n        } else {\n            float n = args[0];\n            float m;\n            for (int i = 1; i < length; i++) {\n                m = args[i];\n                if (m < n)\n                    n = m;\n            }\n            return n;\n        }\n    }\n\n    public static int min(int... args) {\n        int length = args.length;\n        if (length == 0) {\n            throw new IllegalArgumentException(\"Empty argument\");\n        } else {\n            int n = args[0];\n            int m;\n            for (int i = 1; i < length; i++) {\n                m = args[i];\n                if (m < n)\n                    n = m;\n            }\n            return n;\n        }\n    }\n\n    public static float dist(float x1, float y1, float x2, float y2) {\n        final float x = (x2 - x1);\n        final float y = (y2 - y1);\n        return (float) Math.hypot(x, y);\n    }\n\n    public static float dist(float x1, float y1, float z1, float x2, float y2, float z2) {\n        final float x = (x2 - x1);\n        final float y = (y2 - y1);\n        final float z = (z2 - z1);\n        return (float) Math.sqrt(x * x + y * y + z * z);\n    }\n\n    public static boolean near(float x1, float y1, float x2, float y2, float slop) {\n        return dist(x1, y1, x2, y2) < slop;\n    }\n\n    public static boolean near(float x1, float y1, float z1, float x2, float y2, float z2, float slop) {\n        return dist(x1, y1, z1, x2, y2, z2) < slop;\n    }\n\n    public static float mag(float a, float b) {\n        return (float) Math.hypot(a, b);\n    }\n\n    public static float mag(float a, float b, float c) {\n        return (float) Math.sqrt(a * a + b * b + c * c);\n    }\n\n    public static float sq(float v) {\n        return v * v;\n    }\n\n    public static float cross(float v1x, float v1y, float v2x, float v2y) {\n        return v1x * v2y - v1y * v2x;\n    }\n\n    public static float radians(float degrees) {\n        return degrees * DEG_TO_RAD;\n    }\n\n    public static float degrees(float radians) {\n        return radians * RAD_TO_DEG;\n    }\n\n    public static float acos(float value) {\n        return (float) Math.acos(value);\n    }\n\n    public static float asin(float value) {\n        return (float) Math.asin(value);\n    }\n\n    public static float atan(float value) {\n        return (float) Math.atan(value);\n    }\n\n    public static float atan2(float a, float b) {\n        return (float) Math.atan2(a, b);\n    }\n\n    public static float tan(float angle) {\n        return (float) Math.tan(angle);\n    }\n\n    public static int lerp(int start, int stop, float amount) {\n        return start + (int) ((stop - start) * amount);\n    }\n\n    public static float lerp(float start, float stop, float amount) {\n        return start + (stop - start) * amount;\n    }\n\n    public static float delerp(int start, int stop, int value) {\n        if (stop == start) {\n            return 1.0f;\n        } else {\n            return (float) (value - start) / (float) (stop - start);\n        }\n    }\n\n    public static float delerp(float start, float stop, float value) {\n        if (stop == start) {\n            return 1.0f;\n        } else {\n            return (value - start) / (stop - start);\n        }\n    }\n\n    public static float norm(float start, float stop, float value) {\n        return (value - start) / (stop - start);\n    }\n\n    public static float map(float minStart, float minStop, float maxStart, float maxStop, float value) {\n        return maxStart + (maxStart - maxStop) * ((value - minStart) / (minStop - minStart));\n    }\n\n    /**\n     * Returns the input value x clamped to the range [min, max].\n     */\n    public static int clamp(int x, int min, int max) {\n        if (x > max) return max;\n        return Math.max(x, min);\n    }\n\n    /**\n     * Returns the input value x clamped to the range [min, max].\n     */\n    public static float clamp(float x, float min, float max) {\n        if (x > max) return max;\n        return Math.max(x, min);\n    }\n\n    /**\n     * Returns the input value x clamped to the range [min, max].\n     */\n    public static long clamp(long x, long min, long max) {\n        if (x > max) return max;\n        return Math.max(x, min);\n    }\n\n    /**\n     * Returns the next power of two.\n     * Returns the input if it is already power of 2.\n     * Throws IllegalArgumentException if the input is <= 0 or\n     * the answer overflows.\n     */\n    public static int nextPowerOf2(int n) {\n        if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException(\"n is invalid: \" + n);\n        n -= 1;\n        n |= n >> 16;\n        n |= n >> 8;\n        n |= n >> 4;\n        n |= n >> 2;\n        n |= n >> 1;\n        return n + 1;\n    }\n\n    /**\n     * Returns the previous power of two.\n     * Returns the input if it is already power of 2.\n     * Throws IllegalArgumentException if the input is <= 0 or\n     * the answer overflows.\n     */\n    public static int previousPowerOf2(int n) {\n        if (n <= 0 || n > (1 << 30)) throw new IllegalArgumentException(\"n is invalid: \" + n);\n        n |= n >> 1;\n        n |= n >> 2;\n        n |= n >> 4;\n        n |= n >> 8;\n        n |= n >> 16;\n        return n - (n >> 1);\n    }\n\n    // http://stackoverflow.com/questions/109023/how-to-count-the-number-of-set-bits-in-a-32-bit-integer\n    public static int hammingWeight(int n) {\n        n = n - ((n >>> 1) & 0x55555555);\n        n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);\n        return (((n + (n >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24;\n    }\n\n    /**\n     * divide and ceil\n     */\n    public static int ceilDivide(int a, int b) {\n        return (a + b - 1) / b;\n    }\n\n    /**\n     * divide and ceil\n     */\n    public static long ceilDivide(long a, long b) {\n        return (a + b - 1) / b;\n    }\n\n    /**\n     * Get coverage radius of a area\n     *\n     * @param w the width of the area\n     * @param h the height of the area\n     * @param x the x of point in area\n     * @param y the y of point in area\n     * @return the radius\n     */\n    public static float coverageRadius(float w, float h, float x, float y) {\n        float x2;\n        float y2;\n        if (x > w / 2) {\n            x2 = 0;\n        } else {\n            x2 = w;\n        }\n        if (y > h / 2) {\n            y2 = 0;\n        } else {\n            y2 = h;\n        }\n        return dist(x, y, x2, y2);\n    }\n\n    public static float positiveModulo(float x, float y) {\n        float result = x % y;\n        if (x < 0) {\n            result += y;\n        }\n        return result;\n    }\n\n    public static float negativeModulo(float x, float y) {\n        float result = x % y;\n        if (x > 0) {\n            result -= y;\n        }\n        return result;\n    }\n\n    /**\n     * [0, howbig)\n     */\n    public static int random(int howbig) {\n        return (int) (sRandom.nextFloat() * howbig);\n    }\n\n    /**\n     * [howsmall, howbig)\n     */\n    public static int random(int howsmall, int howbig) {\n        if (howsmall >= howbig)\n            return howsmall;\n        return lerp(howsmall, howbig, sRandom.nextFloat());\n    }\n\n    /**\n     * [0, howbig)\n     */\n    public static float random(float howbig) {\n        return sRandom.nextFloat() * howbig;\n    }\n\n    /**\n     * [howsmall, howbig)\n     */\n    public static float random(float howsmall, float howbig) {\n        if (howsmall >= howbig)\n            return howsmall;\n        return lerp(howsmall, howbig, sRandom.nextFloat());\n    }\n\n    public static void randomSeed(long seed) {\n        sRandom.setSeed(seed);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/NumberUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\npublic final class NumberUtils {\n    private NumberUtils() {\n    }\n\n    /**\n     * 0 for false, Non 0 for true\n     *\n     * @param integer the int\n     * @return the boolean\n     */\n    public static boolean int2boolean(int integer) {\n        return integer != 0;\n    }\n\n    /**\n     * false for 0, true for 1\n     *\n     * @param bool the boolean\n     * @return the int\n     */\n    public static int boolean2int(boolean bool) {\n        return bool ? 1 : 0;\n    }\n\n    /**\n     * Do not throw NumberFormatException, use default value\n     *\n     * @param str          the string to be parsed\n     * @param defaultValue the value to return when get error\n     * @return the value of the string\n     */\n    public static int parseIntSafely(String str, int defaultValue) {\n        try {\n            return Integer.parseInt(str);\n        } catch (Throwable e) {\n            return defaultValue;\n        }\n    }\n\n    /**\n     * Do not throw NumberFormatException, use default value\n     *\n     * @param str          the string to be parsed\n     * @param defaultValue the value to return when get error\n     * @return the value of the string\n     */\n    public static long parseLongSafely(String str, long defaultValue) {\n        try {\n            return Long.parseLong(str);\n        } catch (Throwable e) {\n            return defaultValue;\n        }\n    }\n\n    /**\n     * Do not throw NumberFormatException, use default value\n     *\n     * @param str          the string to be parsed\n     * @param defaultValue the value to return when get error\n     * @return the value of the string\n     */\n    public static float parseFloatSafely(String str, float defaultValue) {\n        try {\n            return Float.parseFloat(str);\n        } catch (Throwable e) {\n            return defaultValue;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/OSUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.os.Looper;\nimport android.util.Log;\n\nimport java.io.BufferedReader;\nimport java.io.FileReader;\nimport java.io.IOException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic final class OSUtils {\n    private static final String PROCFS_MEMFILE = \"/proc/meminfo\";\n    private static final Pattern PROCFS_MEMFILE_FORMAT =\n            Pattern.compile(\"^([a-zA-Z]*):[ \\t]*([0-9]*)[ \\t]kB\");\n    private static final String MEMTOTAL_STRING = \"MemTotal\";\n    private static long sTotalMem = Long.MIN_VALUE;\n\n    private OSUtils() {\n    }\n\n    public static void checkMainLoop() {\n        if (Looper.myLooper() != Looper.getMainLooper()) {\n            throw new IllegalStateException(\"It is in not main loop!\");\n        }\n    }\n\n    /**\n     * Get application allocated memory size\n     */\n    public static long getAppAllocatedMemory() {\n        return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();\n    }\n\n    /**\n     * Get application max memory size\n     */\n    public static long getAppMaxMemory() {\n        return Runtime.getRuntime().maxMemory();\n    }\n\n    /**\n     * Get device RAM size\n     */\n    public static long getTotalMemory() {\n        if (sTotalMem == Long.MIN_VALUE) {\n            BufferedReader reader = null;\n            try {\n                reader = new BufferedReader(new FileReader(PROCFS_MEMFILE), 64);\n                String line;\n                while ((line = reader.readLine()) != null) {\n                    Matcher matcher = PROCFS_MEMFILE_FORMAT.matcher(line);\n                    if (matcher.find() && MEMTOTAL_STRING.equals(matcher.group(1))) {\n                        long mem = NumberUtils.parseLongSafely(matcher.group(2), -1L);\n                        if (mem != -1L) {\n                            mem *= 1024;\n                        }\n                        sTotalMem = mem;\n                        break;\n                    }\n                }\n            } catch (IOException e) {\n                Log.e(\"OSUtils\", \"Error getting total memory\", e);\n            } finally {\n                IOUtils.closeQuietly(reader);\n            }\n            if (sTotalMem == Long.MIN_VALUE) {\n                sTotalMem = -1L;\n            }\n        }\n        return sTotalMem;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/ObjectUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport androidx.annotation.Nullable;\n\nimport java.io.PrintWriter;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.Modifier;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\n\npublic final class ObjectUtils {\n    private ObjectUtils() {\n    }\n\n    /**\n     * Returns true if two possibly-null objects are equal.\n     */\n    public static boolean equal(@Nullable Object a, @Nullable Object b) {\n        return Objects.equals(a, b);\n    }\n\n    /**\n     * Returns \"null\" for null or {@code o.toString()}.\n     */\n    public static String toString(Object o) {\n        return (o == null) ? \"null\" : o.toString();\n    }\n\n    private static void dumpObject(Object o, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(o.getClass().getName());\n        writer.write('\\n');\n    }\n\n    private static void dumpBoolean(boolean z, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Boolean.toString(z));\n        writer.write('\\n');\n    }\n\n    private static void dumpByte(byte b, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Byte.toString(b));\n        writer.write('\\n');\n    }\n\n    private static void dumpChar(char c, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Character.toString(c));\n        writer.write('\\n');\n    }\n\n    private static void dumpShort(short s, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Short.toString(s));\n        writer.write('\\n');\n    }\n\n    private static void dumpInt(int i, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Integer.toString(i));\n        writer.write('\\n');\n    }\n\n    private static void dumpLong(long j, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Long.toString(j));\n        writer.write('\\n');\n    }\n\n    private static void dumpFloat(float f, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Float.toString(f));\n        writer.write('\\n');\n    }\n\n    private static void dumpDouble(double d, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(Double.toString(d));\n        writer.write('\\n');\n    }\n\n    private static void dumpArray(Object array, PrintWriter writer, String prefix, boolean skipFirstPrefix) {\n        if (skipFirstPrefix) {\n            dumpObject(array, writer, \"\");\n        } else {\n            dumpObject(array, writer, prefix);\n        }\n\n        String newPrefix = prefix + \"    \";\n        writer.write(newPrefix);\n        writer.write('[');\n        writer.write('\\n');\n\n        switch (array) {\n            case Object[] a -> {\n                for (Object o : a) {\n                    dump(o, writer, newPrefix, false);\n                }\n            }\n            case boolean[] a -> {\n                for (boolean b : a) {\n                    dumpBoolean(b, writer, newPrefix);\n                }\n            }\n            case byte[] a -> {\n                for (byte b : a) {\n                    dumpByte(b, writer, newPrefix);\n                }\n            }\n            case char[] a -> {\n                for (char c : a) {\n                    dumpChar(c, writer, newPrefix);\n                }\n            }\n            case short[] a -> {\n                for (short value : a) {\n                    dumpShort(value, writer, newPrefix);\n                }\n            }\n            case int[] a -> {\n                for (int j : a) {\n                    dumpInt(j, writer, newPrefix);\n                }\n            }\n            case long[] a -> {\n                for (long value : a) {\n                    dumpLong(value, writer, newPrefix);\n                }\n            }\n            case float[] a -> {\n                for (float v : a) {\n                    dumpFloat(v, writer, newPrefix);\n                }\n            }\n            case double[] a -> {\n                for (double v : a) {\n                    dumpDouble(v, writer, newPrefix);\n                }\n            }\n            case List<?> list -> {\n                for (Object o : list) {\n                    dump(o, writer, newPrefix);\n                }\n            }\n            case Set<?> set -> {\n                for (Object o : set) {\n                    dump(o, writer, newPrefix);\n                }\n            }\n            case Map<?, ?> map -> {\n                for (Object key : map.keySet()) {\n                    Object value = map.get(key);\n                    writer.write(newPrefix);\n                    writer.write(key.toString());\n                    writer.write(\": \");\n                    dump(value, writer, newPrefix, true);\n                }\n            }\n            default -> throw new IllegalStateException(array + \" is not array\");\n        }\n\n        writer.write(newPrefix);\n        writer.write(']');\n        writer.write('\\n');\n    }\n\n    public static void dump(Object o, PrintWriter writer) {\n        dump(o, writer, \"\", false);\n    }\n\n    public static void dump(Object o, PrintWriter writer, String prefix) {\n        dump(o, writer, prefix, false);\n    }\n\n    public static void dump(Object o, PrintWriter writer, String prefix, boolean skipFirstPrefix) {\n        if (o == null) {\n            if (!skipFirstPrefix) {\n                writer.write(prefix);\n            }\n            writer.write(\"null\\n\");\n        } else if (o.getClass().isArray() || o instanceof List || o instanceof Set || o instanceof Map) {\n            dumpArray(o, writer, prefix, skipFirstPrefix);\n        } else if (o.getClass().isPrimitive() || o instanceof Boolean ||\n                o instanceof Byte || o instanceof Character || o instanceof Short ||\n                o instanceof Integer || o instanceof Long || o instanceof Float ||\n                o instanceof Double || o instanceof String) {\n            if (!skipFirstPrefix) {\n                writer.write(prefix);\n            }\n            writer.write(o.toString());\n            writer.write('\\n');\n        } else {\n            if (skipFirstPrefix) {\n                dumpObject(o, writer, \"\");\n            } else {\n                dumpObject(o, writer, prefix);\n            }\n\n            String newPrefix = prefix + \"    \";\n            for (Field field : o.getClass().getDeclaredFields()) {\n                // Skip static filed\n                if (Modifier.isStatic(field.getModifiers())) {\n                    continue;\n                }\n                field.setAccessible(true);\n                String name = field.getName();\n                try {\n                    Object value = field.get(o);\n                    writer.write(newPrefix);\n                    writer.write(name);\n                    writer.write(\": \");\n                    dump(value, writer, newPrefix, true);\n                } catch (IllegalAccessException e) {\n                    // Ignore\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/Pool.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.yorozuya;\n\npublic class Pool<T> {\n    private final T[] mArray;\n    private final int mMaxSize;\n    private int mSize;\n\n    @SuppressWarnings(\"unchecked\")\n    public Pool(int size) {\n        if (size <= 0) {\n            throw new IllegalStateException(\"Pool size must > 0, it is \" + size);\n        }\n        mArray = (T[]) new Object[size];\n        mMaxSize = size;\n        mSize = 0;\n    }\n\n    public void push(T t) {\n        if (t != null && mSize < mMaxSize) {\n            mArray[mSize++] = t;\n        }\n    }\n\n    public T pop() {\n        if (mSize > 0) {\n            T t = mArray[--mSize];\n            mArray[mSize] = null;\n            return t;\n        } else {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/ResourcesUtils.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.content.Context;\nimport android.content.res.Resources;\nimport android.util.TypedValue;\n\nimport androidx.annotation.AttrRes;\n\npublic final class ResourcesUtils {\n    private static final Object mAccessLock = new Object();\n    private static TypedValue mTmpValue = new TypedValue();\n\n    private ResourcesUtils() {\n    }\n\n    public static float getFloat(Resources resources, int resId) {\n        TypedValue outValue = new TypedValue();\n        resources.getValue(resId, outValue, true);\n        return outValue.getFloat();\n    }\n\n    private static void getAttrValue(Context context, int attrId, TypedValue value) {\n        context.getTheme().resolveAttribute(attrId, value, true);\n    }\n\n    public static int getAttrColor(Context context, @AttrRes int attrId) {\n        TypedValue value;\n        synchronized (mAccessLock) {\n            value = mTmpValue;\n            if (value == null) {\n                value = new TypedValue();\n            }\n            getAttrValue(context, attrId, value);\n            if (value.type >= TypedValue.TYPE_FIRST_INT\n                    && value.type <= TypedValue.TYPE_LAST_INT) {\n                mTmpValue = value;\n                return value.data;\n            } else {\n                throw new Resources.NotFoundException(\n                        \"Attribute ID #0x\" + Integer.toHexString(attrId) + \" type #0x\"\n                                + Integer.toHexString(value.type) + \" is not valid\");\n            }\n        }\n    }\n\n    public static boolean getAttrBoolean(Context context, @AttrRes int attrId) {\n        synchronized (mAccessLock) {\n            TypedValue value = mTmpValue;\n            if (value == null) {\n                mTmpValue = value = new TypedValue();\n            }\n            getAttrValue(context, attrId, value);\n            if (value.type >= TypedValue.TYPE_FIRST_INT\n                    && value.type <= TypedValue.TYPE_LAST_INT) {\n                return value.data != 0;\n            }\n            throw new Resources.NotFoundException(\n                    \"Attribute ID #0x\" + Integer.toHexString(attrId) + \" type #0x\"\n                            + Integer.toHexString(value.type) + \" is not valid\");\n        }\n    }\n\n    public static int getAttrDimensionPixelOffset(Context context, @AttrRes int attrId) {\n        synchronized (mAccessLock) {\n            TypedValue value = mTmpValue;\n            if (value == null) {\n                mTmpValue = value = new TypedValue();\n            }\n            getAttrValue(context, attrId, value);\n            if (value.type == TypedValue.TYPE_DIMENSION) {\n                return TypedValue.complexToDimensionPixelOffset(\n                        value.data, context.getResources().getDisplayMetrics());\n            }\n            throw new Resources.NotFoundException(\n                    \"Attribute ID #0x\" + Integer.toHexString(attrId) + \" type #0x\"\n                            + Integer.toHexString(value.type) + \" is not valid\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/SimpleAnimatorListener.java",
    "content": "/*\n * Copyright 2016 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.animation.Animator;\n\nimport androidx.annotation.NonNull;\n\npublic abstract class SimpleAnimatorListener implements Animator.AnimatorListener {\n    @Override\n    public void onAnimationStart(@NonNull Animator animation) {\n    }\n\n    @Override\n    public void onAnimationEnd(@NonNull Animator animation) {\n    }\n\n    @Override\n    public void onAnimationCancel(@NonNull Animator animation) {\n    }\n\n    @Override\n    public void onAnimationRepeat(@NonNull Animator animation) {\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/SimpleHandler.java",
    "content": "/*\n * Copyright (C) 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.os.Handler;\nimport android.os.Looper;\n\npublic final class SimpleHandler extends Handler {\n    private static Handler sInstance;\n\n    private SimpleHandler(Looper mainLooper) {\n        super(mainLooper);\n    }\n\n    public static Handler getInstance() {\n        if (sInstance == null) {\n            sInstance = new Handler(Looper.getMainLooper());\n        }\n        return sInstance;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/StringUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.text.TextUtils;\n\nimport org.jsoup.parser.Parser;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic final class StringUtils {\n    public static final String[] EMPTY_STRING_ARRAY = new String[0];\n    public static final char[] WHITE_SPACE_ARRAY = {\n            '\\t', // TAB\n            ' ', // SPACE\n            '\\u00A0', // NO-BREAK SPACE\n            '\\u3000', // IDEOGRAPHIC SPACE\n    };\n\n    private StringUtils() {\n    }\n\n    /**\n     * Unescape xml. It do not work perfectly.\n     */\n    public static String unescapeXml(String str) {\n        return Parser.unescapeEntities(str, true);\n    }\n\n    /**\n     * <p>Replaces all occurrences of a String within another String.</p>\n     *\n     * <p>A {@code null} reference passed to this method is a no-op.</p>\n     *\n     * <pre>\n     * StringUtils.replace(null, *, *)        = null\n     * StringUtils.replace(\"\", *, *)          = \"\"\n     * StringUtils.replace(\"any\", null, *)    = \"any\"\n     * StringUtils.replace(\"any\", *, null)    = \"any\"\n     * StringUtils.replace(\"any\", \"\", *)      = \"any\"\n     * StringUtils.replace(\"aba\", \"a\", null)  = \"aba\"\n     * StringUtils.replace(\"aba\", \"a\", \"\")    = \"b\"\n     * StringUtils.replace(\"aba\", \"a\", \"z\")   = \"zbz\"\n     * </pre>\n     *\n     * @param text         text to search and replace in, may be null\n     * @param searchString the String to search for, may be null\n     * @param replacement  the String to replace it with, may be null\n     * @return the text with any replacements processed,\n     * {@code null} if null String input\n     * @see #replace(String text, String searchString, String replacement, int max)\n     */\n    public static String replace(final String text, final String searchString, final String replacement) {\n        return replace(text, searchString, replacement, -1);\n    }\n\n    /**\n     * <p>Replaces a String with another String inside a larger String,\n     * for the first {@code max} values of the search String.</p>\n     *\n     * <p>A {@code null} reference passed to this method is a no-op.</p>\n     *\n     * <pre>\n     * StringUtils.replace(null, *, *, *)         = null\n     * StringUtils.replace(\"\", *, *, *)           = \"\"\n     * StringUtils.replace(\"any\", null, *, *)     = \"any\"\n     * StringUtils.replace(\"any\", *, null, *)     = \"any\"\n     * StringUtils.replace(\"any\", \"\", *, *)       = \"any\"\n     * StringUtils.replace(\"any\", *, *, 0)        = \"any\"\n     * StringUtils.replace(\"abaa\", \"a\", null, -1) = \"abaa\"\n     * StringUtils.replace(\"abaa\", \"a\", \"\", -1)   = \"b\"\n     * StringUtils.replace(\"abaa\", \"a\", \"z\", 0)   = \"abaa\"\n     * StringUtils.replace(\"abaa\", \"a\", \"z\", 1)   = \"zbaa\"\n     * StringUtils.replace(\"abaa\", \"a\", \"z\", 2)   = \"zbza\"\n     * StringUtils.replace(\"abaa\", \"a\", \"z\", -1)  = \"zbzz\"\n     * </pre>\n     *\n     * @param text         text to search and replace in, may be null\n     * @param searchString the String to search for, may be null\n     * @param replacement  the String to replace it with, may be null\n     * @param max          maximum number of values to replace, or {@code -1} if no maximum\n     * @return the text with any replacements processed,\n     * {@code null} if null String input\n     */\n    public static String replace(final String text, final String searchString, final String replacement, int max) {\n        if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchString) || replacement == null || max == 0) {\n            return text;\n        }\n        int start = 0;\n        int end = text.indexOf(searchString, start);\n        if (end < 0) {\n            return text;\n        }\n        final int replLength = searchString.length();\n        int increase = replacement.length() - replLength;\n        increase = Math.max(increase, 0);\n        increase *= max < 0 ? 16 : Math.min(max, 64);\n        final StringBuilder buf = new StringBuilder(text.length() + increase);\n        while (end >= 0) {\n            buf.append(text.substring(start, end)).append(replacement);\n            start = end + replLength;\n            if (--max == 0) {\n                break;\n            }\n            end = text.indexOf(searchString, start);\n        }\n        buf.append(text.substring(start));\n        return buf.toString();\n    }\n\n    public static boolean endsWith(String string, String[] suffixs) {\n        for (String suffix : suffixs) {\n            if (string.endsWith(suffix)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * <p>Splits the provided text into an array, separator specified.\n     * This is an alternative to using StringTokenizer.</p>\n     *\n     * <p>The separator is not included in the returned String array.\n     * Adjacent separators are treated as one separator.\n     * For more control over the split use the StrTokenizer class.</p>\n     *\n     * <p>A {@code null} input String returns {@code null}.</p>\n     *\n     * <pre>\n     * StringUtils.split(null, *)         = null\n     * StringUtils.split(\"\", *)           = []\n     * StringUtils.split(\"a.b.c\", '.')    = [\"a\", \"b\", \"c\"]\n     * StringUtils.split(\"a..b.c\", '.')   = [\"a\", \"b\", \"c\"]\n     * StringUtils.split(\"a:b:c\", '.')    = [\"a:b:c\"]\n     * StringUtils.split(\"a b c\", ' ')    = [\"a\", \"b\", \"c\"]\n     * </pre>\n     *\n     * @param str           the String to parse, may be null\n     * @param separatorChar the character used as the delimiter\n     * @return an array of parsed Strings, {@code null} if null String input\n     * @since 2.0\n     */\n    // Get from org.apache.commons.lang3.StringUtils\n    public static String[] split(final String str, final char separatorChar) {\n        return splitWorker(str, separatorChar, false);\n    }\n\n    /**\n     * Performs the logic for the {@code split} and\n     * {@code splitPreserveAllTokens} methods that do not return a\n     * maximum array length.\n     *\n     * @param str               the String to parse, may be {@code null}\n     * @param separatorChar     the separate character\n     * @param preserveAllTokens if {@code true}, adjacent separators are\n     *                          treated as empty token separators; if {@code false}, adjacent\n     *                          separators are treated as one separator.\n     * @return an array of parsed Strings, {@code null} if null String input\n     */\n    // Get from org.apache.commons.lang3.StringUtils\n    private static String[] splitWorker(final String str, final char separatorChar, final boolean preserveAllTokens) {\n        // Performance tuned for 2.0 (JDK1.4)\n\n        if (str == null) {\n            return null;\n        }\n        final int len = str.length();\n        if (len == 0) {\n            return EMPTY_STRING_ARRAY;\n        }\n        final List<String> list = new ArrayList<>();\n        int i = 0, start = 0;\n        boolean match = false;\n        boolean lastMatch = false;\n        while (i < len) {\n            if (str.charAt(i) == separatorChar) {\n                if (match || preserveAllTokens) {\n                    list.add(str.substring(start, i));\n                    match = false;\n                    lastMatch = true;\n                }\n                start = ++i;\n                continue;\n            }\n            lastMatch = false;\n            match = true;\n            i++;\n        }\n        //noinspection ConstantValue\n        if (match || preserveAllTokens && lastMatch) {\n            list.add(str.substring(start, i));\n        }\n        return list.toArray(new String[0]);\n    }\n\n    public static String avoidNull(String value) {\n        return avoidNull(value, \"\");\n    }\n\n    public static String avoidNull(String value, String defaultValue) {\n        return value == null ? defaultValue : value;\n    }\n\n    public static boolean isAllDigit(String str) {\n        if (TextUtils.isEmpty(str)) {\n            return false;\n        } else {\n            for (int i = 0, n = str.length(); i < n; i++) {\n                char ch = str.charAt(i);\n                if (ch < '0' || ch > '9') {\n                    return false;\n                }\n            }\n            return true;\n        }\n    }\n\n    public static int length(String str) {\n        return null == str ? 0 : str.length();\n    }\n\n    /**\n     * All null or empty, or all not\n     */\n    public static boolean equals(String str1, String str2) {\n        return (TextUtils.isEmpty(str1) && TextUtils.isEmpty(str2)) ||\n                (!TextUtils.isEmpty(str1) && !TextUtils.isEmpty(str2) && str1.equals(str2));\n    }\n\n    public static int ordinalIndexOf(String str, char c, int n) {\n        if (null == str || n < 0) {\n            return -1;\n        }\n\n        int pos = -1;\n        do {\n            pos = str.indexOf(c, pos + 1);\n        } while (n-- > 0 && pos != -1);\n\n        return pos;\n    }\n\n    /**\n     * Works like {@link String#trim()}, but more white space is excluded.\n     * The white space characters is {@link #WHITE_SPACE_ARRAY}.\n     *\n     * @see #trim(String, char[])\n     */\n    public static String trim(String str) {\n        return trim(str, WHITE_SPACE_ARRAY);\n    }\n\n    /**\n     * Works like {@link String#trim()}, but custom characters is excluded.\n     *\n     * @see #trim(String)\n     */\n    public static String trim(String str, char[] excluded) {\n        if (null == str) {\n            return null;\n        }\n\n        int start = 0, last = str.length() - 1;\n        int end = last;\n        while ((start <= end) && (Arrays.binarySearch(excluded, str.charAt(start)) >= 0)) {\n            start++;\n        }\n        while ((end >= start) && (Arrays.binarySearch(excluded, str.charAt(end)) >= 0)) {\n            end--;\n        }\n        if (start == 0 && end == last) {\n            return str;\n        }\n\n        return str.substring(start, end + 1);\n    }\n\n    public static String remove(String str, char[] removed) {\n        if (TextUtils.isEmpty(str) || null == removed || 0 == removed.length) {\n            return str;\n        }\n\n        final char[] chars = str.toCharArray();\n        int pos = 0;\n        for (int i = 0; i < chars.length; i++) {\n            if (!Utilities.contain(removed, chars[i])) {\n                chars[pos++] = chars[i];\n            }\n        }\n        return new String(chars, 0, pos);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/StringUtils.kt",
    "content": "/*\n * Copyright 2023 Moedog\n *\n * This file is part of EhViewer\n *\n * EhViewer is free software: you can redistribute it and/or\n * modify it under the terms of the GNU General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * EhViewer is distributed in the hope that it will be useful, but\n * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License along with EhViewer.\n * If not, see <https://www.gnu.org/licenses/>.\n */\npackage com.hippo.yorozuya\n\nimport org.jsoup.parser.Parser\n\nfun String.cleanAsDirname(): String = this\n    .replace(Regex(\"[^\\\\p{L}\\\\p{N}\\\\p{P}\\\\p{Z}]\"), \"\")\n    .replace(Regex(\"\\\\s+\"), \" \")\n    .trim()\n\nfun String.unescapeXml(): String = Parser.unescapeEntities(this, true)\n\ninline infix fun <T> CharSequence.trimAnd(block: CharSequence.() -> T): T = block(trim())\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/Utilities.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport androidx.annotation.Nullable;\n\npublic final class Utilities {\n    /**\n     * Whether the array contain the element\n     *\n     * @param array the array\n     * @param obj   the element\n     * @return true for the array contain the element\n     */\n    public static boolean contain(@Nullable Object[] array, @Nullable Object obj) {\n        if (null == array) {\n            return false;\n        }\n\n        for (Object o : array) {\n            if (ObjectUtils.equal(o, obj)) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * Whether the array contain the element\n     *\n     * @param array the array\n     * @param ch    the element\n     * @return true for the array contain the element\n     */\n    public static boolean contain(@Nullable char[] array, char ch) {\n        if (null == array) {\n            return false;\n        }\n\n        for (char c : array) {\n            if (c == ch) {\n                return true;\n            }\n        }\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/ViewUtils.java",
    "content": "/*\n * Copyright 2015 Hippo Seven\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\npackage com.hippo.yorozuya;\n\nimport android.app.Activity;\nimport android.app.Dialog;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewParent;\nimport android.view.ViewTreeObserver;\n\nimport androidx.annotation.IdRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.io.PrintWriter;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic final class ViewUtils {\n    public static final int MAX_SIZE = Integer.MAX_VALUE & ~(0x3 << 30);\n    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);\n\n    private ViewUtils() {\n    }\n\n    /**\n     * Get view center location in window\n     *\n     * @param view     the view to check\n     * @param location an array of two integers in which to hold the coordinates\n     */\n    public static void getCenterInWindows(View view, int[] location) {\n        getLocationInWindow(view, location);\n        location[0] += view.getWidth() / 2;\n        location[1] += view.getHeight() / 2;\n    }\n\n    /**\n     * Get view location in window\n     *\n     * @param view     the view to check\n     * @param location an array of two integers in which to hold the coordinates\n     */\n    public static void getLocationInWindow(View view, int[] location) {\n        getLocationInAncestor(view, location, android.R.id.content);\n    }\n\n    /**\n     * Get view center in ths ancestor\n     *\n     * @param view       the view to start with\n     * @param location   the container of result\n     * @param ancestorId the ancestor id\n     */\n    public static void getCenterInAncestor(View view, int[] location, int ancestorId) {\n        getLocationInAncestor(view, location, ancestorId);\n        location[0] += view.getWidth() / 2;\n        location[1] += view.getHeight() / 2;\n    }\n\n    /**\n     * Get view location in ths ancestor\n     *\n     * @param view       the view to start with\n     * @param location   the container of result\n     * @param ancestorId the ancestor id\n     */\n    public static void getLocationInAncestor(View view, int[] location, int ancestorId) {\n        if (location == null || location.length < 2) {\n            throw new IllegalArgumentException(\n                    \"location must be an array of two integers\");\n        }\n\n        float[] position = new float[2];\n\n        position[0] = view.getLeft();\n        position[1] = view.getTop();\n\n        ViewParent viewParent = view.getParent();\n        while (viewParent instanceof View) {\n            view = (View) viewParent;\n            if (view.getId() == ancestorId) {\n                break;\n            }\n\n            position[0] -= view.getScrollX();\n            position[1] -= view.getScrollY();\n\n            position[0] += view.getLeft();\n            position[1] += view.getTop();\n\n            viewParent = view.getParent();\n        }\n\n        location[0] = (int) (position[0] + 0.5f);\n        location[1] = (int) (position[1] + 0.5f);\n    }\n\n    /**\n     * Get view center in ths ancestor\n     *\n     * @param view     the view to start with\n     * @param location the container of result\n     * @param ancestor the ancestor\n     */\n    public static void getCenterInAncestor(View view, int[] location, View ancestor) {\n        getLocationInAncestor(view, location, ancestor);\n        location[0] += view.getWidth() / 2;\n        location[1] += view.getHeight() / 2;\n    }\n\n    /**\n     * Get view location in ths ancestor\n     *\n     * @param view     the view to start with\n     * @param location the container of result\n     * @param ancestor the ancestor\n     */\n    public static void getLocationInAncestor(View view, int[] location, View ancestor) {\n        if (location == null || location.length < 2) {\n            throw new IllegalArgumentException(\n                    \"location must be an array of two integers\");\n        }\n\n        float[] position = new float[2];\n\n        position[0] = view.getLeft();\n        position[1] = view.getTop();\n\n        ViewParent viewParent = view.getParent();\n        while (viewParent instanceof View) {\n            view = (View) viewParent;\n            if (viewParent == ancestor) {\n                break;\n            }\n\n            position[0] -= view.getScrollX();\n            position[1] -= view.getScrollY();\n\n            position[0] += view.getLeft();\n            position[1] += view.getTop();\n\n            viewParent = view.getParent();\n        }\n\n        location[0] = (int) (position[0] + 0.5f);\n        location[1] = (int) (position[1] + 0.5f);\n    }\n\n    /**\n     * Look for a ancestor view with the given id. If this view has the given\n     * id, return this view.\n     *\n     * @param view the view to start with\n     * @param id   The id to search for.\n     * @return The view that has the given id in the hierarchy or null\n     */\n    public static View getAncestor(View view, int id) {\n        if (view.getId() == id) {\n            return view;\n        }\n\n        ViewParent viewParent = view.getParent();\n        while (viewParent instanceof View) {\n            view = (View) viewParent;\n            if (view.getId() == id) {\n                return view;\n            }\n            viewParent = view.getParent();\n        }\n        return null;\n    }\n\n    /**\n     * Look for a child view with the given id. If this view has the given\n     * id, return this view.\n     *\n     * @param view the view to start with\n     * @param id   the id to search for\n     * @return the view that has the given id in the hierarchy or null\n     */\n    public static View getChild(View view, int id) {\n        if (view.getId() == id) {\n            return view;\n        }\n\n        if (view instanceof ViewGroup viewGroup) {\n            for (int i = 0, n = viewGroup.getChildCount(); i < n; i++) {\n                View child = viewGroup.getChildAt(i);\n                View result = getChild(child, id);\n                if (result != null) {\n                    return result;\n                }\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * Returns a bitmap showing a screenshot of the view passed in.\n     *\n     * @param v The view to get screenshot\n     * @return The screenshot\n     */\n    public static Bitmap getBitmapFromView(View v) {\n        int width = v.getWidth();\n        int height = v.getHeight();\n        if (width == 0 && height == 0) {\n            width = v.getMeasuredWidth();\n            height = v.getMeasuredHeight();\n        }\n        Bitmap bitmap = Bitmap.createBitmap(width, height,\n                Bitmap.Config.ARGB_8888);\n        Canvas canvas = new Canvas(bitmap);\n        canvas.translate(-v.getScrollX(), -v.getScrollY());\n        v.draw(canvas);\n        return bitmap;\n    }\n\n    /**\n     * Remove view from its parent\n     *\n     * @param view the view to remove\n     */\n    public static void removeFromParent(View view) {\n        ViewParent vp = view.getParent();\n        if (vp instanceof ViewGroup)\n            ((ViewGroup) vp).removeView(view);\n    }\n\n    /**\n     * Method that removes the support for HardwareAcceleration from a\n     * {@link View}.<br/>\n     * <br/>\n     * Check AOSP notice:<br/>\n     *\n     * <pre>\n     * 'ComposeShader can only contain shaders of different types (a BitmapShader and a\n     * LinearGradient for instance, but not two instances of BitmapShader)'. But, 'If your\n     * application is affected by any of these missing features or limitations, you can turn\n     * off hardware acceleration for just the affected portion of your application by calling\n     * setLayerType(View.LAYER_TYPE_SOFTWARE, null).'\n     * </pre>\n     *\n     * @param v The view\n     */\n    public static void removeHardwareAccelerationSupport(View v) {\n        if (v.getLayerType() != View.LAYER_TYPE_SOFTWARE) {\n            v.setLayerType(View.LAYER_TYPE_SOFTWARE, null);\n        }\n    }\n\n    public static void addHardwareAccelerationSupport(View v) {\n        if (v.getLayerType() != View.LAYER_TYPE_HARDWARE) {\n            v.setLayerType(View.LAYER_TYPE_HARDWARE, null);\n        }\n    }\n\n    public static void measureView(View v, int width, int height) {\n        int widthMeasureSpec;\n        int heightMeasureSpec;\n        if (width == ViewGroup.LayoutParams.WRAP_CONTENT)\n            widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0,\n                    View.MeasureSpec.UNSPECIFIED);\n        else\n            widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(Math.max(width, 0),\n                    View.MeasureSpec.EXACTLY);\n        if (height == ViewGroup.LayoutParams.WRAP_CONTENT)\n            heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0,\n                    View.MeasureSpec.UNSPECIFIED);\n        else\n            heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(Math.max(height, 0),\n                    View.MeasureSpec.EXACTLY);\n\n        v.measure(widthMeasureSpec, heightMeasureSpec);\n    }\n\n    /**\n     * Determine if the supplied view is under the given point in the\n     * parent view's coordinate system.\n     *\n     * @param view Child view of the parent to hit test\n     * @param x    X position to test in the parent's coordinate system\n     * @param y    Y position to test in the parent's coordinate system\n     * @param slop the slop out of the view, or negative for inside\n     * @return true if the supplied view is under the given point, false otherwise\n     */\n    public static boolean isViewUnder(@Nullable View view, int x, int y, int slop) {\n        if (view == null) {\n            return false;\n        } else {\n            float translationX = view.getTranslationX();\n            float translationY = view.getTranslationY();\n            return x >= view.getLeft() + translationX - slop &&\n                    x < view.getRight() + translationX + slop &&\n                    y >= view.getTop() + translationY - slop &&\n                    y < view.getBottom() + translationY + slop;\n        }\n    }\n\n    /**\n     * Utility to return a default size. Uses the supplied size if the\n     * MeasureSpec imposed no constraints. Will get suitable if allowed\n     * by the MeasureSpec.\n     *\n     * @param size        Default size for this view\n     * @param measureSpec Constraints imposed by the parent\n     * @return The size this view should be.\n     */\n    public static int getSuitableSize(int size, int measureSpec) {\n        int result = size;\n        int specMode = View.MeasureSpec.getMode(measureSpec);\n        int specSize = View.MeasureSpec.getSize(measureSpec);\n\n        switch (specMode) {\n            case View.MeasureSpec.UNSPECIFIED:\n                break;\n            case View.MeasureSpec.EXACTLY:\n                result = specSize;\n                break;\n            case View.MeasureSpec.AT_MOST:\n                return size == 0 ? specSize : Math.min(size, specSize);\n        }\n        return result;\n    }\n\n    /**\n     * removeOnGlobalLayoutListener\n     *\n     * @param viewTreeObserver the ViewTreeObserver\n     * @param l                the OnGlobalLayoutListener\n     */\n    public static void removeOnGlobalLayoutListener(ViewTreeObserver viewTreeObserver,\n                                                    ViewTreeObserver.OnGlobalLayoutListener l) {\n        viewTreeObserver.removeOnGlobalLayoutListener(l);\n    }\n\n    /**\n     * Get index in parent\n     *\n     * @param view The view\n     * @return The index\n     */\n    public static int getIndexInParent(View view) {\n        ViewParent parent = view.getParent();\n        if (parent instanceof ViewGroup viewParent) {\n            int count = viewParent.getChildCount();\n            for (int i = 0; i < count; i++) {\n                View v = viewParent.getChildAt(i);\n                if (v == view) {\n                    return i;\n                }\n            }\n        }\n        return -1;\n    }\n\n    /**\n     * Transform point from parent to child\n     *\n     * @param point  the point\n     * @param parent the parent\n     * @param child  the child\n     */\n    public static void transformPointToViewLocal(float[] point, View parent, View child) {\n        point[0] += parent.getScrollX() - child.getLeft();\n        point[1] += parent.getScrollY() - child.getTop();\n    }\n\n    public static void setEnabledRecursively(View view, boolean enabled) {\n        if (view instanceof ViewGroup viewGroup) {\n            for (int i = 0, n = viewGroup.getChildCount(); i < n; i++) {\n                setEnabledRecursively(viewGroup.getChildAt(i), enabled);\n            }\n        }\n        view.setEnabled(enabled);\n    }\n\n    public static void dumpViewHierarchy(View view, PrintWriter writer) {\n        dumpViewHierarchy(view, writer, \"\");\n    }\n\n    private static void dumpViewHierarchy(View view, PrintWriter writer, String prefix) {\n        writer.write(prefix);\n        writer.write(view.getClass().getName());\n        writer.write('\\n');\n        if (view instanceof ViewGroup viewGroup) {\n            String newPrefix = prefix + \"    \";\n            for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {\n                View child = viewGroup.getChildAt(i);\n                dumpViewHierarchy(child, writer, newPrefix);\n            }\n        }\n        writer.flush();\n    }\n\n    /**\n     * Generate a value suitable for use in {@link View#setId(int)}.\n     * This value will not collide with ID values generated at build time by aapt for R.id.\n     *\n     * @return a generated ID value\n     */\n    public static int generateViewId() {\n        for (; ; ) {\n            final int result = sNextGeneratedId.get();\n            // aapt-generated IDs have the high byte nonzero; clamp to the range under that.\n            int newValue = result + 1;\n            if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.\n            if (sNextGeneratedId.compareAndSet(result, newValue)) {\n                return result;\n            }\n        }\n    }\n\n    /**\n     * Offset this view translationX\n     */\n    public static void translationXBy(View view, float offset) {\n        view.setTranslationX(view.getTranslationX() + offset);\n    }\n\n    /**\n     * Offset this view translationY\n     */\n    public static void translationYBy(View view, float offset) {\n        view.setTranslationY(view.getTranslationY() + offset);\n    }\n\n    /**\n     * The visual right position of this view, in pixels. This is equivalent to the\n     * {@link View#setTranslationX(float) translationX} property plus the current\n     * {@link View#getRight() right} property.\n     *\n     * @return The visual right position of this view, in pixels.\n     */\n    public static float getX2(View view) {\n        return view.getRight() + view.getTranslationX();\n    }\n\n    /**\n     * The visual bottom position of this view, in pixels. This is equivalent to the\n     * {@link View#setTranslationY(float) translationY} property plus the current\n     * {@link View#getBottom()} () bottom} property.\n     *\n     * @return The visual bottom position of this view, in pixels.\n     */\n    public static float getY2(View view) {\n        return view.getBottom() + view.getTranslationY();\n    }\n\n    @NonNull\n    public static View $$(Activity activity, @IdRes int id) {\n        View result = activity.findViewById(id);\n        if (null == result) {\n            throw new NullPointerException(\"Can't find view with id: \" + id);\n        }\n        return result;\n    }\n\n    @NonNull\n    public static View $$(Dialog dialog, @IdRes int id) {\n        View result = dialog.findViewById(id);\n        if (null == result) {\n            throw new NullPointerException(\"Can't find view with id: \" + id);\n        }\n        return result;\n    }\n\n    @NonNull\n    public static View $$(View view, @IdRes int id) {\n        View result = view.findViewById(id);\n        if (null == result) {\n            throw new NullPointerException(\"Can't find view with id: \" + id);\n        }\n        return result;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/collect/IntList.kt",
    "content": "/*\n * Copyright 2015 Hippo Seven\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.hippo.yorozuya.collect\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Suppress(\"JavaDefaultMethodsNotOverriddenByDelegation\")\n@Parcelize\nclass IntList @JvmOverloads constructor(\n    private val delegate: MutableList<Int> = mutableListOf(),\n) : Parcelable, MutableList<Int> by delegate {\n    override val size: Int\n        get() = delegate.size\n\n    override fun isEmpty(): Boolean = delegate.isEmpty()\n\n    override fun clear() = delegate.clear()\n\n    override fun add(element: Int): Boolean = delegate.add(element)\n\n    override fun removeAt(index: Int): Int = delegate.removeAt(index)\n\n    fun getInternalArray(): IntArray = delegate.toIntArray()\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/collect/LongList.kt",
    "content": "/*\n * Copyright 2016 Hippo Seven\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.hippo.yorozuya.collect\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Suppress(\"JavaDefaultMethodsNotOverriddenByDelegation\")\n@Parcelize\nclass LongList(\n    private val delegate: MutableList<Long> = mutableListOf(),\n) : Parcelable, MutableList<Long> by delegate\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/thread/InfiniteThreadExecutor.java",
    "content": "/*\n * Copyright 2015-2016 Hippo Seven\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\npackage com.hippo.yorozuya.thread;\n\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.Queue;\nimport java.util.concurrent.Executor;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class InfiniteThreadExecutor implements Executor {\n    private final long mKeepAliveMillis;\n    private final Queue<Runnable> mWorkQueue;\n    private final ThreadFactory mThreadFactory;\n\n    private final AtomicInteger mThreadCount = new AtomicInteger();\n    private final Object mLock = new Object();\n    private int mEmptyThreadCount;\n\n    public InfiniteThreadExecutor(long keepAliveMillis, Queue<Runnable> workQueue, ThreadFactory threadFactory) {\n        mKeepAliveMillis = keepAliveMillis;\n        mWorkQueue = workQueue;\n        mThreadFactory = threadFactory;\n    }\n\n    @Override\n    public void execute(@NonNull Runnable command) {\n        synchronized (mLock) {\n            mWorkQueue.add(command);\n            if (mEmptyThreadCount > 0) {\n                --mEmptyThreadCount;\n                mLock.notify();\n                return;\n            }\n        }\n\n        mThreadFactory.newThread(new Task()).start();\n    }\n\n    public int getThreadCount() {\n        return mThreadCount.get();\n    }\n\n    private class Task implements Runnable {\n        @Override\n        public void run() {\n            mThreadCount.incrementAndGet();\n\n            boolean hasWait = false;\n            for (; ; ) {\n                Runnable command;\n                synchronized (mLock) {\n                    command = mWorkQueue.poll();\n                    if (command == null) {\n                        if (hasWait) {\n                            --mEmptyThreadCount;\n                        }\n                        break;\n                    }\n                }\n\n                try {\n                    command.run();\n                } catch (Exception e) {\n                    Log.e(\"InfiniteThreadExecutor\", \"Error running command\", e);\n                }\n\n                synchronized (mLock) {\n                    ++mEmptyThreadCount;\n                    try {\n                        mLock.wait(mKeepAliveMillis);\n                    } catch (InterruptedException e) {\n                        // Ignore\n                    }\n                }\n\n                hasWait = true;\n            }\n\n            mThreadCount.decrementAndGet();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/thread/PriorityThread.java",
    "content": "/*\n * Copyright 2015-2016 Hippo Seven\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\npackage com.hippo.yorozuya.thread;\n\nimport android.os.Process;\n\npublic class PriorityThread extends Thread {\n    private final int mPriority;\n\n    public PriorityThread(Runnable runnable, int priority) {\n        super(runnable);\n        mPriority = priority;\n    }\n\n    public PriorityThread(Runnable runnable, String name, int priority) {\n        super(runnable, name);\n        mPriority = priority;\n    }\n\n    @Override\n    public void run() {\n        Process.setThreadPriority(mPriority);\n        super.run();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/hippo/yorozuya/thread/PriorityThreadFactory.java",
    "content": "/*\n * Copyright 2015-2016 Hippo Seven\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\npackage com.hippo.yorozuya.thread;\n\nimport androidx.annotation.NonNull;\n\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * A thread factory that creates threads with a given thread priority.\n */\npublic class PriorityThreadFactory implements ThreadFactory {\n    private final int mPriority;\n    private final AtomicInteger mIdGenerator = new AtomicInteger();\n    private final String mName;\n\n    public PriorityThreadFactory(String name, int priority) {\n        mName = name;\n        mPriority = priority;\n    }\n\n    @Override\n    public Thread newThread(@NonNull Runnable r) {\n        return new PriorityThread(r, mName + \"-\" + mIdGenerator.getAndIncrement(), mPriority);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt",
    "content": "package eu.kanade.tachiyomi.network.interceptor\n\nimport android.content.Context\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebResourceResponse\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport android.widget.Toast\nimport androidx.core.content.ContextCompat\nimport com.hippo.ehviewer.R\nimport com.hippo.ehviewer.client.EhCookieStore\nimport com.hippo.ehviewer.client.exception.CloudflareBypassException\nimport com.hippo.ehviewer.util.isWebViewOutdated\nimport com.hippo.util.launchIO\nimport com.hippo.util.launchUI\nimport java.io.IOException\nimport java.util.concurrent.CountDownLatch\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport okhttp3.Interceptor\nimport okhttp3.Request\nimport okhttp3.Response\nimport org.jsoup.Jsoup\n\nclass CloudflareInterceptor(val context: Context) : WebViewInterceptor(context) {\n    private val executor = ContextCompat.getMainExecutor(context)\n\n    override fun shouldIntercept(response: Response): Boolean {\n        // Check if Cloudflare anti-bot is on\n        return if (response.code in ERROR_CODES && response.header(\"Server\") in SERVER_CHECK) {\n            val document = Jsoup.parse(\n                response.peekBody(Long.MAX_VALUE).string(),\n                response.request.url.toString(),\n            )\n\n            // solve with webview only on captcha, not on geo block\n            document.getElementById(\"challenge-error-title\") != null ||\n                document.getElementById(\"challenge-error-text\") != null\n        } else {\n            false\n        }\n    }\n\n    @OptIn(DelicateCoroutinesApi::class)\n    override fun intercept(\n        chain: Interceptor.Chain,\n        request: Request,\n        response: Response,\n    ): Response {\n        // Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that\n        // we don't crash the entire app\n        return runCatching {\n            response.close()\n            launchIO { EhCookieStore.deleteCookie(request.url, EhCookieStore.KEY_CLOUDFLARE) }\n            resolveWithWebView(request)\n            chain.proceed(request)\n        }.getOrElse { throw IOException(it) }\n    }\n\n    @OptIn(DelicateCoroutinesApi::class)\n    private fun resolveWithWebView(originalRequest: Request) {\n        // We need to lock this thread until the WebView finds the challenge solution url, because\n        // OkHttp doesn't support asynchronous interceptors.\n        val latch = CountDownLatch(1)\n\n        var webview: WebView? = null\n\n        var challengeFound = false\n        var cloudflareBypassed = false\n\n        val origRequestUrl = originalRequest.url.toString()\n        val headers = parseHeaders(originalRequest.headers)\n        EhCookieStore.loadForWebView(origRequestUrl) {\n            it.name != EhCookieStore.KEY_CLOUDFLARE\n        }\n\n        executor.execute {\n            webview = createWebView()\n\n            webview.webViewClient = object : WebViewClient() {\n                override fun onPageFinished(view: WebView, url: String) {\n                    cloudflareBypassed = EhCookieStore.saveFromWebView(origRequestUrl) {\n                        it.name == EhCookieStore.KEY_CLOUDFLARE\n                    }\n\n                    if (cloudflareBypassed) {\n                        latch.countDown()\n                    }\n\n                    if (url == origRequestUrl && !challengeFound) {\n                        // The first request didn't return the challenge, abort.\n                        latch.countDown()\n                    }\n                }\n\n                override fun onReceivedHttpError(\n                    view: WebView?,\n                    request: WebResourceRequest?,\n                    errorResponse: WebResourceResponse?,\n                ) {\n                    if (request?.isForMainFrame == true) {\n                        if (errorResponse?.statusCode in ERROR_CODES) {\n                            // Found the Cloudflare challenge page.\n                            challengeFound = true\n                        } else {\n                            // Unlock thread, the challenge wasn't found.\n                            latch.countDown()\n                        }\n                    }\n                }\n            }\n\n            webview.loadUrl(origRequestUrl, headers)\n        }\n\n        latch.awaitFor30Seconds()\n\n        executor.execute {\n            webview?.run {\n                stopLoading()\n                destroy()\n            }\n        }\n\n        // Throw exception if we failed to bypass Cloudflare\n        if (!cloudflareBypassed) {\n            // Prompt user to update WebView if it seems too outdated\n            if (isWebViewOutdated) {\n                launchUI { Toast.makeText(context, R.string.information_webview_outdated, Toast.LENGTH_LONG).show() }\n            }\n\n            throw CloudflareBypassException()\n        }\n    }\n}\n\nprivate val ERROR_CODES = listOf(403, 503)\nprivate val SERVER_CHECK = arrayOf(\"cloudflare-nginx\", \"cloudflare\")\n"
  },
  {
    "path": "app/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt",
    "content": "package eu.kanade.tachiyomi.network.interceptor\n\nimport android.content.Context\nimport android.webkit.WebSettings\nimport android.webkit.WebView\nimport com.hippo.ehviewer.util.setDefaultSettings\nimport java.util.Locale\nimport java.util.concurrent.CountDownLatch\nimport java.util.concurrent.TimeUnit\nimport okhttp3.Headers\nimport okhttp3.Interceptor\nimport okhttp3.Request\nimport okhttp3.Response\n\nabstract class WebViewInterceptor(private val context: Context) : Interceptor {\n    /**\n     * When this is called, it initializes the WebView if it wasn't already. We use this to avoid\n     * blocking the main thread too much. If used too often we could consider moving it to the\n     * Application class.\n     */\n    private val initWebView by lazy {\n        try {\n            WebSettings.getDefaultUserAgent(context)\n        } catch (_: Exception) {\n            // Avoid some crashes like when Chrome/WebView is being updated.\n        }\n    }\n\n    abstract fun shouldIntercept(response: Response): Boolean\n\n    abstract fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response\n\n    override fun intercept(chain: Interceptor.Chain): Response {\n        val request = chain.request()\n        val response = chain.proceed(request)\n        if (!shouldIntercept(response)) {\n            return response\n        }\n\n        initWebView\n\n        return intercept(chain, request, response)\n    }\n\n    fun parseHeaders(headers: Headers): Map<String, String> = headers\n        // Keeping unsafe header makes webview throw [net::ERR_INVALID_ARGUMENT]\n        .filter { (name, value) ->\n            isRequestHeaderSafe(name, value)\n        }\n        .groupBy(keySelector = { (name, _) -> name }) { (_, value) -> value }\n        .mapValues { it.value.getOrNull(0).orEmpty() }\n\n    fun CountDownLatch.awaitFor30Seconds() {\n        await(30, TimeUnit.SECONDS)\n    }\n\n    fun createWebView(): WebView = WebView(context).apply {\n        setDefaultSettings()\n    }\n}\n\n// Based on [IsRequestHeaderSafe] in https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc\nprivate fun isRequestHeaderSafe(name: String, value: String): Boolean {\n    val name = name.lowercase(Locale.ENGLISH)\n    val value = value.lowercase(Locale.ENGLISH)\n    if (name in unsafeHeaderNames || name.startsWith(\"proxy-\")) return false\n    return !(name == \"connection\" && value == \"upgrade\")\n}\n\nprivate val unsafeHeaderNames = arrayOf(\n    \"content-length\",\n    \"host\",\n    \"trailer\",\n    \"te\",\n    \"upgrade\",\n    \"cookie2\",\n    \"keep-alive\",\n    \"transfer-encoding\",\n    \"set-cookie\",\n)\n"
  },
  {
    "path": "app/src/main/res/anim/accelerate_quart.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<accelerateInterpolator xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:factor=\"2.0\" />\n"
  },
  {
    "path": "app/src/main/res/anim/decelerate_quart.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<decelerateInterpolator xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:factor=\"2.0\" />\n"
  },
  {
    "path": "app/src/main/res/anim/decelerate_quint.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<decelerateInterpolator xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:factor=\"2.5\" />\n"
  },
  {
    "path": "app/src/main/res/anim/scene_close_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\">\n\n    <alpha\n        android:duration=\"150\"\n        android:fillAfter=\"true\"\n        android:fillBefore=\"false\"\n        android:fillEnabled=\"true\"\n        android:fromAlpha=\"1.0\"\n        android:interpolator=\"@android:interpolator/linear\"\n        android:startOffset=\"100\"\n        android:toAlpha=\"0.0\" />\n\n    <translate\n        android:duration=\"250\"\n        android:fillAfter=\"true\"\n        android:fillBefore=\"true\"\n        android:fillEnabled=\"true\"\n        android:fromYDelta=\"0%\"\n        android:interpolator=\"@anim/accelerate_quart\"\n        android:toYDelta=\"8%\" />\n\n</set>\n"
  },
  {
    "path": "app/src/main/res/anim/scene_open_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\">\n\n    <alpha\n        android:duration=\"200\"\n        android:fillAfter=\"true\"\n        android:fillBefore=\"false\"\n        android:fillEnabled=\"true\"\n        android:fromAlpha=\"0.0\"\n        android:interpolator=\"@anim/decelerate_quart\"\n        android:toAlpha=\"1.0\" />\n\n    <translate\n        android:duration=\"350\"\n        android:fillAfter=\"true\"\n        android:fillBefore=\"true\"\n        android:fillEnabled=\"true\"\n        android:fromYDelta=\"8%\"\n        android:interpolator=\"@anim/decelerate_quint\"\n        android:toYDelta=\"0\" />\n\n</set>\n"
  },
  {
    "path": "app/src/main/res/anim/scene_open_enter_horizontal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<set xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shareInterpolator=\"false\">\n\n    <alpha\n        android:duration=\"200\"\n        android:fillAfter=\"true\"\n        android:fillBefore=\"false\"\n        android:fillEnabled=\"true\"\n        android:fromAlpha=\"0.0\"\n        android:interpolator=\"@anim/decelerate_quart\"\n        android:toAlpha=\"1.0\" />\n\n    <translate\n        android:duration=\"350\"\n        android:fillAfter=\"true\"\n        android:fillBefore=\"true\"\n        android:fillEnabled=\"true\"\n        android:fromXDelta=\"8%\"\n        android:interpolator=\"@anim/decelerate_quint\"\n        android:toXDelta=\"0\" />\n\n</set>\n"
  },
  {
    "path": "app/src/main/res/anim/scene_open_exit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <alpha\n        android:duration=\"217\"\n        android:fillAfter=\"true\"\n        android:fillBefore=\"false\"\n        android:fillEnabled=\"true\"\n        android:fromAlpha=\"1.0\"\n        android:interpolator=\"@android:interpolator/accelerate_decelerate\"\n        android:toAlpha=\"0.7\" />\n\n</set>\n"
  },
  {
    "path": "app/src/main/res/color/content_reactive.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item android:color=\"@color/content_activated\" android:state_activated=\"true\" />\n    <item android:color=\"@color/content\" />\n\n</selector>\n"
  },
  {
    "path": "app/src/main/res/color/content_reactive_black.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item android:color=\"@color/content_activated_black\" android:state_activated=\"true\" />\n    <item android:color=\"@color/content_black\" />\n\n</selector>\n"
  },
  {
    "path": "app/src/main/res/color/primary_text_material_black.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item android:color=\"@color/primary_text_disabled_material_black\" android:state_enabled=\"false\" />\n    <item android:color=\"@color/primary_text_default_material_black\" />\n</selector>\n"
  },
  {
    "path": "app/src/main/res/drawable/big_download.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"120dp\"\n    android:height=\"120dp\"\n    android:tint=\"?drawableColorSecondary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_big_download\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/big_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"120dp\"\n    android:height=\"120dp\"\n    android:tint=\"?drawableColorSecondary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_big_filter\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/big_history.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"120dp\"\n    android:height=\"120dp\"\n    android:tint=\"?drawableColorSecondary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_big_history\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/big_sad_pandroid.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"120dp\"\n    android:height=\"120dp\"\n    android:tint=\"?drawableColorSecondary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M16.5,10.8c-0.61,0-1.1,0.49-1.1,1.1c0,0.61,0.49,1.1,1.1,1.1s1.1-0.49,1.1-1.1C17.6,11.29,17.11,10.8,16.5,10.8z\" />\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M8.2,10.1c-0.61,0-1.1,0.49-1.1,1.1s0.49,1.1,1.1,1.1s1.1-0.49,1.1-1.1S8.81,10.1,8.2,10.1z\" />\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M22.1,16.4c-0.46-3.33-2.24-5.93-4.81-7.45L19.5,6.3c0.1-0.1,0.1-0.3,0-0.4c-0.1-0.1-0.3-0.1-0.4,0l-2.27,2.79c-1.26-0.66-2.69-1.07-4.23-1.19c-1.4-0.15-2.77-0.01-4.05,0.37L6.7,4.7C6.6,4.6,6.4,4.5,6.3,4.6C6.2,4.7,6.1,4.9,6.2,5l1.79,3.05c-2.83,1.02-5.12,3.3-6.19,6.55l15.44,1.37c0,0.02-0.01,0.04-0.01,0.05c0,0.59,0.48,1.07,1.07,1.07c0.54,0,0.97-0.41,1.04-0.93L22.1,16.4z M2.63,14.07c1.51-3.74,4.83-6.03,8.84-6.03c0.35,0,0.71,0.02,1.07,0.06c4.55,0.36,7.94,3.33,8.84,7.64l-2.12-0.19c-0.27-0.68-0.95-1.46-0.95-1.46s-0.58,0.66-0.89,1.29L2.63,14.07z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/category_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <color android:color=\"@android:color/white\" />\n    </item>\n    <item android:drawable=\"?android:attr/selectableItemBackground\" />\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/check_text_view_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item>\n        <selector\n            android:enterFadeDuration=\"@android:integer/config_shortAnimTime\"\n            android:exitFadeDuration=\"@android:integer/config_shortAnimTime\">\n            <item android:state_checked=\"true\">\n                <color android:color=\"@color/check_text_view_mask\" />\n            </item>\n        </selector>\n    </item>\n    <item android:drawable=\"?android:attr/selectableItemBackground\" />\n</layer-list>"
  },
  {
    "path": "app/src/main/res/drawable/default_avatar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"64dp\"\n    android:height=\"64dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@color/teal_500\"\n        android:pathData=\"M0,12a12,12 0 1,0 24,0a12,12 0 1,0 -24,0\" />\n\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M16.5,9.9L16.1,9.6L19.1,5.9C19.2,5.8,19.4,5.8,19.5,5.9L19.5,5.9C19.6,6,19.6,6.2,19.5,6.3L16.5,9.9Z\" />\n\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M9.1,8.8L8.6,9.1L6.2,5C6.1,4.9,6.2,4.7,6.3,4.6L6.3,4.6C6.4,4.5,6.6,4.6,6.7,4.7L9.1,8.8Z\" />\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M1.8,14.6L22.1,16.4C21.4,11.3,17.6,7.9,12.6,7.5C7.8,7,3.4,9.7,1.8,14.6Z\" />\n\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M15.4,11.9a1.1,1.1 0 1,0 2.2,0a1.1,1.1 0 1,0 -2.2,0\" />\n\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M7.1,11.2a1.1,1.1 0 1,0 2.2,0a1.1,1.1 0 1,0 -2.2,0\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/divider_gallery_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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 xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <solid android:color=\"@color/divider\" />\n\n    <size android:height=\"1dp\" />\n\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/divider_gallery_detail_dark.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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 xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <solid android:color=\"@color/divider_black\" />\n\n    <size android:height=\"1dp\" />\n\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_dark_mode_24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9c0,-0.46 -0.04,-0.92 -0.1,-1.36c-0.98,1.37 -2.58,2.26 -4.4,2.26c-2.98,0 -5.4,-2.42 -5.4,-5.4c0,-1.81 0.89,-3.42 2.26,-4.4C12.92,3.04 12.46,3 12,3L12,3z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_format_list_numbered_24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M2,17h2v0.5L3,17.5v1h1v0.5L2,19v1h3v-4L2,16v1zM3,8h1L4,4L2,4v1h1v3zM2,11h1.8L2,13.1v0.9h3v-1L3.2,13L5,10.9L5,10L2,10v1zM7,5v2h14L21,5L7,5zM7,19h14v-2L7,17v2zM7,13h14v-2L7,11v2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_menu_24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M3,18h18v-2L3,16v2zM3,13h18v-2L3,11v2zM3,6v2h18L21,6L3,6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_reorder_24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M3,15h18v-2L3,13v2zM3,19h18v-2L3,17v2zM3,11h18L21,9L3,9v2zM3,5v2h18L21,5L3,5z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_warning_24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <group\n        android:scaleX=\"0.37947997\"\n        android:scaleY=\"0.37947997\"\n        android:translateX=\"35.1\"\n        android:translateY=\"39.584126\">\n        <path\n            android:fillColor=\"@color/ic_launcher_foreground\"\n            android:fillType=\"evenOdd\"\n            android:pathData=\"M39.893,68.066L39.893,75.977L0,75.977L0,4.59L39.893,4.59L39.893,12.5L8.984,12.5L8.984,34.766L38.086,34.766L38.086,42.578L8.984,42.578L8.984,68.066L39.893,68.066Z\" />\n        <path\n            android:fillColor=\"@color/ic_launcher_foreground\"\n            android:fillType=\"evenOdd\"\n            android:pathData=\"M99.609,75.977L90.918,75.977L90.918,41.699A23.436,23.436 0,0 0,90.662 38.109Q90.055,34.207 88.013,31.958Q85.629,29.334 81.028,28.863A20.84,20.84 0,0 0,78.906 28.76A23.134,23.134 0,0 0,74.45 29.16Q71.926,29.656 69.994,30.767A11.122,11.122 0,0 0,66.968 33.301A13.32,13.32 0,0 0,64.756 37.276Q63.399,40.971 63.213,46.495A53.517,53.517 0,0 0,63.184 48.291L63.184,75.977L54.395,75.977L54.395,0L63.184,0L63.184,22.266A66.935,66.935 0,0 1,63.115 25.383Q63.043,26.914 62.897,28.251A33.547,33.547 0,0 1,62.695 29.785L63.281,29.785A16.11,16.11 0,0 1,69.394 24.035A19.448,19.448 0,0 1,70.068 23.682A20.88,20.88 0,0 1,77.292 21.622A25.763,25.763 0,0 1,79.98 21.484A32.643,32.643 0,0 1,85.483 21.917Q91.249,22.905 94.678,26.147Q98.472,29.735 99.347,36.631A35.274,35.274 0,0 1,99.609 41.064L99.609,75.977Z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_monochrome.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Tarsin Norbin\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"44dp\"\n    android:height=\"44dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <group\n        android:scaleX=\"0.37947997\"\n        android:scaleY=\"0.37947997\"\n        android:translateX=\"35.1\"\n        android:translateY=\"39.584126\">\n        <path\n            android:fillColor=\"@android:color/white\"\n            android:fillType=\"evenOdd\"\n            android:pathData=\"M39.893,68.066L39.893,75.977L0,75.977L0,4.59L39.893,4.59L39.893,12.5L8.984,12.5L8.984,34.766L38.086,34.766L38.086,42.578L8.984,42.578L8.984,68.066L39.893,68.066Z\" />\n        <path\n            android:fillColor=\"@android:color/white\"\n            android:fillType=\"evenOdd\"\n            android:pathData=\"M99.609,75.977L90.918,75.977L90.918,41.699A23.436,23.436 0,0 0,90.662 38.109Q90.055,34.207 88.013,31.958Q85.629,29.334 81.028,28.863A20.84,20.84 0,0 0,78.906 28.76A23.134,23.134 0,0 0,74.45 29.16Q71.926,29.656 69.994,30.767A11.122,11.122 0,0 0,66.968 33.301A13.32,13.32 0,0 0,64.756 37.276Q63.399,40.971 63.213,46.495A53.517,53.517 0,0 0,63.184 48.291L63.184,75.977L54.395,75.977L54.395,0L63.184,0L63.184,22.266A66.935,66.935 0,0 1,63.115 25.383Q63.043,26.914 62.897,28.251A33.547,33.547 0,0 1,62.695 29.785L63.281,29.785A16.11,16.11 0,0 1,69.394 24.035A19.448,19.448 0,0 1,70.068 23.682A20.88,20.88 0,0 1,77.292 21.622A25.763,25.763 0,0 1,79.98 21.484A32.643,32.643 0,0 1,85.483 21.917Q91.249,22.905 94.678,26.147Q98.472,29.735 99.347,36.631A35.274,35.274 0,0 1,99.609 41.064L99.609,75.977Z\" />\n    </group>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_pause_108dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillAlpha=\"1\"\n        android:fillColor=\"@color/colorPrimary\"\n        android:pathData=\"m42,68h8V40H42ZM58,40v28h8V40Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_play_arrow_108dp.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillAlpha=\"1\"\n        android:fillColor=\"@color/colorPrimary\"\n        android:pathData=\"M46,40V68L68,54Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/image_failed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_file_image\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/round_side_rect.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"?colorControlHighlight\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <solid android:color=\"@android:color/white\" />\n            <corners android:radius=\"64dp\" />\n        </shape>\n    </item>\n</ripple>"
  },
  {
    "path": "app/src/main/res/drawable/spacer_keyline.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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 xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <size\n        android:width=\"@dimen/keyline_margin\"\n        android:height=\"@dimen/keyline_margin\" />\n\n    <solid android:color=\"@android:color/transparent\" />\n\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/spacer_x6.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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 xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <size\n        android:width=\"6dp\"\n        android:height=\"6dp\" />\n\n    <solid android:color=\"@android:color/transparent\" />\n\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/tile_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<selector xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item android:drawable=\"@drawable/tile_background_activated\" android:state_activated=\"true\" />\n    <item android:drawable=\"@drawable/transparent\" />\n\n</selector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_adb_primary_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_adb\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_archive_primary_x48.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_archive\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_arrow_left_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_arrow_left\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_book_open_primary_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_book_open\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_book_open_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_book_open\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_check_all_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_check_all\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_check_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_check\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_clear_all_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_clear_all\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_close_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_close\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_cookie_brown_x48.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"@color/brown_500\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_cookie\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_delete_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_delete\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_dots_vertical_secondary_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_dots_vertical\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_download_box_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_download_box\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_download_primary_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_download\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_download_x16.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:tint=\"?android:attr/textColorSecondary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_download\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_download_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_download\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_eh_subscription_black_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_eh_subscription\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_filter_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_fire_black_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_fire\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_folder_move_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_folder_move\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_go_to_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_go_to\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_heart_box_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_heart_box\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_heart_broken_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_heart_broken\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_heart_outline_primary_x48.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_heart_outline\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_heart_primary_x48.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_heart\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_heart_x16.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:tint=\"?android:attr/textColorSecondary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_heart\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_heart_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_heart\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_help_circle_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_help_circle\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_history_black_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_history\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_homepage_black_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_homepage\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_info_outline_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_info_outline\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_info_primary_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_info\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_last_page_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Tarsin Norbin\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M7,18 L5.6,16.6 10.2,12 5.6,7.4 7,6 13,12ZM16,18V6H18V18Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_magnify_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_magnify\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_pause_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_pause\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_pencil_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_pencil\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_pin_top_24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2023 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"1024\"\n    android:viewportHeight=\"1024\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M818.3,102.1H205.7a51,51 0,0 1,0 -102.1h612.6a51,51 0,0 1,0 102.1zM243,594.2L461,373.7v599.3a51,51 0,0 0,102.1 0V373.7l218,220a51,51 0,0 0,72 0,46.5 46.5,0 0,0 0,-67.9l-306.3,-309.3a55.1,55.1 0,0 0,-74.5 0l-306.3,309.3a46.5,46.5 0,0 0,0 67.9,51 51,0 0,0 77.1,0.5z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_play_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_play\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_plus_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_plus\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_refresh_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_refresh\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_reply_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_reply\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_sad_panda_primary_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_sad_panda\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_sec_primary_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_send_dark_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_send\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_settings_black_x24.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_settings\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_share_primary_x48.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_share\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_similar_primary_x48.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_similar\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_slider_bubble.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"@dimen/slider_bubble_width\"\n    android:height=\"@dimen/slider_bubble_height\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"26\"\n    android:viewportHeight=\"32\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_slider_bubble\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_star_half_x16.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:tint=\"@color/yellow_800\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_star_half\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_star_outline_x16.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:tint=\"@color/yellow_800\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_star_outline\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_star_x16.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16dp\"\n    android:height=\"16dp\"\n    android:tint=\"@color/yellow_800\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_star\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/v_utorrent_primary_x48.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:tint=\"?drawableColorThemePrimary\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"@string/pd_utorrent\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v25/ic_shortcut_start.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"48\"\n    android:viewportHeight=\"48\">\n    <path\n        android:fillAlpha=\"1\"\n        android:fillColor=\"@color/shortcut_bg\"\n        android:pathData=\"M46,24A22,22 0,0 1,24 46,22 22,0 0,1 2,24 22,22 0,0 1,24 2,22 22,0 0,1 46,24Z\" />\n    <path\n        android:fillAlpha=\"1\"\n        android:fillColor=\"@color/colorPrimary\"\n        android:pathData=\"M20,17V31L31,24Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v25/ic_shortcut_stop.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"48dp\"\n    android:height=\"48dp\"\n    android:viewportWidth=\"48\"\n    android:viewportHeight=\"48\">\n    <path\n        android:fillAlpha=\"1\"\n        android:fillColor=\"@color/shortcut_bg\"\n        android:pathData=\"M46,24A22,22 0,0 1,24 46,22 22,0 0,1 2,24 22,22 0,0 1,24 2,22 22,0 0,1 46,24Z\" />\n    <path\n        android:fillAlpha=\"1\"\n        android:fillColor=\"@color/colorPrimary\"\n        android:pathData=\"m18,31h4L22,17h-4zM26,17v14h4L30,17Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v26/ic_shortcut_start.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/shortcut_bg\" />\n    <foreground android:drawable=\"@drawable/ic_play_arrow_108dp\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/drawable-v26/ic_shortcut_stop.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/shortcut_bg\" />\n    <foreground android:drawable=\"@drawable/ic_pause_108dp\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/layout/activity_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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\n    <com.hippo.easyrecyclerview.EasyRecyclerView\n        android:id=\"@+id/recycler_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipToPadding=\"false\"\n        android:paddingBottom=\"@dimen/fab_rv_padding_size\"\n        app:fitsSystemWindowsInsets=\"bottom\" />\n\n    <TextView\n        android:id=\"@+id/tip\"\n        style=\"@style/TextAppearance.AppCompat.Medium\"\n        android:layout_width=\"228dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"16dp\"\n        android:gravity=\"center_horizontal\"\n        android:text=\"@string/no_filter\"\n        app:fitsSystemWindowsInsets=\"bottom\" />\n\n    <com.google.android.material.floatingactionbutton.FloatingActionButton\n        android:id=\"@+id/fab\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:layout_marginEnd=\"@dimen/corner_fab_margin\"\n        android:layout_marginBottom=\"@dimen/corner_fab_margin\"\n        app:layout_fitsSystemWindowsInsets=\"bottom\"\n        app:srcCompat=\"@drawable/v_plus_dark_x24\"\n        style=\"@style/Widget.FloatingActionButton\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_gallery.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/main\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.hippo.glview.view.GLRootView\n        android:id=\"@+id/gl_root_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:visibility=\"gone\" />\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/gl_loading\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        app:indicatorColor=\"?attr/colorPrimary\" />\n\n    <com.hippo.ehviewer.widget.GalleryHeader\n        android:id=\"@+id/gallery_header\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"top\">\n\n        <com.hippo.widget.TextClock\n            android:id=\"@+id/clock\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"right\"\n            android:layout_marginTop=\"@dimen/gallery_widget_margin_v\"\n            android:layout_marginRight=\"@dimen/gallery_widget_margin_h\"\n            android:textColor=\"?android:attr/textColorSecondary\" />\n\n        <TextView\n            android:id=\"@+id/progress\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"@dimen/gallery_widget_margin_v\"\n            android:textColor=\"?android:attr/textColorSecondary\" />\n\n        <com.hippo.widget.BatteryView\n            android:id=\"@+id/battery\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"left\"\n            android:layout_marginLeft=\"@dimen/gallery_widget_margin_h\"\n            android:layout_marginTop=\"@dimen/gallery_widget_margin_v\"\n            android:drawablePadding=\"4dp\"\n            app:color=\"?android:attr/textColorSecondary\"\n            app:warningColor=\"@color/red_500\" />\n\n    </com.hippo.ehviewer.widget.GalleryHeader>\n\n    <com.hippo.ehviewer.widget.SeekBarPanel\n        android:id=\"@+id/seek_bar_panel\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom\"\n        android:background=\"?attr/gallerySliderBackgroundColor\"\n        android:orientation=\"horizontal\"\n        android:paddingHorizontal=\"16dp\"\n        android:visibility=\"invisible\">\n\n        <TextView\n            android:id=\"@+id/left\"\n            android:layout_width=\"32dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:gravity=\"center_horizontal\" />\n\n        <FrameLayout\n            android:layout_width=\"0dp\"\n            android:layout_height=\"48dp\"\n            android:layout_weight=\"1\">\n\n            <com.hippo.ehviewer.widget.ReversibleSeekBar\n                android:id=\"@+id/seek_bar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\" />\n\n        </FrameLayout>\n\n        <TextView\n            android:id=\"@+id/right\"\n            android:layout_width=\"32dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:gravity=\"center_horizontal\" />\n\n        <ImageView\n            android:id=\"@+id/auto_transfer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"8dp\"\n            app:srcCompat=\"@drawable/v_play_x24\"\n            app:tint=\"?attr/colorPrimary\" />\n\n    </com.hippo.ehviewer.widget.SeekBarPanel>\n\n    <com.hippo.widget.ColorView\n        android:id=\"@+id/mask\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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:clipChildren=\"false\"\n    app:consumeSystemWindowsInsets=\"start|end\"\n    app:edgeToEdge=\"true\"\n    app:fitsSystemWindowsInsets=\"start|end\">\n\n    <androidx.drawerlayout.widget.DrawerLayout\n        android:id=\"@+id/draw_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fitsSystemWindows=\"true\">\n\n        <com.hippo.widget.IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:fitsSystemWindows=\"true\">\n\n            <androidx.coordinatorlayout.widget.CoordinatorLayout\n                android:id=\"@+id/snackbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <com.hippo.ehviewer.widget.EhStageLayout\n                    android:id=\"@+id/fragment_container\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\" />\n            </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n        </com.hippo.widget.IgnoreFitsSystemWindowsFullyDraggableDrawerContentLayout>\n\n        <com.google.android.material.navigation.NavigationView\n            android:id=\"@+id/nav_view\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"start\"\n            android:background=\"?android:attr/colorBackground\"\n            app:headerLayout=\"@layout/nav_header_main\"\n            app:insetForeground=\"@null\"\n            app:itemShapeAppearanceOverlay=\"@style/ShapeAppearanceOverlay.MaterialComponents.NavigationView\"\n            app:itemShapeInsetBottom=\"0dp\"\n            app:itemShapeInsetStart=\"0dp\"\n            app:itemShapeInsetTop=\"0dp\"\n            app:menu=\"@menu/nav_drawer_main\" />\n\n        <com.hippo.widget.DrawerView\n            android:id=\"@+id/right_drawer\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"end\"\n            android:fitsSystemWindows=\"false\"\n            android:maxWidth=\"@dimen/drawer_max_width\" />\n\n    </androidx.drawerlayout.widget.DrawerLayout>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_preference.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\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=\"match_parent\"\n    android:clipChildren=\"false\"\n    android:orientation=\"vertical\"\n    app:consumeSystemWindowsInsets=\"start|end\"\n    app:edgeToEdge=\"true\"\n    app:fitsSystemWindowsInsets=\"start|end\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?attr/toolbarColor\"\n        android:elevation=\"4dp\"\n        android:fitsSystemWindows=\"true\"\n        app:statusBarForeground=\"?colorPrimaryDark\">\n\n        <androidx.appcompat.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_height=\"?attr/actionBarSize\"\n            android:layout_width=\"match_parent\"\n            android:theme=\"@style/ThemeOverlay.MaterialComponents.Dark.ActionBar\"\n            app:popupTheme=\"?attr/toolbarPopupTheme\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <androidx.coordinatorlayout.widget.CoordinatorLayout\n        android:id=\"@+id/snackbar\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <androidx.fragment.app.FragmentContainerView\n            android:id=\"@+id/fragment\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:clipChildren=\"false\" />\n    </androidx.coordinatorlayout.widget.CoordinatorLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_set_security.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\"\n    app:fitsSystemWindowsInsets=\"bottom\">\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:padding=\"@dimen/keyline_margin\"\n        android:text=\"@string/set_pattern_protection_tip\"\n        android:textAppearance=\"@style/TextAppearance.AppCompat.Medium\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\">\n\n        <com.hippo.widget.MaxSizeContainer\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_margin=\"32dp\"\n            app:maxHeight=\"320dp\"\n            app:maxWidth=\"320dp\">\n\n            <com.hippo.widget.lockpattern.LockPatternView\n                android:id=\"@+id/pattern_view\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n        </com.hippo.widget.MaxSizeContainer>\n    </FrameLayout>\n\n    <androidx.appcompat.widget.AppCompatCheckBox\n        android:id=\"@+id/fingerprint_checkbox\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:layout_marginBottom=\"20dp\"\n        android:text=\"@string/enable_biometric\"\n        android:visibility=\"gone\" />\n\n    <LinearLayout\n        android:id=\"@+id/buttonPanel\"\n        style=\"?attr/buttonBarStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:clipToPadding=\"false\"\n        android:gravity=\"bottom\"\n        android:orientation=\"horizontal\"\n        android:paddingHorizontal=\"12dp\"\n        android:paddingTop=\"4dp\"\n        android:paddingBottom=\"20dp\">\n\n        <Button\n            android:id=\"@+id/cancel\"\n            style=\"@style/Widget.MaterialComponents.Button.OutlinedButton\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"4dp\"\n            android:layout_weight=\"1\"\n            android:text=\"@android:string/cancel\" />\n\n        <Button\n            android:id=\"@+id/set\"\n            style=\"@style/Widget.MaterialComponents.Button\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"4dp\"\n            android:layout_weight=\"1\"\n            android:text=\"@string/set\" />\n\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_webview.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<FrameLayout 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:animateLayoutChanges=\"true\"\n    android:orientation=\"vertical\"\n    app:fitsSystemWindowsInsets=\"bottom\">\n\n    <WebView\n        android:id=\"@+id/webview\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:visibility=\"gone\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_add_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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    android:paddingHorizontal=\"?attr/dialogPreferredPadding\"\n    android:paddingTop=\"@dimen/abc_dialog_padding_top_material\">\n\n    <com.hippo.widget.CuteSpinner\n        android:id=\"@+id/spinner\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:entries=\"@array/filter_entries\" />\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:id=\"@+id/text_input_layout\"\n        style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:layout_marginTop=\"16dp\"\n        android:hint=\"@string/filter_text\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            style=\"@style/FixedLineHeightEditText\"\n            android:id=\"@+id/edit_text\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:imeOptions=\"actionDone\"\n            android:inputType=\"text\"\n            android:maxLines=\"1\"\n            android:singleLine=\"true\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_archive_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/abc_dialog_padding_top_material\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:textAppearance=\"@style/TextAppearance.AppCompat.Medium\" />\n\n    <ListView\n        android:id=\"@+id/list_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_checkbox_builder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"?selectableItemBackground\"\n    android:minHeight=\"?listPreferredItemHeightSmall\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"?attr/dialogPreferredPadding\">\n\n    <androidx.appcompat.widget.AppCompatCheckBox\n        android:id=\"@+id/checkbox\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:background=\"@null\"\n        android:clickable=\"false\"\n        android:focusable=\"false\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_edittext_builder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<com.google.android.material.textfield.TextInputLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingHorizontal=\"?attr/dialogPreferredPadding\"\n    android:paddingTop=\"@dimen/abc_dialog_padding_top_material\">\n\n    <com.google.android.material.textfield.TextInputEditText\n        style=\"@style/FixedLineHeightEditText\"\n        android:id=\"@+id/edit_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:imeOptions=\"actionDone\"\n        android:inputType=\"text\"\n        android:maxLines=\"1\"\n        android:singleLine=\"true\" />\n\n</com.google.android.material.textfield.TextInputLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_edittextcheckbox_builder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\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    android:paddingHorizontal=\"?attr/dialogPreferredPadding\"\n    android:paddingTop=\"@dimen/abc_dialog_padding_top_material\">\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:id=\"@+id/text_input_layout\"\n        style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            style=\"@style/FixedLineHeightEditText\"\n            android:id=\"@+id/edit_text\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:imeOptions=\"actionDone\"\n            android:inputType=\"text\"\n            android:maxLines=\"1\"\n            android:singleLine=\"true\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:minHeight=\"?listPreferredItemHeightSmall\"\n        android:orientation=\"vertical\">\n\n        <androidx.appcompat.widget.AppCompatCheckBox\n            android:id=\"@+id/checkbox\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:background=\"@null\"\n            android:clickable=\"false\"\n            android:focusable=\"false\" />\n\n    </FrameLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_gallery_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<androidx.core.widget.NestedScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\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:divider=\"@drawable/spacer_keyline\"\n        android:orientation=\"vertical\"\n        android:paddingHorizontal=\"?dialogPreferredPadding\"\n        android:paddingTop=\"@dimen/abc_dialog_padding_top_material\"\n        android:showDividers=\"middle\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\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:text=\"@string/settings_read_screen_rotation\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <com.hippo.widget.CuteSpinner\n                android:id=\"@+id/screen_rotation\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"2\"\n                android:entries=\"@array/screen_rotation_entries\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\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:text=\"@string/settings_read_reading_direction\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <com.hippo.widget.CuteSpinner\n                android:id=\"@+id/reading_direction\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"2\"\n                android:entries=\"@array/reading_direction_entries\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\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:text=\"@string/settings_read_page_scaling\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <com.hippo.widget.CuteSpinner\n                android:id=\"@+id/page_scaling\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"2\"\n                android:entries=\"@array/page_scaling_entries\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\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:text=\"@string/settings_read_start_position\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <com.hippo.widget.CuteSpinner\n                android:id=\"@+id/start_position\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"2\"\n                android:entries=\"@array/start_position_entries\" />\n\n        </LinearLayout>\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\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:text=\"@string/settings_read_theme\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <com.hippo.widget.CuteSpinner\n                android:id=\"@+id/read_theme\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"2\"\n                android:entries=\"@array/read_theme_entries\" />\n\n        </LinearLayout>\n\n        <Switch\n            android:id=\"@+id/keep_screen_on\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_keep_screen_on\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <Switch\n            android:id=\"@+id/show_clock\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_show_clock\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <Switch\n            android:id=\"@+id/show_progress\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_show_progress\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <Switch\n            android:id=\"@+id/show_battery\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_show_battery\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <Switch\n            android:id=\"@+id/show_page_interval\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_show_page_interval\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"4\"\n                android:text=\"@string/settings_read_turn_page_interval\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <SeekBar\n                android:id=\"@+id/turn_page_interval\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"5\"\n                android:max=\"14\" />\n\n        </LinearLayout>\n\n        <Switch\n            android:id=\"@+id/volume_page\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_volume_page\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\">\n\n            <TextView\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_weight=\"4\"\n                android:text=\"@string/settings_read_volume_page_interval\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <SeekBar\n                android:id=\"@+id/volume_page_interval\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"5\"\n                android:max=\"14\" />\n\n        </LinearLayout>\n\n        <Switch\n            android:id=\"@+id/reverse_volume_page\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_reverse_volume\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <Switch\n            android:id=\"@+id/reading_fullscreen\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_reading_fullscreen\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <Switch\n            android:id=\"@+id/custom_screen_lightness\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/settings_read_custom_screen_lightness\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <SeekBar\n            android:id=\"@+id/screen_lightness\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:max=\"200\" />\n\n    </LinearLayout>\n</androidx.core.widget.NestedScrollView>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_go_to.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"horizontal\"\n    android:paddingHorizontal=\"?attr/dialogPreferredPadding\"\n    android:paddingTop=\"@dimen/abc_dialog_padding_top_material\">\n\n    <TextView\n        android:id=\"@+id/start\"\n        android:layout_width=\"32dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:gravity=\"center_horizontal\" />\n\n    <com.hippo.widget.Slider\n        android:id=\"@+id/slider\"\n        style=\"@style/Slider\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"48dp\"\n        android:layout_gravity=\"center_horizontal\"\n        android:layout_weight=\"1\" />\n\n    <TextView\n        android:id=\"@+id/end\"\n        android:layout_width=\"32dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:gravity=\"center_horizontal\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_item_select_with_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n     Copyright (C) 2014 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\n<!--\n    This layout file is used by the AlertDialog when displaying a list of items.\n    This layout file is inflated and used as the TextView to display individual\n    items.\n-->\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/text1\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:drawablePadding=\"@dimen/keyline_margin\"\n    android:ellipsize=\"marquee\"\n    android:gravity=\"center_vertical\"\n    android:minHeight=\"?attr/listPreferredItemHeightSmall\"\n    android:paddingLeft=\"?attr/listPreferredItemPaddingLeft\"\n    android:paddingRight=\"?attr/listPreferredItemPaddingRight\"\n    android:textAppearance=\"?attr/textAppearanceListItemSmall\"\n    android:textColor=\"?attr/textColorAlertDialogListItem\" />\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_js_prompt.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<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    android:padding=\"@dimen/keyline_margin\">\n\n    <TextView\n        android:id=\"@+id/message\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"@style/TextAppearance.AppCompat.Subhead\" />\n\n    <EditText\n        style=\"@style/FixedLineHeightEditText\"\n        android:id=\"@+id/value\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/keyline_margin\"\n        android:inputType=\"text\"\n        android:importantForAutofill=\"no\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_list_checkbox_builder.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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    <com.hippo.widget.IndicatingListView\n        android:id=\"@+id/list_view\"\n        style=\"@style/Indicating\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\"\n        android:cacheColorHint=\"@null\"\n        android:clipToPadding=\"false\"\n        android:divider=\"?attr/listDividerAlertDialog\"\n        android:fadingEdge=\"none\"\n        android:overScrollMode=\"ifContentScrolls\"\n        android:paddingVertical=\"8dp\"\n        android:scrollbars=\"vertical\" />\n\n    <androidx.appcompat.widget.AppCompatCheckBox\n        android:id=\"@+id/checkbox\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"?attr/dialogPreferredPadding\"\n        android:layout_marginBottom=\"@dimen/abc_dialog_padding_top_material\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_rate.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"16dp\">\n\n    <TextView\n        android:id=\"@+id/rating_text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"16dp\"\n        android:gravity=\"center_horizontal\"\n        android:textColor=\"?android:attr/textColorPrimary\"\n        android:textSize=\"@dimen/text_little_small\" />\n\n    <com.hippo.ehviewer.widget.GalleryRatingBar\n        android:id=\"@+id/rating_view\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:theme=\"@style/RatingBarTheme\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_recycler_view.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<com.hippo.easyrecyclerview.EasyRecyclerView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    style=\"@style/Indicating\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:paddingVertical=\"@dimen/abc_dialog_padding_top_material\" />\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_torrent_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingVertical=\"@dimen/abc_dialog_padding_top_material\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:textAppearance=\"@style/TextAppearance.AppCompat.Medium\" />\n\n    <ListView\n        android:id=\"@+id/list_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/drawer_list_rv.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<com.google.android.material.card.MaterialCardView 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:orientation=\"vertical\"\n    app:cardBackgroundColor=\"?android:attr/windowBackground\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:orientation=\"vertical\">\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/toolbarColor\"\n            android:elevation=\"4dp\"\n            android:saveEnabled=\"false\"\n            android:theme=\"@style/ThemeOverlay.MaterialComponents.Dark.ActionBar\"\n            app:popupTheme=\"?attr/toolbarPopupTheme\" />\n\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\">\n\n            <TextView\n                android:id=\"@+id/tip\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center\"\n                android:visibility=\"gone\" />\n\n            <com.hippo.easyrecyclerview.EasyRecyclerView\n                android:id=\"@+id/recycler_view_drawer\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\" />\n\n        </FrameLayout>\n    </LinearLayout>\n</com.google.android.material.card.MaterialCardView>"
  },
  {
    "path": "app/src/main/res/layout/gallery_detail_actions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/actions\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:paddingTop=\"@dimen/keyline_margin\">\n\n    <TextView\n        android:id=\"@+id/newerVersion\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:gravity=\"center\"\n        android:padding=\"8dp\"\n        android:text=\"@string/newer_version_avaliable\"\n        android:textColor=\"?attr/textColorThemeAccent\"\n        android:visibility=\"gone\" />\n\n    <HorizontalScrollView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:clipToPadding=\"false\"\n        android:paddingHorizontal=\"@dimen/keyline_margin\"\n        android:scrollbars=\"none\">\n\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:divider=\"@drawable/spacer_keyline\"\n            android:orientation=\"horizontal\"\n            android:showDividers=\"middle\">\n\n            <FrameLayout\n                android:id=\"@+id/heart_group\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:focusable=\"true\">\n\n                <TextView\n                    android:id=\"@+id/heart\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:drawablePadding=\"8dp\"\n                    android:gravity=\"center_horizontal\"\n                    android:text=\"@string/favorited\"\n                    android:textColor=\"?android:attr/textColorPrimary\" />\n\n                <TextView\n                    android:id=\"@+id/heart_outline\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:drawablePadding=\"8dp\"\n                    android:gravity=\"center_horizontal\"\n                    android:text=\"@string/not_favorited\"\n                    android:textColor=\"?android:attr/textColorPrimary\" />\n\n            </FrameLayout>\n\n            <TextView\n                android:id=\"@+id/share\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:drawablePadding=\"8dp\"\n                android:focusable=\"true\"\n                android:gravity=\"center_horizontal\"\n                android:text=\"@string/share\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <TextView\n                android:id=\"@+id/torrent\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:drawablePadding=\"8dp\"\n                android:focusable=\"true\"\n                android:gravity=\"center_horizontal\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <TextView\n                android:id=\"@+id/archive\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:drawablePadding=\"8dp\"\n                android:focusable=\"true\"\n                android:gravity=\"center_horizontal\"\n                android:text=\"@string/archive\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <TextView\n                android:id=\"@+id/similar\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:drawablePadding=\"8dp\"\n                android:focusable=\"true\"\n                android:gravity=\"center_horizontal\"\n                android:text=\"@string/similar_gallery\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n        </LinearLayout>\n    </HorizontalScrollView>\n\n    <LinearLayout\n        android:id=\"@+id/rate\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\"\n        android:orientation=\"vertical\"\n        android:paddingBottom=\"@dimen/keyline_margin\">\n\n        <androidx.appcompat.widget.AppCompatRatingBar\n            android:id=\"@+id/rating\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"8dp\"\n            android:isIndicator=\"true\"\n            android:theme=\"@style/RatingBarTheme\" />\n\n        <TextView\n            android:id=\"@+id/rating_text\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/gallery_detail_comments.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/comments\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:clickable=\"true\"\n    android:divider=\"@drawable/spacer_keyline\"\n    android:focusable=\"true\"\n    android:orientation=\"vertical\"\n    android:padding=\"@dimen/keyline_margin\"\n    android:showDividers=\"middle\">\n\n    <TextView\n        android:id=\"@+id/comments_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:textColor=\"?attr/textColorThemeAccent\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/gallery_detail_content.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <include layout=\"@layout/gallery_detail_header\" />\n\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <LinearLayout\n            android:id=\"@+id/below_header\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:divider=\"?attr/galleryDetailDivider\"\n            android:dividerPadding=\"@dimen/keyline_margin\"\n            android:orientation=\"vertical\"\n            android:showDividers=\"middle\">\n\n            <include layout=\"@layout/gallery_detail_info\" />\n\n            <include layout=\"@layout/gallery_detail_actions\" />\n\n            <include layout=\"@layout/gallery_detail_tags\" />\n\n            <include layout=\"@layout/gallery_detail_comments\" />\n\n            <include layout=\"@layout/gallery_detail_previews\" />\n\n        </LinearLayout>\n\n        <com.google.android.material.progressindicator.CircularProgressIndicator\n            android:id=\"@+id/progress\"\n            style=\"@style/ProgressView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_margin=\"@dimen/keyline_margin\" />\n\n    </FrameLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/gallery_detail_header.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/header\"\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=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <View\n            android:id=\"@+id/color_bg\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"0dp\"\n            android:layout_weight=\"1\"\n            android:background=\"?attr/galleryDetailHeaderBackgroundColor\"\n            android:elevation=\"4dp\"\n            app:fitsSystemWindowsInsets=\"top\" />\n\n        <Space\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"28dp\" />\n\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <RelativeLayout\n            android:id=\"@+id/header_content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"@android:color/transparent\"\n            app:fitsSystemWindowsInsets=\"top\">\n\n            <com.hippo.widget.LoadImageView\n                android:id=\"@+id/thumb\"\n                android:layout_width=\"@dimen/gallery_detail_thumb_width\"\n                android:layout_height=\"@dimen/gallery_detail_thumb_height\"\n                android:layout_marginLeft=\"@dimen/keyline_margin\"\n                android:layout_marginTop=\"48dp\"\n                android:layout_marginBottom=\"@dimen/keyline_margin\" />\n\n            <TextView\n                android:id=\"@+id/title\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_alignTop=\"@id/thumb\"\n                android:layout_marginHorizontal=\"@dimen/keyline_margin\"\n                android:layout_toRightOf=\"@id/thumb\"\n                android:ellipsize=\"end\"\n                android:maxLines=\"5\"\n                android:textColor=\"@android:color/white\"\n                android:textSize=\"@dimen/text_little_large\" />\n\n            <TextView\n                android:id=\"@+id/uploader\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_below=\"@id/title\"\n                android:layout_alignLeft=\"@id/title\"\n                android:layout_marginTop=\"8dp\"\n                android:layout_marginRight=\"@dimen/keyline_margin\"\n                android:background=\"?selectableItemBackground\"\n                android:ellipsize=\"end\"\n                android:singleLine=\"true\"\n                android:textColor=\"@android:color/white\"\n                android:textSize=\"@dimen/text_little_small\" />\n\n            <TextView\n                android:id=\"@+id/category\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_below=\"@id/uploader\"\n                android:layout_alignLeft=\"@id/title\"\n                android:layout_marginTop=\"12dp\"\n                android:layout_marginRight=\"@dimen/keyline_margin\"\n                android:background=\"@drawable/category_background\"\n                android:ellipsize=\"end\"\n                android:paddingHorizontal=\"8dp\"\n                android:paddingVertical=\"2dp\"\n                android:singleLine=\"true\"\n                android:textAllCaps=\"true\"\n                android:textStyle=\"bold\" />\n\n            <ImageView\n                android:id=\"@+id/back_action\"\n                android:layout_width=\"48dp\"\n                android:layout_height=\"48dp\"\n                android:layout_alignParentTop=\"true\"\n                android:layout_alignParentLeft=\"true\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"12dp\"\n                app:srcCompat=\"@drawable/v_arrow_left_dark_x24\"\n                app:tint=\"@color/primary_drawable_black\" />\n\n            <ImageView\n                android:id=\"@+id/other_actions\"\n                android:layout_width=\"48dp\"\n                android:layout_height=\"48dp\"\n                android:layout_alignParentTop=\"true\"\n                android:layout_alignParentRight=\"true\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"12dp\"\n                app:srcCompat=\"@drawable/v_dots_vertical_secondary_dark_x24\"\n                app:tint=\"@color/primary_drawable_black\" />\n\n        </RelativeLayout>\n\n        <com.google.android.material.card.MaterialCardView\n            android:id=\"@+id/action_card\"\n            style=\"@style/CardView.Normal\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginHorizontal=\"@dimen/keyline_margin\"\n            app:cardBackgroundColor=\"?attr/galleryDetailButtonBackgroundColor\"\n            app:cardUseCompatPadding=\"true\">\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:orientation=\"horizontal\">\n\n                <TextView\n                    android:id=\"@+id/download\"\n                    style=\"@style/ButtonInCard\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:clickable=\"true\"\n                    android:focusable=\"true\"\n                    android:textColor=\"?attr/textColorThemePrimary\" />\n\n                <View\n                    android:layout_width=\"1dp\"\n                    android:layout_height=\"match_parent\"\n                    android:layout_marginVertical=\"8dp\"\n                    android:background=\"?attr/dividerColor\" />\n\n                <TextView\n                    android:id=\"@+id/read\"\n                    style=\"@style/ButtonInCard\"\n                    android:layout_width=\"0dp\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:clickable=\"true\"\n                    android:focusable=\"true\"\n                    android:text=\"@string/read\"\n                    android:textColor=\"?attr/textColorThemeAccent\" />\n\n            </LinearLayout>\n        </com.google.android.material.card.MaterialCardView>\n    </LinearLayout>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/gallery_detail_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/info\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"?selectableItemBackground\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:orientation=\"vertical\"\n    android:padding=\"@dimen/keyline_margin\">\n\n    <TableLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:paddingHorizontal=\"@dimen/keyline_margin\">\n\n        <TableRow>\n\n            <TextView\n                android:id=\"@+id/language\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"left\"\n                android:singleLine=\"true\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <TextView\n                android:id=\"@+id/pages\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:gravity=\"center\"\n                android:singleLine=\"true\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <TextView\n                android:id=\"@+id/size\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"right\"\n                android:singleLine=\"true\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n        </TableRow>\n\n        <TableRow android:paddingTop=\"8dp\">\n\n            <TextView\n                android:id=\"@+id/favoredTimes\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"left\"\n                android:singleLine=\"true\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n            <TextView\n                android:id=\"@+id/posted\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:gravity=\"right\"\n                android:singleLine=\"true\"\n                android:textColor=\"?android:attr/textColorPrimary\" />\n\n        </TableRow>\n    </TableLayout>\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=\"16dp\"\n        android:text=\"@string/more_information\"\n        android:textColor=\"?attr/textColorThemeAccent\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/gallery_detail_previews.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/previews\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:clickable=\"true\"\n    android:divider=\"@drawable/spacer_keyline\"\n    android:focusable=\"true\"\n    android:orientation=\"vertical\"\n    android:padding=\"@dimen/keyline_margin\"\n    android:showDividers=\"middle\"\n    app:fitsSystemWindowsInsets=\"bottom\">\n\n    <com.hippo.widget.SimpleGridAutoSpanLayout\n        android:id=\"@+id/grid_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        app:columnCount=\"3\"\n        app:itemMargin=\"8dp\" />\n\n    <TextView\n        android:id=\"@+id/preview_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:textColor=\"?attr/textColorThemeAccent\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/gallery_detail_tags.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tags\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:padding=\"@dimen/keyline_margin\">\n\n    <TextView\n        android:id=\"@+id/no_tags\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:text=\"@string/no_tags\"\n        android:textColor=\"?attr/textColorThemeAccent\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/gallery_tag_group.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"horizontal\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\" />\n"
  },
  {
    "path": "app/src/main/res/layout/item_cute_spinner_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/text1\"\n    style=\"?attr/spinnerDropDownItemStyle\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:ellipsize=\"marquee\"\n    android:singleLine=\"true\" />\n"
  },
  {
    "path": "app/src/main/res/layout/item_download.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    style=\"@style/CardView.Reactive\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"2dp\"\n    android:background=\"?selectableItemBackground\"\n    android:checkable=\"true\"\n    android:clickable=\"true\"\n    android:focusable=\"true\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.hippo.ehviewer.widget.ResizeableFixedThumb\n            android:id=\"@+id/thumb\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            app:alwaysCutAndScale=\"true\"\n            app:retryType=\"longClick\" />\n\n        <TextView\n            android:id=\"@+id/title\"\n            style=\"@style/CardTitle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentTop=\"true\"\n            android:layout_marginHorizontal=\"8dp\"\n            android:layout_marginTop=\"4dp\"\n            android:layout_toRightOf=\"@id/thumb\" />\n\n        <TextView\n            android:id=\"@+id/uploader\"\n            style=\"@style/CardMessage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/title\"\n            android:layout_alignLeft=\"@id/title\"\n            android:layout_marginTop=\"2dp\" />\n\n        <com.hippo.ehviewer.widget.SimpleRatingView\n            android:id=\"@+id/rating\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/uploader\"\n            android:layout_alignLeft=\"@id/title\"\n            android:layout_marginTop=\"2dp\" />\n\n        <TextView\n            android:id=\"@+id/category\"\n            style=\"@style/CardMessage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignLeft=\"@id/title\"\n            android:layout_alignBottom=\"@id/thumb\"\n            android:layout_marginBottom=\"8dp\"\n            android:paddingHorizontal=\"8dp\"\n            android:paddingVertical=\"2dp\"\n            android:textAllCaps=\"true\"\n            android:textColor=\"@android:color/white\" />\n\n        <FrameLayout\n            android:id=\"@+id/actions\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignBottom=\"@+id/thumb\"\n            android:layout_alignParentRight=\"true\"\n            android:paddingRight=\"8dp\">\n\n            <ImageView\n                android:id=\"@+id/start\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"8dp\"\n                app:srcCompat=\"@drawable/v_play_x24\" />\n\n            <ImageView\n                android:id=\"@+id/stop\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"8dp\"\n                app:srcCompat=\"@drawable/v_pause_x24\" />\n\n        </FrameLayout>\n\n        <FrameLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignBottom=\"@+id/thumb\"\n            android:layout_toLeftOf=\"@id/actions\">\n\n            <ImageView\n                android:id=\"@+id/move\"\n                android:layout_width=\"40dp\"\n                android:layout_height=\"40dp\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"8dp\"\n                app:srcCompat=\"@drawable/ic_baseline_reorder_24\" />\n\n        </FrameLayout>\n\n        <TextView\n            android:id=\"@+id/state\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/actions\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_marginBottom=\"4dp\"\n            android:paddingRight=\"16dp\"\n            android:textColor=\"?attr/textColorThemeAccent\" />\n\n        <ProgressBar\n            android:id=\"@+id/progress_bar\"\n            style=\"?android:attr/progressBarStyleHorizontal\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/actions\"\n            android:layout_alignLeft=\"@id/title\"\n            android:layout_marginRight=\"8dp\"\n            android:indeterminate=\"false\" />\n\n        <TextView\n            android:id=\"@+id/percent\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/progress_bar\"\n            android:layout_alignLeft=\"@id/title\"\n            android:textSize=\"@dimen/text_super_small\" />\n\n        <TextView\n            android:id=\"@+id/speed\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/progress_bar\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_marginRight=\"8dp\"\n            android:textSize=\"@dimen/text_super_small\" />\n\n    </RelativeLayout>\n</com.google.android.material.card.MaterialCardView>\n"
  },
  {
    "path": "app/src/main/res/layout/item_drawer_favorites.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:minHeight=\"?attr/listPreferredItemHeightSmall\"\n    android:orientation=\"horizontal\">\n\n    <TextView\n        android:id=\"@+id/key\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\"\n        android:gravity=\"center_vertical\"\n        android:paddingLeft=\"?attr/listPreferredItemPaddingLeft\"\n        android:textAppearance=\"?attr/textAppearanceListItemSmall\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n    <TextView\n        android:id=\"@+id/value\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_vertical\"\n        android:paddingRight=\"?attr/listPreferredItemPaddingRight\"\n        android:textAppearance=\"?attr/textAppearanceListItemSmall\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_drawer_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\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:background=\"?selectableItemBackground\"\n    android:minHeight=\"?attr/listPreferredItemHeightSmall\"\n    android:orientation=\"horizontal\">\n\n    <TextView\n        android:id=\"@+id/tv_key\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:layout_weight=\"1\"\n        android:paddingLeft=\"?attr/listPreferredItemPaddingLeft\"\n        android:textAppearance=\"?attr/textAppearanceListItemSmall\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n    <ImageView\n        android:id=\"@+id/iv_option\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"center_vertical\"\n        android:clickable=\"true\"\n        android:focusable=\"true\"\n        android:paddingLeft=\"?attr/listPreferredItemPaddingLeft\"\n        android:paddingRight=\"?attr/listPreferredItemPaddingRight\"\n        app:srcCompat=\"@drawable/ic_baseline_reorder_24\" />\n\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\">\n\n    <com.google.android.material.checkbox.MaterialCheckBox\n        android:id=\"@+id/checkbox\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@android:color/transparent\"\n        android:clickable=\"false\"\n        android:ellipsize=\"end\"\n        android:focusable=\"false\"\n        android:maxLines=\"1\"\n        android:paddingStart=\"0dp\"\n        android:paddingEnd=\"24dp\" />\n\n    <ImageView xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n        android:id=\"@+id/delete\"\n        android:layout_width=\"32dp\"\n        android:layout_height=\"32dp\"\n        android:layout_gravity=\"end|center_vertical\"\n        android:background=\"?selectableItemBackgroundBorderless\"\n        android:clickable=\"true\"\n        android:contentDescription=\"@string/delete\"\n        android:focusable=\"true\"\n        android:padding=\"4dp\"\n        app:srcCompat=\"@drawable/v_delete_x24\" />\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/item_filter_header.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/text\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"16dp\"\n    android:paddingVertical=\"8dp\"\n    android:textAppearance=\"@style/TextAppearance.MaterialComponents.Headline6\" />\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_comment.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\"\n    android:paddingVertical=\"8dp\">\n\n    <TextView\n        android:id=\"@+id/user\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"?selectableItemBackground\" />\n\n    <TextView\n        android:id=\"@+id/time\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentRight=\"true\" />\n\n    <com.hippo.widget.LinkifyTextView\n        android:id=\"@+id/comment\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_below=\"@id/user\"\n        android:layout_alignLeft=\"@id/user\"\n        android:layout_marginTop=\"8.0dp\"\n        android:ellipsize=\"end\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_comment_more.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:padding=\"@dimen/keyline_margin\">\n\n    <TextView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:text=\"@string/click_more_comments\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_comment_progress.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:padding=\"@dimen/keyline_margin\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_grid.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/card\"\n    style=\"@style/CardView.Normal\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"2dp\"\n    android:background=\"?selectableItemBackground\"\n    android:checkable=\"true\"\n    android:clickable=\"true\"\n    android:focusable=\"true\">\n\n    <RelativeLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\">\n\n        <com.hippo.ehviewer.widget.TileThumb\n            android:id=\"@+id/thumb\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:foreground=\"@drawable/tile_background\"\n            android:scaleType=\"centerCrop\"\n            app:retryType=\"none\" />\n\n        <TextView\n            android:id=\"@+id/title\"\n            style=\"@style/CardTitle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/thumb\"\n            android:layout_margin=\"4dp\" />\n\n        <com.hippo.ehviewer.widget.SimpleRatingView\n            android:id=\"@+id/rating\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_below=\"@id/title\"\n            android:layout_alignParentLeft=\"true\"\n            android:layout_marginLeft=\"4dp\"\n            android:layout_marginBottom=\"4dp\" />\n\n        <TextView\n            android:id=\"@+id/pages\"\n            style=\"@style/CardMessage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_alignBottom=\"@id/rating\"\n            android:layout_marginRight=\"4dp\" />\n\n    </RelativeLayout>\n\n    <TextView\n        android:id=\"@+id/category\"\n        android:layout_width=\"32dp\"\n        android:layout_height=\"24dp\"\n        android:layout_gravity=\"top|right\" />\n\n    <TextView xmlns:tools=\"http://schemas.android.com/tools\"\n        android:id=\"@+id/simple_language\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"top|right\"\n        android:layout_marginRight=\"2dp\"\n        android:textColor=\"@android:color/white\"\n        android:textStyle=\"bold\"\n        tools:ignore=\"SpUsage\" />\n\n</com.google.android.material.card.MaterialCardView>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_info_data.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:orientation=\"horizontal\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\">\n\n    <TextView\n        android:id=\"@+id/key\"\n        android:layout_width=\"90dp\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"8dp\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n    <TextView\n        android:id=\"@+id/value\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"8dp\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_info_header.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<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=\"horizontal\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\">\n\n    <TextView\n        android:id=\"@+id/key\"\n        android:layout_width=\"90dp\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"8dp\"\n        android:textColor=\"?android:attr/textColorSecondary\" />\n\n    <TextView\n        android:id=\"@+id/value\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:padding=\"8dp\"\n        android:textColor=\"?android:attr/textColorSecondary\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/card\"\n    style=\"@style/CardView.Reactive\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"2dp\"\n    android:background=\"?selectableItemBackground\"\n    android:checkable=\"true\"\n    android:clickable=\"true\"\n    android:focusable=\"true\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <com.hippo.ehviewer.widget.ResizeableFixedThumb\n            android:id=\"@+id/thumb\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"0dp\"\n            app:alwaysCutAndScale=\"true\"\n            app:retryType=\"click\" />\n\n        <TextView\n            android:id=\"@+id/title\"\n            style=\"@style/CardTitle\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentTop=\"true\"\n            android:layout_marginHorizontal=\"8dp\"\n            android:layout_marginTop=\"4dp\"\n            android:layout_toRightOf=\"@id/thumb\" />\n\n        <TextView\n            android:id=\"@+id/category\"\n            style=\"@style/CardMessage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignLeft=\"@id/title\"\n            android:layout_alignBottom=\"@id/thumb\"\n            android:layout_marginBottom=\"8dp\"\n            android:paddingHorizontal=\"8dp\"\n            android:paddingVertical=\"2dp\"\n            android:textAllCaps=\"true\"\n            android:textColor=\"@android:color/white\" />\n\n        <com.hippo.ehviewer.widget.SimpleRatingView\n            android:id=\"@+id/rating\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/category\"\n            android:layout_alignLeft=\"@id/title\"\n            android:layout_marginBottom=\"4dp\" />\n\n        <TextView\n            android:id=\"@+id/uploader\"\n            style=\"@style/CardMessage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@+id/rating\"\n            android:layout_alignLeft=\"@id/title\"\n            android:layout_marginBottom=\"2dp\" />\n\n        <TextView\n            android:id=\"@+id/note\"\n            style=\"@style/CardMessage\"\n            android:textStyle=\"italic\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@+id/rating\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_marginRight=\"6dp\"\n            android:paddingRight=\"2sp\" />\n\n        <TextView\n            android:id=\"@+id/posted\"\n            style=\"@style/CardMessage\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignBottom=\"@id/thumb\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_marginRight=\"6dp\"\n            android:layout_marginBottom=\"4dp\" />\n\n        <LinearLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/posted\"\n            android:layout_alignParentRight=\"true\"\n            android:layout_marginRight=\"6dp\"\n            android:divider=\"@drawable/spacer_x6\"\n            android:gravity=\"center_vertical\"\n            android:orientation=\"horizontal\"\n            android:showDividers=\"middle\">\n\n            <ImageView\n                android:id=\"@+id/downloaded\"\n                android:layout_width=\"16dp\"\n                android:layout_height=\"16dp\"\n                android:visibility=\"gone\"\n                app:srcCompat=\"@drawable/v_download_x16\" />\n\n            <ImageView\n                android:id=\"@+id/favourited\"\n                android:layout_width=\"16dp\"\n                android:layout_height=\"16dp\"\n                android:visibility=\"gone\"\n                app:srcCompat=\"@drawable/v_heart_x16\" />\n\n            <TextView\n                android:id=\"@+id/simple_language\"\n                style=\"@style/CardMessage\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n            <TextView\n                android:id=\"@+id/pages\"\n                style=\"@style/CardMessage\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\" />\n\n        </LinearLayout>\n    </RelativeLayout>\n</com.google.android.material.card.MaterialCardView>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_list_thumb_height.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<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    <TextView\n        style=\"@style/CardTitle\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"4dp\"\n        android:lines=\"2\" />\n\n    <TextView\n        style=\"@style/CardMessage\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"2dp\"\n        android:lines=\"1\" />\n\n    <com.hippo.ehviewer.widget.SimpleRatingView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"2dp\" />\n\n    <TextView\n        android:id=\"@+id/category\"\n        style=\"@style/CardMessage\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"4dp\"\n        android:layout_marginBottom=\"8dp\"\n        android:lines=\"1\"\n        android:paddingHorizontal=\"8dp\"\n        android:paddingVertical=\"2dp\"\n        android:textAllCaps=\"true\"\n        android:textColor=\"@android:color/white\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_preview.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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    <com.hippo.ehviewer.widget.FixedThumb xmlns:auto=\"http://schemas.android.com/apk/res-auto\"\n        android:id=\"@+id/image\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:foreground=\"?selectableItemBackground\"\n        auto:aspect=\"0.667\"\n        auto:maxAspect=\"0.8\"\n        auto:minAspect=\"0.5\"\n        auto:retryType=\"longClick\" />\n\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_gallery_tag.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    style=\"@style/CardMessage\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"4dp\"\n    android:background=\"@drawable/round_side_rect\"\n    android:paddingHorizontal=\"12dp\"\n    android:paddingVertical=\"4dp\"\n    android:textColor=\"@android:color/white\" />\n"
  },
  {
    "path": "app/src/main/res/layout/item_history.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:clipChildren=\"false\"\n    android:clipToPadding=\"false\">\n\n    <include layout=\"@layout/item_gallery_list\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_hosts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:clickable=\"true\"\n    android:focusable=\"true\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\"\n    android:paddingVertical=\"8dp\">\n\n    <TextView\n        android:id=\"@+id/host\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textColor=\"?android:attr/textColorPrimary\" />\n\n    <TextView\n        android:id=\"@+id/ip\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"8dp\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/item_select_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/text1\"\n    style=\"?attr/materialAlertDialogBodyTextStyle\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:ellipsize=\"marquee\"\n    android:gravity=\"center_vertical\"\n    android:minHeight=\"?attr/listPreferredItemHeightSmall\"\n    android:paddingLeft=\"?attr/listPreferredItemPaddingLeft\"\n    android:paddingRight=\"?attr/listPreferredItemPaddingRight\"\n    android:paddingVertical=\"6dp\" />\n"
  },
  {
    "path": "app/src/main/res/layout/item_simple_list_2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"?selectableItemBackground\"\n    android:orientation=\"vertical\"\n    android:paddingLeft=\"?attr/listPreferredItemPaddingLeft\"\n    android:paddingRight=\"?attr/listPreferredItemPaddingRight\"\n    android:paddingVertical=\"8dp\">\n\n    <TextView\n        android:id=\"@android:id/text1\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"4dp\"\n        android:textAppearance=\"?attr/textAppearanceListItemSmall\"\n        android:textColor=\"?android:attr/textColorPrimary\"\n        android:textSize=\"@dimen/text_little_small\" />\n\n    <TextView\n        android:id=\"@android:id/text2\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:textAppearance=\"?attr/textAppearanceListItemSmall\"\n        android:textColor=\"?android:attr/textColorSecondary\"\n        android:textSize=\"@dimen/text_small\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/nav_header_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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=\"160dp\"\n    android:orientation=\"vertical\">\n\n    <ImageView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:contentDescription=\"@null\"\n        android:scaleType=\"centerCrop\"\n        app:srcCompat=\"@drawable/sadpanda_low_poly\" />\n\n    <com.hippo.widget.LoadImageView\n        android:id=\"@+id/avatar\"\n        android:layout_width=\"64dp\"\n        android:layout_height=\"64dp\"\n        android:layout_gravity=\"bottom\"\n        android:layout_marginLeft=\"16dp\"\n        android:layout_marginBottom=\"48dp\"\n        android:scaleType=\"centerCrop\"\n        app:shapeAppearanceOverlay=\"@style/ShapeAppearanceOverlay.App.CornerSize50Percent\" />\n\n    <TextView\n        android:id=\"@+id/display_name\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom\"\n        android:layout_marginLeft=\"16dp\"\n        android:layout_marginBottom=\"16dp\"\n        android:textColor=\"@android:color/white\"\n        android:textSize=\"@dimen/text_small\" />\n\n    <FrameLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom|end\"\n        android:background=\"@android:color/transparent\"\n        android:clipToPadding=\"false\"\n        android:padding=\"8dp\">\n\n        <ImageView\n            android:id=\"@+id/night_mode\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:background=\"?selectableItemBackgroundBorderless\"\n            android:clickable=\"true\"\n            android:focusable=\"true\"\n            android:padding=\"6dp\"\n            app:srcCompat=\"@drawable/ic_baseline_dark_mode_24\"\n            app:tint=\"#fff\" />\n    </FrameLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/preference_dialog_proxy.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"?attr/dialogPreferredPadding\"\n    android:paddingTop=\"18dp\">\n\n    <com.hippo.widget.CuteSpinner\n        android:id=\"@+id/type\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:entries=\"@array/proxy_types\"\n        android:paddingVertical=\"8dp\" />\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:id=\"@+id/ip_input_layout\"\n        style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:hint=\"@string/proxy_host_or_ip\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            style=\"@style/FixedLineHeightEditText\"\n            android:id=\"@+id/ip\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:inputType=\"text\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n\n    <com.google.android.material.textfield.TextInputLayout\n        android:id=\"@+id/port_input_layout\"\n        style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:hint=\"@string/proxy_port\">\n\n        <com.google.android.material.textfield.TextInputEditText\n            style=\"@style/FixedLineHeightEditText\"\n            android:id=\"@+id/port\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:inputType=\"numberDecimal\" />\n\n    </com.google.android.material.textfield.TextInputLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/preference_dialog_task.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"horizontal\"\n    android:paddingHorizontal=\"?attr/dialogPreferredPadding\"\n    android:paddingVertical=\"@dimen/abc_dialog_padding_top_material\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:layout_marginEnd=\"@dimen/abc_dialog_padding_top_material\" />\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_vertical\"\n        android:text=\"@string/please_wait\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/preference_recyclerview.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ This file is part of LSPosed.\n  ~\n  ~ LSPosed 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  ~ LSPosed 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 LSPosed.  If not, see <https://www.gnu.org/licenses/>.\n  ~\n  ~ Copyright (C) 2020 EdXposed Contributors\n  ~ Copyright (C) 2021 LSPosed Contributors\n  -->\n\n<com.hippo.easyrecyclerview.EasyRecyclerView 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=\"@android:id/list\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:clipToPadding=\"false\"\n    android:fadeScrollbars=\"true\"\n    android:scrollbarStyle=\"outsideOverlay\"\n    android:scrollbars=\"vertical\"\n    app:fitsSystemWindowsInsets=\"bottom\"\n    tools:ignore=\"UnusedResources\"\n    tools:viewBindingIgnore=\"true\" />\n"
  },
  {
    "path": "app/src/main/res/layout/scene_cookie_sign_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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:orientation=\"vertical\"\n    app:fitsSystemWindowsInsets=\"top|bottom\">\n\n    <ScrollView\n        android:id=\"@+id/cookie_signin_form\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fillViewport=\"true\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            android:padding=\"@dimen/keyline_margin\">\n\n            <ImageView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:padding=\"@dimen/keyline_margin\"\n                app:srcCompat=\"@drawable/v_cookie_brown_x48\" />\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:paddingHorizontal=\"32dp\"\n                android:paddingTop=\"24dp\"\n                android:text=\"@string/cookie_explain\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"@dimen/text_little_small\" />\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/ipb_member_id_layout\"\n                style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:layout_marginTop=\"@dimen/keyline_margin\"\n                android:hint=\"ipb_member_id\"\n                android:minWidth=\"@dimen/single_max_width\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    style=\"@style/FixedLineHeightEditText\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:imeOptions=\"actionNext\"\n                    android:inputType=\"text\"\n                    android:maxLines=\"1\"\n                    android:singleLine=\"true\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/ipb_pass_hash_layout\"\n                style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:hint=\"ipb_pass_hash\"\n                android:minWidth=\"@dimen/single_max_width\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    style=\"@style/FixedLineHeightEditText\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:imeOptions=\"actionDone\"\n                    android:inputType=\"text\"\n                    android:maxLines=\"1\"\n                    android:singleLine=\"true\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <Space\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\" />\n\n            <LinearLayout\n                android:id=\"@+id/buttonPanel\"\n                style=\"?attr/buttonBarStyle\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clipToPadding=\"false\"\n                android:gravity=\"bottom\"\n                android:orientation=\"vertical\"\n                android:paddingHorizontal=\"12dp\"\n                android:paddingVertical=\"4dp\">\n\n                <Button\n                    android:id=\"@+id/ok\"\n                    style=\"@style/Widget.MaterialComponents.Button\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"@android:string/ok\" />\n\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clipToPadding=\"false\"\n                android:gravity=\"center_horizontal\"\n                android:orientation=\"horizontal\"\n                android:paddingHorizontal=\"12dp\"\n                android:paddingVertical=\"4dp\">\n\n                <TextView\n                    android:id=\"@+id/from_clipboard\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"?selectableItemBackground\"\n                    android:paddingHorizontal=\"8dp\"\n                    android:text=\"@string/from_clipboard\"\n                    android:textColor=\"?attr/textColorThemeAccent\" />\n\n            </LinearLayout>\n        </LinearLayout>\n    </ScrollView>\n\n    <FrameLayout\n        android:id=\"@+id/progress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clickable=\"true\"\n        android:visibility=\"gone\">\n\n        <com.google.android.material.progressindicator.CircularProgressIndicator\n            style=\"@style/ProgressView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\" />\n\n    </FrameLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_download.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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\n    <FrameLayout\n        android:id=\"@+id/content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.hippo.easyrecyclerview.EasyRecyclerView\n            android:id=\"@+id/recycler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:paddingHorizontal=\"@dimen/gallery_content_margin_h\"\n            android:paddingVertical=\"@dimen/gallery_content_margin_v\"\n            app:fitsSystemWindowsInsets=\"bottom\" />\n\n        <com.hippo.easyrecyclerview.FastScroller\n            android:id=\"@+id/fast_scroller\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"right\"\n            android:paddingLeft=\"20dp\"\n            android:paddingRight=\"4dp\"\n            android:paddingVertical=\"8dp\"\n            app:fitsSystemWindowsInsets=\"bottom\" />\n\n    </FrameLayout>\n\n    <TextView\n        android:id=\"@+id/tip\"\n        style=\"@style/TextAppearance.AppCompat.Medium\"\n        android:layout_width=\"228dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"16dp\"\n        android:gravity=\"center_horizontal\"\n        android:text=\"@string/no_download_info\" />\n\n    <com.hippo.widget.FabLayout\n        android:id=\"@+id/fab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingRight=\"@dimen/corner_fab_margin\"\n        android:paddingBottom=\"@dimen/corner_fab_margin\"\n        app:fitsSystemWindowsInsets=\"bottom\">\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_check_all_dark_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_pin_top_24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_play_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_pause_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_delete_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_folder_move_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_close_dark_x24\" />\n\n    </com.hippo.widget.FabLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_favorites.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/main_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <com.hippo.widget.ContentLayout\n        android:id=\"@+id/content_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:fitsSystemWindowsInsets=\"top|bottom\" />\n\n    <com.hippo.ehviewer.widget.SearchBar\n        android:id=\"@+id/search_bar\"\n        style=\"@style/CardView.Normal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"@dimen/gallery_search_bar_margin_h\"\n        android:layout_marginVertical=\"@dimen/gallery_search_bar_margin_v\"\n        app:layout_fitsSystemWindowsInsets=\"top|bottom\" />\n\n    <com.hippo.widget.FabLayout\n        android:id=\"@+id/fab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingRight=\"@dimen/corner_fab_margin\"\n        android:paddingBottom=\"@dimen/corner_fab_margin\"\n        app:fitsSystemWindowsInsets=\"top|bottom\">\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_heart_box_dark_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_go_to_dark_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_last_page_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_refresh_dark_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\"\n            app:srcCompat=\"@drawable/v_check_all_dark_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\"\n            app:srcCompat=\"@drawable/v_download_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\"\n            app:srcCompat=\"@drawable/v_delete_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:visibility=\"gone\"\n            app:srcCompat=\"@drawable/v_folder_move_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n    </com.hippo.widget.FabLayout>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_gallery_comments.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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: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        <com.hippo.easyrecyclerview.EasyRecyclerView\n            android:id=\"@+id/recycler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:clipToPadding=\"false\"\n            android:paddingBottom=\"@dimen/fab_rv_padding_size\"\n            app:fitsSystemWindowsInsets=\"bottom\" />\n\n    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n    <TextView\n        android:id=\"@+id/tip\"\n        style=\"@style/TextAppearance.AppCompat.Medium\"\n        android:layout_width=\"228dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"8dp\"\n        android:gravity=\"center_horizontal\"\n        android:text=\"@string/no_one_comments_gallery\"\n        app:fitsSystemWindowsInsets=\"bottom\" />\n\n    <LinearLayout\n        android:id=\"@+id/edit_panel\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"bottom\"\n        android:background=\"?colorPrimarySurface\"\n        android:elevation=\"4dp\"\n        android:orientation=\"horizontal\"\n        android:visibility=\"invisible\"\n        android:outlineAmbientShadowColor=\"?attr/widgetColorThemePrimary\"\n        android:outlineSpotShadowColor=\"?attr/widgetColorThemePrimary\"\n        app:fitsSystemWindowsInsets=\"bottom\">\n\n        <EditText\n            style=\"@style/FixedLineHeightEditText\"\n            android:id=\"@+id/edit_text\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:background=\"@null\"\n            android:gravity=\"left|center_vertical\"\n            android:inputType=\"textMultiLine\"\n            android:maxHeight=\"168dp\"\n            android:minHeight=\"56dp\"\n            android:textSize=\"16sp\"\n            android:padding=\"@dimen/keyline_margin\"\n            android:textColor=\"?colorOnPrimarySurface\"\n            android:theme=\"@style/CommentEditText\"\n            android:importantForAutofill=\"no\" />\n\n        <ImageView\n            android:id=\"@+id/send\"\n            android:layout_width=\"56dp\"\n            android:layout_height=\"56dp\"\n            android:layout_gravity=\"bottom\"\n            android:background=\"?selectableItemBackgroundBorderless\"\n            android:clickable=\"true\"\n            android:focusable=\"true\"\n            android:padding=\"16dp\"\n            app:tint=\"?colorOnPrimarySurface\" />\n\n    </LinearLayout>\n\n    <com.hippo.widget.FabLayout\n        android:id=\"@+id/fab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingRight=\"@dimen/corner_fab_margin\"\n        android:paddingBottom=\"@dimen/corner_fab_margin\"\n        app:fitsSystemWindowsInsets=\"bottom\">\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/fab\"\n            style=\"@style/Widget.FloatingActionButton\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_reply_dark_x24\" />\n\n    </com.hippo.widget.FabLayout>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_gallery_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/main\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n\n    <ScrollView\n        android:id=\"@+id/scroll_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:overScrollMode=\"never\"\n        android:scrollbars=\"none\">\n\n        <include layout=\"@layout/gallery_detail_content\" />\n\n    </ScrollView>\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress_view\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        app:fitsSystemWindowsInsets=\"top|bottom\" />\n\n    <TextView\n        android:id=\"@+id/tip\"\n        style=\"@style/TextAppearance.AppCompat.Medium\"\n        android:layout_width=\"228dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"8dp\"\n        android:gravity=\"center_horizontal\"\n        app:fitsSystemWindowsInsets=\"top|bottom\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_gallery_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<com.hippo.easyrecyclerview.EasyRecyclerView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/recycler_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    app:fitsSystemWindowsInsets=\"bottom\" />\n"
  },
  {
    "path": "app/src/main/res/layout/scene_gallery_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/main_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:clipChildren=\"false\">\n\n    <com.hippo.widget.ContentLayout\n        android:id=\"@+id/content_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:fitsSystemWindowsInsets=\"top|bottom\" />\n\n    <com.hippo.ehviewer.widget.SearchLayout\n        android:id=\"@+id/search_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        app:layout_fitsSystemWindowsInsets=\"top|bottom\" />\n\n    <com.hippo.ehviewer.widget.SearchBar\n        android:id=\"@+id/search_bar\"\n        style=\"@style/CardView.Normal\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginHorizontal=\"@dimen/gallery_search_bar_margin_h\"\n        android:layout_marginVertical=\"@dimen/gallery_search_bar_margin_v\"\n        app:layout_fitsSystemWindowsInsets=\"top|bottom\" />\n\n    <com.hippo.widget.FabLayout\n        android:id=\"@+id/fab_layout\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingRight=\"@dimen/corner_fab_margin\"\n        android:paddingBottom=\"@dimen/corner_fab_margin\"\n        app:fitsSystemWindowsInsets=\"top|bottom\">\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_magnify_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_go_to_dark_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_last_page_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton.Accent.Mini\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            app:srcCompat=\"@drawable/v_refresh_dark_x24\" />\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            style=\"@style/Widget.FloatingActionButton\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n    </com.hippo.widget.FabLayout>\n\n    <com.hippo.widget.FabLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:paddingRight=\"@dimen/corner_fab_margin\"\n        android:paddingBottom=\"@dimen/corner_fab_margin\"\n        app:fitsSystemWindowsInsets=\"top|bottom\">\n\n        <com.google.android.material.floatingactionbutton.FloatingActionButton\n            android:id=\"@+id/search_fab\"\n            style=\"@style/Widget.FloatingActionButton.Accent\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:scaleX=\"0\"\n            android:scaleY=\"0\"\n            android:visibility=\"invisible\"\n            app:srcCompat=\"@drawable/v_magnify_x24\" />\n\n    </com.hippo.widget.FabLayout>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_gallery_previews.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<com.hippo.widget.ContentLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/content_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:clipChildren=\"false\"\n    android:clipToPadding=\"false\"\n    app:fitsSystemWindowsInsets=\"bottom\" />\n"
  },
  {
    "path": "app/src/main/res/layout/scene_history.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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\n    <FrameLayout\n        android:id=\"@+id/content\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <com.hippo.easyrecyclerview.EasyRecyclerView\n            android:id=\"@+id/recycler_view\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:paddingHorizontal=\"@dimen/gallery_content_margin_h\"\n            android:paddingVertical=\"@dimen/gallery_content_margin_v\"\n            app:fitsSystemWindowsInsets=\"bottom\" />\n\n        <com.hippo.easyrecyclerview.FastScroller\n            android:id=\"@+id/fast_scroller\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"right\"\n            android:paddingLeft=\"20dp\"\n            android:paddingRight=\"4dp\"\n            android:paddingVertical=\"8dp\"\n            app:fitsSystemWindowsInsets=\"bottom\" />\n\n    </FrameLayout>\n\n    <TextView\n        android:id=\"@+id/tip\"\n        style=\"@style/TextAppearance.AppCompat.Medium\"\n        android:layout_width=\"228dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"16dp\"\n        android:gravity=\"center_horizontal\"\n        android:text=\"@string/no_history\"\n        app:fitsSystemWindowsInsets=\"bottom\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_login.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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:orientation=\"vertical\"\n    app:fitsSystemWindowsInsets=\"top|bottom\">\n\n    <ScrollView\n        android:id=\"@+id/login_form\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:fillViewport=\"true\">\n\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            android:padding=\"@dimen/keyline_margin\">\n\n            <ImageView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:padding=\"32dp\"\n                app:srcCompat=\"@mipmap/ic_launcher\" />\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/username_layout\"\n                style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:hint=\"@string/username\"\n                android:minWidth=\"@dimen/single_max_width\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    style=\"@style/FixedLineHeightEditText\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:autofillHints=\"username\"\n                    android:imeOptions=\"actionNext\"\n                    android:inputType=\"text\"\n                    android:maxLines=\"1\"\n                    android:singleLine=\"true\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <com.google.android.material.textfield.TextInputLayout\n                android:id=\"@+id/password_layout\"\n                style=\"@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:hint=\"@string/password\"\n                android:minWidth=\"@dimen/single_max_width\"\n                app:passwordToggleEnabled=\"true\">\n\n                <com.google.android.material.textfield.TextInputEditText\n                    style=\"@style/FixedLineHeightEditText\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:autofillHints=\"password\"\n                    android:imeOptions=\"actionDone\"\n                    android:inputType=\"textPassword\"\n                    android:maxLines=\"1\"\n                    android:singleLine=\"true\" />\n\n            </com.google.android.material.textfield.TextInputLayout>\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:maxWidth=\"@dimen/single_max_width\"\n                android:paddingTop=\"24dp\"\n                android:text=\"@string/app_waring\"\n                android:textColor=\"?android:attr/textColorPrimary\"\n                android:textSize=\"@dimen/text_small\"\n                android:textStyle=\"bold\" />\n\n            <Space\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\" />\n\n            <LinearLayout\n                android:id=\"@+id/buttonPanel\"\n                style=\"?attr/buttonBarStyle\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clipToPadding=\"false\"\n                android:gravity=\"bottom\"\n                android:orientation=\"vertical\"\n                android:paddingHorizontal=\"12dp\"\n                android:paddingVertical=\"4dp\">\n\n                <Button\n                    android:id=\"@+id/sign_in\"\n                    style=\"@style/Widget.MaterialComponents.Button\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"@string/sign_in\" />\n\n                <Button\n                    android:id=\"@+id/register\"\n                    style=\"@style/Widget.MaterialComponents.Button.OutlinedButton\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_weight=\"1\"\n                    android:text=\"@string/register\" />\n            </LinearLayout>\n\n            <LinearLayout\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:clipToPadding=\"false\"\n                android:gravity=\"center_horizontal\"\n                android:orientation=\"horizontal\"\n                android:paddingHorizontal=\"12dp\"\n                android:paddingVertical=\"4dp\">\n\n                <TextView\n                    android:id=\"@+id/sign_in_via_webview\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"?selectableItemBackground\"\n                    android:paddingHorizontal=\"8dp\"\n                    android:text=\"@string/sign_in_via_webview\"\n                    android:textColor=\"?attr/textColorThemeAccent\" />\n\n                <TextView\n                    android:id=\"@+id/sign_in_via_cookies\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:background=\"?selectableItemBackground\"\n                    android:paddingHorizontal=\"8dp\"\n                    android:text=\"@string/sign_in_via_cookies\"\n                    android:textColor=\"?attr/textColorThemeAccent\" />\n\n            </LinearLayout>\n\n            <TextView\n                android:id=\"@+id/tourist_mode\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:background=\"?selectableItemBackground\"\n                android:paddingHorizontal=\"8dp\"\n                android:text=\"@string/tourist_mode\"\n                android:textColor=\"?attr/textColorThemeAccent\" />\n        </LinearLayout>\n    </ScrollView>\n\n    <FrameLayout\n        android:id=\"@+id/progress\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clickable=\"true\"\n        android:visibility=\"gone\">\n\n        <com.google.android.material.progressindicator.CircularProgressIndicator\n            style=\"@style/ProgressView\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\" />\n\n    </FrameLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_progress.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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    app:fitsSystemWindowsInsets=\"top|bottom\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <TextView\n        android:id=\"@+id/tip\"\n        style=\"@style/TextAppearance.AppCompat.Medium\"\n        android:layout_width=\"228dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"8dp\"\n        android:gravity=\"center_horizontal\"\n        android:text=\"@string/no_one_comments_gallery\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_security.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<FrameLayout 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    app:fitsSystemWindowsInsets=\"top|bottom\">\n\n    <com.hippo.widget.MaxSizeContainer\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:layout_margin=\"32dp\"\n        app:maxHeight=\"360dp\"\n        app:maxWidth=\"320dp\">\n\n        <com.hippo.widget.lockpattern.LockPatternView\n            android:id=\"@+id/pattern_view\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n\n    </com.hippo.widget.MaxSizeContainer>\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_select_site.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\"\n    android:paddingTop=\"@dimen/keyline_margin\"\n    app:fitsSystemWindowsInsets=\"top|bottom\">\n\n    <ScrollView\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"0dp\"\n        android:layout_gravity=\"center_horizontal\"\n        android:layout_weight=\"1\"\n        android:clipToPadding=\"false\"\n        android:fillViewport=\"true\"\n        android:paddingVertical=\"@dimen/keyline_margin\">\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=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/select_scene\"\n                android:textAppearance=\"@style/TextAppearance.AppCompat.Title\" />\n\n            <Space\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\" />\n\n            <com.google.android.material.button.MaterialButtonToggleGroup\n                android:id=\"@+id/button_group\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_horizontal\"\n                android:layout_marginBottom=\"@dimen/keyline_margin\"\n                app:selectionRequired=\"true\"\n                app:singleSelection=\"true\">\n\n                <com.google.android.material.button.MaterialButton\n                    android:id=\"@+id/site_e\"\n                    style=\"?attr/materialButtonOutlinedStyle\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"8dp\"\n                    android:text=\"@string/site_e\" />\n\n                <com.google.android.material.button.MaterialButton\n                    android:id=\"@+id/site_ex\"\n                    style=\"?attr/materialButtonOutlinedStyle\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:padding=\"8dp\"\n                    android:text=\"@string/site_ex\" />\n            </com.google.android.material.button.MaterialButtonToggleGroup>\n\n            <TextView\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/select_scene_explain\" />\n\n            <Space\n                android:layout_width=\"0dp\"\n                android:layout_height=\"0dp\"\n                android:layout_weight=\"1\" />\n\n        </LinearLayout>\n    </ScrollView>\n\n    <LinearLayout\n        android:id=\"@+id/buttonPanel\"\n        style=\"?attr/buttonBarStyle\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:clipToPadding=\"false\"\n        android:gravity=\"bottom\"\n        android:orientation=\"horizontal\"\n        android:paddingHorizontal=\"12dp\"\n        android:paddingTop=\"4dp\"\n        android:paddingBottom=\"20dp\">\n\n        <Button\n            android:id=\"@+id/ok\"\n            style=\"@style/Widget.MaterialComponents.Button\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\"\n            android:text=\"@android:string/ok\" />\n\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/scene_toolbar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <com.google.android.material.appbar.AppBarLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:elevation=\"4dp\"\n        android:fitsSystemWindows=\"true\"\n        android:background=\"?attr/toolbarColor\"\n        app:statusBarForeground=\"?colorPrimaryDark\">\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:theme=\"@style/ThemeOverlay.MaterialComponents.Dark.ActionBar\"\n            app:popupTheme=\"?attr/toolbarPopupTheme\" />\n    </com.google.android.material.appbar.AppBarLayout>\n\n    <FrameLayout\n        android:id=\"@+id/content_panel\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"0dp\"\n        android:layout_weight=\"1\" />\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/search_action.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<com.google.android.material.tabs.TabLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/action\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@android:color/transparent\"\n    app:tabIndicator=\"@null\"\n    app:tabMode=\"fixed\">\n\n    <com.google.android.material.tabs.TabItem\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/keyword_search\" />\n\n    <com.google.android.material.tabs.TabItem\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@string/image_search\" />\n\n</com.google.android.material.tabs.TabLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/search_advance.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<com.hippo.ehviewer.widget.AdvanceSearchTable xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/search_advance_search_table\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\" />\n"
  },
  {
    "path": "app/src/main/res/layout/search_category.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    style=\"@style/CardView.Normal\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_margin=\"2dp\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:clipChildren=\"false\"\n        android:clipToPadding=\"false\"\n        android:orientation=\"vertical\"\n        android:paddingHorizontal=\"@dimen/search_category_padding_h\"\n        android:paddingBottom=\"@dimen/search_category_padding_v\">\n\n        <TextView\n            android:id=\"@+id/category_title\"\n            style=\"@style/CategoryTitle\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"@dimen/search_category_title_height\"\n            android:saveEnabled=\"false\" />\n\n        <FrameLayout\n            android:id=\"@+id/category_content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:clipChildren=\"false\" />\n\n    </LinearLayout>\n</com.google.android.material.card.MaterialCardView>\n"
  },
  {
    "path": "app/src/main/res/layout/search_image.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<com.hippo.ehviewer.widget.ImageSearchLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\" />\n"
  },
  {
    "path": "app/src/main/res/layout/search_normal.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<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    <com.hippo.ehviewer.widget.CategoryTable\n        android:id=\"@+id/search_category_table\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"@dimen/search_item_interval\" />\n\n    <com.hippo.widget.RadioGridGroup\n        android:id=\"@+id/normal_search_mode\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:checkedButton=\"@+id/search_normal_search\"\n        android:orientation=\"vertical\"\n        android:paddingTop=\"@dimen/search_item_interval\"\n        app:columnCount=\"2\">\n\n        <com.google.android.material.radiobutton.MaterialRadioButton\n            android:id=\"@+id/search_normal_search\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_normal_search\" />\n\n        <com.google.android.material.radiobutton.MaterialRadioButton\n            android:id=\"@+id/search_subscription_search\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_subscription_search\" />\n\n        <com.google.android.material.radiobutton.MaterialRadioButton\n            android:id=\"@+id/search_specify_uploader\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_specify_uploader\" />\n\n        <com.google.android.material.radiobutton.MaterialRadioButton\n            android:id=\"@+id/search_specify_tag\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_specify_tag\" />\n\n    </com.hippo.widget.RadioGridGroup>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"horizontal\">\n\n        <ImageView\n            android:id=\"@+id/normal_search_mode_help\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"36dp\"\n            android:layout_gravity=\"center\"\n            android:background=\"?selectableItemBackgroundBorderless\"\n            android:padding=\"6dp\"\n            app:srcCompat=\"@drawable/v_help_circle_x24\"\n            tools:ignore=\"ContentDescription\" />\n\n        <Space\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_weight=\"1\" />\n\n        <Switch\n            android:id=\"@+id/search_enable_advance\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:minWidth=\"36dp\"\n            android:minHeight=\"36dp\"\n            android:text=\"@string/search_enable_advance\" />\n\n    </LinearLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_advance_search_table.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginBottom=\"@dimen/advance_search_table_item_margin\">\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginRight=\"@dimen/advance_search_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_sh\" />\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginLeft=\"@dimen/advance_search_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_sto\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginVertical=\"@dimen/advance_search_table_item_margin\">\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginRight=\"@dimen/advance_search_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_sr\" />\n\n        <Spinner\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginLeft=\"@dimen/advance_search_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:entries=\"@array/search_min_rating\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginVertical=\"@dimen/advance_search_table_item_margin\">\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginRight=\"@dimen/advance_search_table_item_margin\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_sp\" />\n\n        <EditText\n            style=\"@style/FixedLineHeightEditText\"\n            android:layout_width=\"64dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginHorizontal=\"@dimen/advance_search_table_item_margin\"\n            android:imeOptions=\"actionNext\"\n            android:inputType=\"number\"\n            android:maxLines=\"1\"\n            android:importantForAutofill=\"no\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginHorizontal=\"@dimen/advance_search_table_item_margin\"\n            android:text=\"@string/search_sp_to\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n\n        <EditText\n            style=\"@style/FixedLineHeightEditText\"\n            android:layout_width=\"64dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:paddingHorizontal=\"@dimen/advance_search_table_item_margin\"\n            android:imeOptions=\"actionNone\"\n            android:inputType=\"number\"\n            android:maxLines=\"1\"\n            android:importantForAutofill=\"no\" />\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=\"@dimen/advance_search_table_item_margin\"\n            android:text=\"@string/search_sp_suffix\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginVertical=\"@dimen/advance_search_table_item_margin\">\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:text=\"@string/search_sf\"\n            android:textColor=\"?android:attr/textColorPrimary\" />\n    </LinearLayout>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"@dimen/advance_search_table_item_margin\">\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginRight=\"@dimen/advance_search_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_sfl\" />\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginHorizontal=\"@dimen/advance_search_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_sfu\" />\n\n        <com.google.android.material.checkbox.MaterialCheckBox\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_vertical\"\n            android:layout_marginLeft=\"@dimen/advance_search_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:minWidth=\"0dp\"\n            android:minHeight=\"0dp\"\n            android:text=\"@string/search_sft\" />\n    </LinearLayout>\n</merge>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_category_table.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <TableRow>\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginRight=\"@dimen/category_table_item_margin\"\n            android:layout_marginBottom=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/doujinshi\"\n            android:text=\"@string/doujinshi\"\n            android:textColor=\"@android:color/white\" />\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"@dimen/category_table_item_margin\"\n            android:layout_marginBottom=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/manga\"\n            android:text=\"@string/manga\"\n            android:textColor=\"@android:color/white\" />\n    </TableRow>\n\n    <TableRow>\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginRight=\"@dimen/category_table_item_margin\"\n            android:layout_marginVertical=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/artist_cg\"\n            android:text=\"@string/artist_cg\"\n            android:textColor=\"@android:color/white\" />\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"@dimen/category_table_item_margin\"\n            android:layout_marginVertical=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/game_cg\"\n            android:text=\"@string/game_cg\"\n            android:textColor=\"@android:color/white\" />\n    </TableRow>\n\n    <TableRow>\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginRight=\"@dimen/category_table_item_margin\"\n            android:layout_marginVertical=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/western\"\n            android:text=\"@string/western\"\n            android:textColor=\"@android:color/white\" />\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"@dimen/category_table_item_margin\"\n            android:layout_marginVertical=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/non_h\"\n            android:text=\"@string/non_h\"\n            android:textColor=\"@android:color/white\" />\n    </TableRow>\n\n    <TableRow>\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginRight=\"@dimen/category_table_item_margin\"\n            android:layout_marginVertical=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/image_set\"\n            android:text=\"@string/image_set\"\n            android:textColor=\"@android:color/white\" />\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"@dimen/category_table_item_margin\"\n            android:layout_marginVertical=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/cosplay\"\n            android:text=\"@string/cosplay\"\n            android:textColor=\"@android:color/white\" />\n    </TableRow>\n\n    <TableRow>\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"@dimen/category_table_item_margin\"\n            android:layout_marginRight=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/asian_porn\"\n            android:text=\"@string/asian_porn\"\n            android:textColor=\"@android:color/white\" />\n\n        <com.hippo.widget.CheckTextView\n            style=\"@style/CategoryText\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginLeft=\"@dimen/category_table_item_margin\"\n            android:layout_marginTop=\"@dimen/category_table_item_margin\"\n            android:layout_weight=\"1\"\n            android:background=\"@color/misc\"\n            android:text=\"@string/misc\"\n            android:textColor=\"@android:color/white\" />\n    </TableRow>\n\n</merge>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_content_layout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <com.google.android.material.progressindicator.CircularProgressIndicator\n        android:id=\"@+id/progress\"\n        style=\"@style/ProgressView\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\" />\n\n    <TextView\n        android:id=\"@+id/tip\"\n        style=\"@style/TextAppearance.AppCompat.Medium\"\n        android:layout_width=\"228dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center\"\n        android:drawablePadding=\"8dp\"\n        android:gravity=\"center_horizontal\" />\n\n    <FrameLayout\n        android:id=\"@+id/content_view\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:clipChildren=\"false\">\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            android:clipChildren=\"false\">\n\n            <com.hippo.easyrecyclerview.EasyRecyclerView\n                android:id=\"@+id/recycler_view\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:paddingHorizontal=\"@dimen/gallery_content_margin_h\"\n                android:paddingBottom=\"@dimen/gallery_content_margin_v\" />\n\n        </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>\n\n        <com.google.android.material.progressindicator.LinearProgressIndicator\n            android:id=\"@+id/bottom_progress\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:indeterminate=\"true\"\n            android:visibility=\"gone\"\n            app:hideAnimationBehavior=\"inward\"\n            app:showAnimationBehavior=\"outward\" />\n\n        <com.hippo.easyrecyclerview.FastScroller\n            android:id=\"@+id/fast_scroller\"\n            android:layout_width=\"30dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_gravity=\"right\"\n            android:paddingLeft=\"20dp\"\n            android:paddingRight=\"4dp\"\n            android:paddingVertical=\"8dp\" />\n\n    </FrameLayout>\n\n</merge>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_gallery_guide_1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <TextView\n        style=\"@style/Guide.Title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:text=\"@string/guide_gallery_left\" />\n\n    <TextView\n        style=\"@style/Guide.Title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:text=\"@string/guide_gallery_right\" />\n\n    <TextView\n        style=\"@style/Guide.Title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:text=\"@string/guide_gallery_menu\" />\n\n    <TextView\n        style=\"@style/Guide.Title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:gravity=\"center\"\n        android:text=\"@string/guide_gallery_progress\" />\n\n</merge>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_gallery_guide_2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <TextView\n        style=\"@style/Guide.Title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center\"\n        android:text=\"@string/guide_gallery_long_click\" />\n\n</merge>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_image_search.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <com.hippo.widget.FixedAspectImageView\n        android:id=\"@+id/preview\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:adjustViewBounds=\"true\"\n        android:maxWidth=\"@dimen/image_search_max_size\"\n        android:maxHeight=\"@dimen/image_search_max_size\"\n        android:visibility=\"gone\" />\n\n    <com.google.android.material.button.MaterialButton\n        android:id=\"@+id/select_image\"\n        style=\"@style/Widget.MaterialComponents.Button\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_gravity=\"center_horizontal\"\n        android:text=\"@string/select_image\" />\n\n</merge>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_search_bar.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright (C) 2015 Hippo Seven\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<merge xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:orientation=\"vertical\">\n\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"48dp\">\n\n            <TextView\n                android:id=\"@+id/search_title\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:background=\"?selectableItemBackground\"\n                android:gravity=\"center_vertical\"\n                android:maxLines=\"1\"\n                android:paddingHorizontal=\"48dp\"\n                android:singleLine=\"true\"\n                android:textColor=\"?android:attr/textColorSecondary\"\n                android:textSize=\"@dimen/text_little_large\" />\n\n            <ImageView\n                android:id=\"@+id/search_menu\"\n                android:layout_width=\"48dp\"\n                android:layout_height=\"48dp\"\n                android:layout_gravity=\"left|center_vertical\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"12dp\" />\n\n            <ImageView\n                android:id=\"@+id/search_action\"\n                android:layout_width=\"48dp\"\n                android:layout_height=\"48dp\"\n                android:layout_gravity=\"right|center_vertical\"\n                android:background=\"?selectableItemBackgroundBorderless\"\n                android:clickable=\"true\"\n                android:focusable=\"true\"\n                android:padding=\"12dp\" />\n\n            <com.hippo.ehviewer.widget.SearchEditText\n                style=\"@style/FixedLineHeightEditText\"\n                android:id=\"@+id/search_edit_text\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"center_vertical\"\n                android:layout_marginHorizontal=\"48dp\"\n                android:background=\"@null\"\n                android:imeOptions=\"actionSearch\"\n                android:inputType=\"text\"\n                android:maxLines=\"1\"\n                android:singleLine=\"true\"\n                android:textSize=\"@dimen/text_little_small\"\n                android:visibility=\"gone\" />\n\n        </FrameLayout>\n\n        <LinearLayout\n            android:id=\"@+id/list_container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            android:visibility=\"gone\">\n\n            <View\n                android:id=\"@+id/list_header\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"1dp\"\n                android:background=\"?attr/dividerColor\" />\n\n            <com.hippo.easyrecyclerview.EasyRecyclerView\n                android:id=\"@+id/search_bar_list\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\" />\n\n        </LinearLayout>\n    </LinearLayout>\n</merge>\n"
  },
  {
    "path": "app/src/main/res/layout-land/activity_set_security.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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=\"match_parent\"\n    android:baselineAligned=\"true\"\n    android:orientation=\"horizontal\"\n    android:paddingHorizontal=\"@dimen/keyline_margin\"\n    app:fitsSystemWindowsInsets=\"bottom\">\n\n    <FrameLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\">\n\n        <TextView\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal|top\"\n            android:gravity=\"center\"\n            android:padding=\"@dimen/keyline_margin\"\n            android:text=\"@string/set_pattern_protection_tip\"\n            android:textAppearance=\"@style/TextAppearance.AppCompat.Medium\" />\n\n        <androidx.appcompat.widget.AppCompatCheckBox\n            android:id=\"@+id/fingerprint_checkbox\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center\"\n            android:layout_marginBottom=\"20dp\"\n            android:text=\"@string/enable_biometric\"\n            android:visibility=\"gone\" />\n\n        <LinearLayout\n            android:id=\"@+id/buttonPanel\"\n            style=\"?attr/buttonBarStyle\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"bottom\"\n            android:clipToPadding=\"false\"\n            android:orientation=\"vertical\"\n            android:paddingHorizontal=\"12dp\"\n            android:paddingVertical=\"4dp\">\n\n            <Button\n                android:id=\"@+id/set\"\n                style=\"@style/Widget.MaterialComponents.Button\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"@string/set\" />\n\n            <Button\n                android:id=\"@+id/cancel\"\n                style=\"@style/Widget.MaterialComponents.Button.OutlinedButton\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_weight=\"1\"\n                android:text=\"@android:string/cancel\" />\n\n        </LinearLayout>\n    </FrameLayout>\n\n    <FrameLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_weight=\"1\">\n\n        <com.hippo.widget.lockpattern.LockPatternView\n            android:id=\"@+id/pattern_view\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\" />\n\n    </FrameLayout>\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/menu/activity_filter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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_tip\"\n        android:icon=\"@drawable/v_info_outline_dark_x24\"\n        android:title=\"@string/tip\"\n        app:showAsAction=\"ifRoom\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/activity_u_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2018 Hippo Seven\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<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_apply\"\n        android:icon=\"@drawable/v_check_dark_x24\"\n        android:title=\"@string/apply\"\n        app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/context_comment.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2023 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\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_bold\"\n        android:title=\"@string/format_bold\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_italic\"\n        android:title=\"@string/format_italic\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_strikethrough\"\n        android:title=\"@string/format_strikethrough\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_underline\"\n        android:title=\"@string/format_underline\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_url\"\n        android:title=\"@string/format_url\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_clear\"\n        android:title=\"@string/format_plain\"\n        app:showAsAction=\"ifRoom\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/download_label_option.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/menu_label_rename\"\n        android:orderInCategory=\"100\"\n        android:title=\"@string/rename_label\"\n        app:showAsAction=\"never\" />\n    <item\n        android:id=\"@+id/menu_label_remove\"\n        android:orderInCategory=\"101\"\n        android:title=\"@string/delete\"\n        app:showAsAction=\"never\" />\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/drawer_download.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <item\n\n        android:id=\"@+id/action_add\"\n        android:icon=\"@drawable/v_plus_dark_x24\"\n        android:title=\"@string/add\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/action_default_download_label\"\n        android:icon=\"@drawable/v_download_box_dark_x24\"\n        android:title=\"@string/default_download_label\"\n        app:showAsAction=\"ifRoom\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/drawer_favorites.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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_default_favorites_slot\"\n        android:icon=\"@drawable/v_heart_box_dark_x24\"\n        android:title=\"@string/default_favorites_collection\"\n        app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/drawer_gallery_list.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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_help\"\n        android:icon=\"@drawable/v_help_circle_x24\"\n        android:title=\"@string/readme\"\n        app:showAsAction=\"always\" />\n\n    <item\n        android:id=\"@+id/action_add\"\n        android:icon=\"@drawable/v_plus_dark_x24\"\n        android:title=\"@string/add\"\n        app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/nav_drawer_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <group android:checkableBehavior=\"single\">\n        <item\n            android:id=\"@+id/nav_homepage\"\n            android:icon=\"@drawable/v_homepage_black_x24\"\n            android:title=\"@string/homepage\" />\n\n        <item\n            android:id=\"@+id/nav_subscription\"\n            android:icon=\"@drawable/v_eh_subscription_black_x24\"\n            android:title=\"@string/subscription\" />\n\n        <item\n            android:id=\"@+id/nav_whats_hot\"\n            android:icon=\"@drawable/v_fire_black_x24\"\n            android:title=\"@string/whats_hot\" />\n\n        <item\n            android:id=\"@+id/nav_toplist\"\n            android:icon=\"@drawable/ic_baseline_format_list_numbered_24\"\n            android:title=\"@string/toplist\" />\n\n        <item\n            android:id=\"@+id/nav_favourite\"\n            android:icon=\"@drawable/v_heart_x24\"\n            android:title=\"@string/favourite\" />\n\n        <item\n            android:id=\"@+id/nav_history\"\n            android:icon=\"@drawable/v_history_black_x24\"\n            android:title=\"@string/history\" />\n\n        <item\n            android:id=\"@+id/nav_downloads\"\n            android:icon=\"@drawable/v_download_x24\"\n            android:title=\"@string/downloads\" />\n\n        <item\n            android:id=\"@+id/nav_settings\"\n            android:icon=\"@drawable/v_settings_black_x24\"\n            android:title=\"@string/settings\" />\n\n        <item\n            android:id=\"@+id/nav_stub\"\n            android:title=\"@string/stub\"\n            android:visible=\"false\" />\n\n    </group>\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/quicksearch_option.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <item\n        android:id=\"@+id/menu_qs_remove\"\n        android:orderInCategory=\"100\"\n        android:title=\"@string/delete\"\n        app:showAsAction=\"never\" />\n</menu>"
  },
  {
    "path": "app/src/main/res/menu/scene_download.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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_filter\"\n        android:icon=\"@drawable/v_filter_dark_x24\"\n        android:title=\"@string/download_filter\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_start_all\"\n        android:icon=\"@drawable/v_play_x24\"\n        android:title=\"@string/download_start_all\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_stop_all\"\n        android:icon=\"@drawable/v_pause_x24\"\n        android:title=\"@string/download_stop_all\"\n        app:showAsAction=\"ifRoom\" />\n\n    <item\n        android:id=\"@+id/action_start_all_reversed\"\n        android:title=\"@string/download_start_all_reversed\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_sort\"\n        android:title=\"@string/download_sort_by\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_reset_reading_progress\"\n        android:title=\"@string/download_reset_reading_progress\"\n        app:showAsAction=\"never\" />\n\n    <item\n        android:id=\"@+id/action_open_download_labels\"\n        android:title=\"@string/download_labels\"\n        app:showAsAction=\"never\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/scene_gallery_detail.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <item\n        android:id=\"@+id/action_refresh\"\n        android:title=\"@string/refresh\" />\n\n    <item\n        android:id=\"@+id/action_add_tag\"\n        android:title=\"@string/action_add_tag\" />\n\n    <item\n        android:id=\"@+id/action_clear_image_cache\"\n        android:title=\"@string/action_clear_image_cache\" />\n\n    <item\n        android:id=\"@+id/action_open_in_other_app\"\n        android:title=\"@string/open_in_other_app\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/scene_gallery_previews.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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_go_to\"\n        android:icon=\"@drawable/v_go_to_dark_x24\"\n        android:title=\"@string/go_to\"\n        app:showAsAction=\"ifRoom\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/scene_history.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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_clear_all\"\n        android:icon=\"@drawable/v_clear_all_dark_x24\"\n        android:title=\"@string/clear_all\"\n        app:showAsAction=\"always\" />\n\n</menu>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/transition/trans_fade.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<fade />\n"
  },
  {
    "path": "app/src/main/res/transition/trans_move.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<transitionSet>\n    <changeTransform />\n    <changeBounds />\n    <changeImageTransform />\n</transitionSet>\n"
  },
  {
    "path": "app/src/main/res/values/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <string-array name=\"list_mode_entries\" translatable=\"false\">\n        <item>@string/settings_eh_list_mode_detail</item>\n        <item>@string/settings_eh_list_mode_thumb</item>\n    </string-array>\n\n    <string-array name=\"list_mode_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n    </string-array>\n\n    <string-array name=\"gallery_site_entries\" translatable=\"false\">\n        <item>@string/site_e</item>\n        <item>@string/site_ex</item>\n    </string-array>\n\n    <string-array name=\"gallery_site_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n    </string-array>\n\n    <string-array name=\"launch_page_entries\" translatable=\"false\">\n        <item>@string/homepage</item>\n        <item>@string/subscription</item>\n        <item>@string/whats_hot</item>\n        <item>@string/toplist</item>\n    </string-array>\n\n    <string-array name=\"launch_page_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n        <item>3</item>\n    </string-array>\n\n    <string-array name=\"search_min_rating\">\n        <item>@string/star_2</item>\n        <item>@string/star_3</item>\n        <item>@string/star_4</item>\n        <item>@string/star_5</item>\n    </string-array>\n\n    <string-array name=\"multi_thread_download_entries\" translatable=\"false\">\n        <item>1</item>\n        <item>3</item>\n        <item>5</item>\n        <item>7</item>\n    </string-array>\n\n    <string-array name=\"multi_thread_download_entry_values\" translatable=\"false\">\n        <item>1</item>\n        <item>3</item>\n        <item>5</item>\n        <item>7</item>\n    </string-array>\n\n    <string-array name=\"download_delay_entries\" translatable=\"false\">\n        <item>0</item>\n        <item>500</item>\n        <item>1000</item>\n        <item>1500</item>\n        <item>2000</item>\n        <item>3000</item>\n        <item>4000</item>\n        <item>5000</item>\n        <item>10000</item>\n        <item>20000</item>\n    </string-array>\n\n    <string-array name=\"download_delay_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>500</item>\n        <item>1000</item>\n        <item>1500</item>\n        <item>2000</item>\n        <item>3000</item>\n        <item>4000</item>\n        <item>5000</item>\n        <item>10000</item>\n        <item>20000</item>\n    </string-array>\n\n    <string-array name=\"download_origin_image_entries\">\n        <item>@string/settings_download_download_origin_image_never</item>\n        <item>@string/settings_download_download_origin_image_force</item>\n        <item>@string/settings_download_download_origin_image_only</item>\n    </string-array>\n\n    <string-array name=\"download_origin_image_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"preload_image_entries\" translatable=\"false\">\n        <item>3</item>\n        <item>5</item>\n        <item>7</item>\n        <item>11</item>\n        <item>13</item>\n        <item>17</item>\n    </string-array>\n\n    <string-array name=\"preload_image_entry_values\" translatable=\"false\">\n        <item>3</item>\n        <item>5</item>\n        <item>7</item>\n        <item>11</item>\n        <item>13</item>\n        <item>17</item>\n    </string-array>\n\n    <string-array name=\"screen_rotation_entries\">\n        <item>@string/settings_read_screen_rotation_default</item>\n        <item>@string/settings_read_screen_rotation_portrait</item>\n        <item>@string/settings_read_screen_rotation_landscape</item>\n        <item>@string/settings_read_screen_rotation_sensor</item>\n    </string-array>\n\n    <string-array name=\"screen_rotation_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n        <item>3</item>\n    </string-array>\n\n    <string-array name=\"reading_direction_entries\">\n        <item>@string/settings_read_reading_direction_left_to_right</item>\n        <item>@string/settings_read_reading_direction_right_to_Left</item>\n        <item>@string/settings_read_reading_direction_top_to_bottom</item>\n    </string-array>\n\n    <string-array name=\"reading_direction_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"page_scaling_entries\">\n        <item>@string/settings_read_page_scaling_actual_size</item>\n        <item>@string/settings_read_page_scaling_fit_to_width</item>\n        <item>@string/settings_read_page_scaling_fit_to_height</item>\n        <item>@string/settings_read_page_scaling_fit_to_screen</item>\n        <item>@string/settings_read_page_scaling_fixed_scale</item>\n    </string-array>\n\n    <string-array name=\"page_scaling_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n        <item>3</item>\n        <item>4</item>\n    </string-array>\n\n    <string-array name=\"start_position_entries\">\n        <item>@string/settings_read_start_position_top_left</item>\n        <item>@string/settings_read_start_position_top_right</item>\n        <item>@string/settings_read_start_position_bottom_left</item>\n        <item>@string/settings_read_start_position_bottom_right</item>\n        <item>@string/settings_read_start_position_center</item>\n    </string-array>\n\n    <string-array name=\"start_position_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n        <item>3</item>\n        <item>4</item>\n    </string-array>\n\n    <string-array name=\"read_cache_size_entries\" translatable=\"false\">\n        <item>320 MB</item>\n        <item>640 MB</item>\n        <item>1280 MB</item>\n        <item>2560 MB</item>\n        <item>5120 MB</item>\n    </string-array>\n\n    <string-array name=\"read_cache_size_entry_values\" translatable=\"false\">\n        <item>320</item>\n        <item>640</item>\n        <item>1280</item>\n        <item>2560</item>\n        <item>5120</item>\n    </string-array>\n\n    <string-array name=\"filter_entries\">\n        <item>@string/filter_title</item>\n        <item>@string/filter_uploader</item>\n        <item>@string/filter_tag</item>\n        <item>@string/filter_tag_namespace</item>\n        <item>@string/filter_commenter</item>\n        <item>@string/filter_comment</item>\n    </string-array>\n\n    <string-array name=\"app_language_entries\">\n        <item>@string/app_language_system</item>\n        <item>@string/app_language_en</item>\n        <item>@string/app_language_ja</item>\n        <item>@string/app_language_zh_cn</item>\n        <item>@string/app_language_zh_hk</item>\n        <item>@string/app_language_zh_tw</item>\n    </string-array>\n\n    <string-array name=\"app_language_entry_values\">\n        <item>system</item>\n        <item>en</item>\n        <item>ja</item>\n        <item>zh-CN</item>\n        <item>zh-HK</item>\n        <item>zh-TW</item>\n    </string-array>\n\n    <string-array name=\"tag_translation_metadata\">\n        <item>tag-translations-zh-rCN.json.sha1</item>\n        <item>https://raw.githubusercontent.com/EhViewer-NekoInverter/EhTagTranslation/tag-translations/tag-translations/tag-translations-zh-rCN.json.sha1</item>\n        <item>tag-translations-zh-rCN.json</item>\n        <item>https://raw.githubusercontent.com/EhViewer-NekoInverter/EhTagTranslation/tag-translations/tag-translations/tag-translations-zh-rCN.json</item>\n    </string-array>\n\n    <string-array name=\"proxy_types\" tools:ignore=\"InconsistentArrays\">\n        <item>@string/proxy_direct</item>\n        <item>@string/proxy_system</item>\n        <item>HTTP</item>\n    </string-array>\n\n    <string-array name=\"night_mode_entries\" translatable=\"false\">\n        <item>@string/dark_theme_follow_system</item>\n        <item>@string/dark_theme_off</item>\n        <item>@string/dark_theme_on</item>\n    </string-array>\n\n    <string-array name=\"night_mode_values\">\n        <item>-1</item>\n        <item>1</item>\n        <item>2</item>\n    </string-array>\n\n    <string-array name=\"download_state\">\n        <item>@string/download_all</item>\n        <item>@string/download_state_none</item>\n        <item>@string/download_state_wait</item>\n        <item>@string/download_state_downloading</item>\n        <item>@string/download_state_finish</item>\n        <item>@string/download_state_failed</item>\n        <item>@string/download_filter_title</item>\n    </string-array>\n\n    <string-array name=\"download_sort\">\n        <item>@string/download_sort_added_time_desc</item>\n        <item>@string/download_sort_added_time_asc</item>\n        <item>@string/download_sort_title_asc</item>\n        <item>@string/download_sort_title_desc</item>\n        <item>@string/download_sort_author_asc</item>\n        <item>@string/download_sort_author_desc</item>\n        <item>@string/download_sort_name_asc</item>\n        <item>@string/download_sort_name_desc</item>\n        <item>@string/download_sort_category_asc</item>\n        <item>@string/download_sort_category_desc</item>\n        <item>@string/download_sort_shuffle</item>\n    </string-array>\n\n    <string-array name=\"read_theme_entries\">\n        <item>@string/settings_read_theme_follow_app</item>\n        <item>@string/settings_read_theme_dark</item>\n        <item>@string/settings_read_theme_light</item>\n    </string-array>\n\n    <string-array name=\"read_theme_entry_values\" translatable=\"false\">\n        <item>0</item>\n        <item>1</item>\n        <item>2</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <declare-styleable name=\"Theme\">\n        <attr name=\"dividerColor\" format=\"color\" />\n        <attr name=\"toolbarColor\" format=\"color\" />\n\n        <attr name=\"textColorThemePrimary\" format=\"color\" />\n        <attr name=\"textColorThemeAccent\" format=\"color\" />\n\n        <!-- For light theme, it's secondary dark color -->\n        <!-- For dark theme, it's secondary light color -->\n        <!-- For black theme, it's secondary light color -->\n        <attr name=\"drawableColorSecondary\" format=\"color\" />\n        <!-- For light theme, it's primary color -->\n        <!-- For dark theme, it's primary light color -->\n        <!-- For black theme, it's primary light color -->\n        <attr name=\"drawableColorThemePrimary\" format=\"color\" />\n\n        <!-- Color of card background -->\n        <attr name=\"contentColorPrimary\" format=\"color\" />\n        <attr name=\"contentColorThemePrimary\" format=\"color\" />\n        <attr name=\"contentColorThemeAccent\" format=\"color\" />\n        <attr name=\"contentColorReactive\" format=\"color\" />\n\n        <attr name=\"widgetColorThemePrimary\" format=\"color\" />\n        <attr name=\"widgetColorThemeAccent\" format=\"color\" />\n\n        <attr name=\"guideBackgroundColor\" format=\"color\" />\n        <attr name=\"guideTitleColor\" format=\"color\" />\n        <attr name=\"guideTextColor\" format=\"color\" />\n\n        <attr name=\"progressColor\" format=\"color\" />\n\n        <attr name=\"tagGroupBackgroundColor\" format=\"color\" />\n        <attr name=\"tagBackgroundColor\" format=\"color\" />\n\n        <attr name=\"galleryDetailHeaderTitleColor\" format=\"color\" />\n        <attr name=\"galleryDetailHeaderBackgroundColor\" format=\"color\" />\n        <attr name=\"galleryDetailButtonBackgroundColor\" format=\"color\" />\n\n        <attr name=\"gallerySliderBackgroundColor\" format=\"color\" />\n\n        <attr name=\"toolbarPopupTheme\" format=\"reference\" />\n        <attr name=\"galleryDetailDivider\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"ProgressView\">\n        <attr name=\"color\" />\n        <attr name=\"indeterminate\" format=\"boolean\" />\n        <attr name=\"progress\" format=\"float\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"MaxSizeContainer\">\n        <attr name=\"maxWidth\" format=\"dimension\" />\n        <attr name=\"maxHeight\" format=\"dimension\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"FixedAspectImageView\">\n        <attr name=\"aspect\" format=\"float\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"LoadImageView\">\n        <attr name=\"retryType\">\n            <enum name=\"none\" value=\"0\" />\n            <enum name=\"click\" value=\"1\" />\n            <enum name=\"longClick\" value=\"2\" />\n        </attr>\n    </declare-styleable>\n\n    <declare-styleable name=\"FixedThumb\">\n        <attr name=\"minAspect\" format=\"float\" />\n        <attr name=\"maxAspect\" format=\"float\" />\n        <attr name=\"alwaysCutAndScale\" format=\"boolean\"/>\n    </declare-styleable>\n\n    <declare-styleable name=\"CheckTextView\">\n        <attr name=\"android:foregroundGravity\" />\n        <attr name=\"android:foreground\" />\n        <attr name=\"foregroundInsidePadding\" format=\"boolean\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"DividerView\">\n        <attr name=\"dividerWidth\" format=\"dimension\" />\n        <attr name=\"dividerHeight\" format=\"dimension\" />\n        <attr name=\"dividerColor\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"AutoWrapLayout\">\n        <attr name=\"alignment\">\n            <enum name=\"top\" value=\"0\" />\n            <enum name=\"center\" value=\"1\" />\n            <enum name=\"bottom\" value=\"2\" />\n        </attr>\n    </declare-styleable>\n\n    <declare-styleable name=\"SimpleGridLayout\">\n        <attr name=\"columnCount\" format=\"integer\" />\n        <attr name=\"itemMargin\" format=\"dimension\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"Slider\">\n        <attr name=\"start\" format=\"integer\" />\n        <attr name=\"end\" format=\"integer\" />\n        <attr name=\"slider_progress\" format=\"integer\" />\n        <attr name=\"thickness\" />\n        <attr name=\"radius\" format=\"dimension\" />\n        <attr name=\"color\" />\n        <attr name=\"textColor\" format=\"color\" />\n        <attr name=\"textSize\" format=\"dimension\" />\n        <attr name=\"dark\" format=\"boolean\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"BatteryView\">\n        <attr name=\"color\" />\n        <attr name=\"warningColor\" format=\"color\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"Indicating\">\n        <attr name=\"indicatorHeight\" format=\"dimension\" />\n        <attr name=\"indicatorColor\" format=\"color\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"DialogPreference\">\n        <attr name=\"dialogTitle\" format=\"string\" />\n        <attr name=\"dialogIcon\" format=\"reference\" />\n        <attr name=\"positiveButtonText\" format=\"string\" />\n        <attr name=\"negativeButtonText\" format=\"string\" />\n        <attr name=\"dialogLayout\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"ListPreference\">\n        <!-- The human-readable array to present as a list. Each entry must have a corresponding\n             index in entryValues. -->\n        <attr name=\"entries\" format=\"reference\" />\n        <!-- The array to find the value to save for a preference when an entry from\n             entries is selected. If a user clicks on the second item in entries, the\n             second item in this array will be saved to the preference. -->\n        <attr name=\"entryValues\" format=\"reference\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"ActivityPreference\">\n        <attr name=\"activity\" format=\"string\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"UrlPreference\">\n        <attr name=\"url\" format=\"string\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"FastScroller\">\n        <attr name=\"handler\" format=\"reference\" />\n        <attr name=\"draggable\" format=\"boolean\" />\n    </declare-styleable>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/bools.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <bool name=\"dad_spin_bars\">true</bool>\n    <bool name=\"tag_translatable\">false</bool>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/color_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<resources>\n    <color name=\"ic_launcher_background\">#E2E0D0</color>\n    <color name=\"ic_launcher_foreground\">#5C0D11</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <!-- primary_drawable     54% -->\n    <!-- secondary_drawable   26% -->\n    <!-- primary_drawable_dark     100% -->\n    <!-- secondary_drawable_dark    30% -->\n    <!-- primary_drawable_black     80% -->\n    <!-- secondary_drawable_black   24% -->\n    <color name=\"primary_drawable\">#8a000000</color>\n    <color name=\"secondary_drawable\">#42000000</color>\n    <color name=\"primary_drawable_dark\">#ffffffff</color>\n    <color name=\"primary_drawable_black\">#ccffffff</color>\n    <color name=\"secondary_drawable_black\">#3dffffff</color>\n\n    <color name=\"primary_text_default_material_black\">#d9ffffff</color>\n    <color name=\"primary_text_disabled_material_black\">#4dffffff</color>\n\n    <color name=\"content\">#fff</color>\n    <color name=\"content_black\">@color/grey_925</color>\n\n    <color name=\"content_activated\">@color/grey_300</color>\n    <color name=\"content_activated_black\">@color/grey_700</color>\n\n    <color name=\"widget\">@color/grey_200</color>\n    <color name=\"widget_black\">@color/grey_850</color>\n\n    <color name=\"progress\">@color/colorAccent</color>\n    <color name=\"progress_black\">@color/grey_700</color>\n\n    <color name=\"tag_group_background\">@color/colorAccent</color>\n    <color name=\"tag_group_background_black\">@color/grey_875</color>\n\n    <color name=\"tag_background\">@color/colorPrimary</color>\n    <color name=\"tag_background_black\">@color/grey_875</color>\n\n    <color name=\"gallery_detail_button_background\">@color/colorBackground</color>\n    <color name=\"gallery_detail_button_background_black\">@color/grey_875</color>\n\n    <color name=\"red_500\">#fff44336</color>\n    <color name=\"teal_500\">#ff009688</color>\n    <color name=\"teal_700\">#ff00796b</color>\n    <color name=\"purple_a200\">#ffe040fb</color>\n    <color name=\"purple_a700\">#ffaa00ff</color>\n    <color name=\"cyan_600\">#ff00acc1</color>\n    <color name=\"light_green_600\">#ff7cb342</color>\n    <color name=\"yellow_800\">#fff9a825</color>\n    <color name=\"deep_orange_400\">#ffff7043</color>\n    <color name=\"grey_925\">#ff191919</color>\n    <color name=\"grey_900\">#ff212121</color>\n    <color name=\"grey_875\">#ff292929</color>\n    <color name=\"grey_850\">#ff323232</color>\n    <color name=\"grey_825\">#ff3a3a3a</color>\n    <color name=\"grey_775\">#ff4a4a4a</color>\n    <color name=\"grey_700\">#ff616161</color>\n    <color name=\"grey_600\">#ff757575</color>\n    <color name=\"grey_300\">#ffe0e0e0</color>\n    <color name=\"grey_200\">#ffeeeeee</color>\n    <color name=\"grey_100\">#fff5f5f5</color>\n    <color name=\"brown_500\">#ff795548</color>\n\n    <color name=\"colorPrimary\">@color/teal_500</color>\n    <color name=\"colorPrimaryDark\">@color/teal_700</color>\n    <color name=\"colorAccent\">@color/purple_a200</color>\n\n    <color name=\"loading_indicator_red\">#ffe53935</color>\n    <color name=\"loading_indicator_purple\">@color/purple_a700</color>\n    <color name=\"loading_indicator_blue\">#ff1d87e4</color>\n    <color name=\"loading_indicator_cyan\">@color/cyan_600</color>\n    <color name=\"loading_indicator_green\">@color/light_green_600</color>\n    <color name=\"loading_indicator_yellow\">@color/yellow_800</color>\n    <color name=\"loading_indicator_orange\">@color/deep_orange_400</color>\n\n    <color name=\"check_text_view_mask\">#4a000000</color>\n\n    <!-- Red -->\n    <color name=\"doujinshi\">#fff44336</color>\n    <!-- Orange -->\n    <color name=\"manga\">#ffff9800</color>\n    <!-- Yellow 700 -->\n    <color name=\"artist_cg\">#ffFbc02d</color>\n    <!-- Green -->\n    <color name=\"game_cg\">#ff4caf50</color>\n    <!-- Light Green -->\n    <color name=\"western\">#ff8bc34a</color>\n    <!-- Blue -->\n    <color name=\"non_h\">#ff2196f3</color>\n    <!-- Indigo -->\n    <color name=\"image_set\">#ff3f51b5</color>\n    <!-- Purple -->\n    <color name=\"cosplay\">#ff9c27b0</color>\n    <!-- Deep Purple 300 -->\n    <color name=\"asian_porn\">#ff9575cd</color>\n    <!-- Pink 300 -->\n    <color name=\"misc\">#fff06292</color>\n\n    <!-- divider -->\n    <color name=\"divider\">#20000000</color>\n\n    <color name=\"divider_black\">#20ffffff</color>\n    <!-- shadow -->\n\n    <!-- Gallery Activity -->\n    <color name=\"gallery_slider_background\">@color/grey_100</color>\n\n    <color name=\"guide_bg\">#e5009688</color>\n\n    <!-- Lock pattern view -->\n    <color name=\"lock_pattern_view_regular_color\">#8a000000</color>\n    <color name=\"lock_pattern_view_success_color\">@color/colorPrimary</color>\n    <color name=\"lock_pattern_view_error_color\">@color/red_500</color>\n\n    <!--Shortcut background-->\n    <color name=\"shortcut_bg\">#F5F5F5</color>\n    <color name=\"colorBackground\">#fff</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <dimen name=\"keyline_margin\">16dp</dimen>\n    <dimen name=\"single_max_width\">260dp</dimen>\n    <dimen name=\"drawer_max_width\">280dp</dimen>\n\n    <!-- Text size -->\n    <dimen name=\"text_super_large\">24sp</dimen>\n    <dimen name=\"text_little_large\">20sp</dimen>\n    <dimen name=\"text_little_small\">16sp</dimen>\n    <dimen name=\"text_small\">14sp</dimen>\n    <dimen name=\"text_super_small\">12sp</dimen>\n\n    <!-- RatingView -->\n    <dimen name=\"rating_size\">16dp</dimen>\n    <dimen name=\"rating_interval\">1dp</dimen>\n\n    <!-- 4dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_list_interval\">0dp</dimen>\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_list_margin_h\">4dp</dimen>\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_list_margin_v\">4dp</dimen>\n\n    <!-- 4dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_grid_interval\">0dp</dimen>\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_grid_margin_h\">4dp</dimen>\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_grid_margin_v\">4dp</dimen>\n\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_search_bar_margin_h\">6dp</dimen>\n    <!-- 12dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_search_bar_margin_v\">8dp</dimen>\n\n    <dimen name=\"gallery_padding_top_search_bar\">64dp</dimen>\n    <dimen name=\"gallery_padding_bottom_fab\">80dp</dimen>\n\n    <!-- 4dp - 4dp, 4dp for card padding -->\n    <dimen name=\"search_layout_interval\">0dp</dimen>\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"search_layout_margin_h\">4dp</dimen>\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"search_layout_margin_v\">4dp</dimen>\n\n    <!-- DrawerArrowDrawable -->\n    <dimen name=\"dad_drawable_size\">24dp</dimen>\n    <dimen name=\"dad_bar_size\">18dp</dimen>\n    <dimen name=\"dad_top_bottom_bar_arrow_size\">11.31dp</dimen>\n    <dimen name=\"dad_thickness\">2dp</dimen>\n    <dimen name=\"dad_gap_between_bars\">3dp</dimen>\n    <dimen name=\"dad_middle_bar_arrow_size\">16dp</dimen>\n\n    <!-- AddDeleteDrawable -->\n    <dimen name=\"add_size\">20dp</dimen>\n    <dimen name=\"add_thickness\">2dp</dimen>\n\n    <!-- CategoryTable -->\n    <dimen name=\"category_table_item_height\">40dp</dimen>\n    <dimen name=\"category_table_item_margin\">4dp</dimen>\n\n    <!-- AdvanceSearchTable -->\n    <dimen name=\"advance_search_table_item_margin\">4dp</dimen>\n\n    <!-- Search item -->\n    <dimen name=\"search_item_interval\">8dp</dimen>\n    <dimen name=\"search_category_padding_h\">@dimen/keyline_margin</dimen>\n    <dimen name=\"search_category_padding_v\">@dimen/keyline_margin</dimen>\n    <dimen name=\"search_category_title_height\">48dp</dimen>\n\n    <dimen name=\"image_search_max_size\">240dp</dimen>\n\n    <!-- Switch padding -->\n    <dimen name=\"switch_padding\">8dp</dimen>\n\n    <!-- FabLayout -->\n    <dimen name=\"fab_layout_primary_margin\">24dp</dimen>\n    <dimen name=\"fab_layout_secondary_margin\">16dp</dimen>\n    <dimen name=\"fab_size\">56dp</dimen>\n    <dimen name=\"fab_min_size\">40dp</dimen>\n    <dimen name=\"fab_rv_padding_size\">80dp</dimen>\n\n    <!-- Slider -->\n    <dimen name=\"slider_bubble_width\">26dp</dimen>\n    <dimen name=\"slider_bubble_height\">32dp</dimen>\n\n    <dimen name=\"corner_fab_margin\">16dp</dimen>\n\n    <dimen name=\"gallery_detail_thumb_width\">128dp</dimen>\n    <dimen name=\"gallery_detail_thumb_height\">192dp</dimen>\n\n    <dimen name=\"gallery_widget_margin_h\">12dp</dimen>\n    <dimen name=\"gallery_widget_margin_v\">12dp</dimen>\n\n    <dimen name=\"gallery_guide_divider_width\">3dp</dimen>\n\n    <!-- Lock pattern view -->\n    <dimen name=\"lock_pattern_dot_line_width\">3dp</dimen>\n    <dimen name=\"lock_pattern_dot_size\">12dp</dimen>\n    <dimen name=\"lock_pattern_dot_size_activated\">28dp</dimen>\n\n    <!-- Custom preference -->\n\n    <!-- Gallery -->\n    <dimen name=\"gallery_content_margin_h\">4dp</dimen>\n    <dimen name=\"gallery_content_margin_v\">4dp</dimen>\n    <dimen name=\"gallery_pager_interval\">48dp</dimen>\n    <dimen name=\"gallery_scroll_interval\">28dp</dimen>\n    <dimen name=\"gallery_page_min_height\">365dp</dimen>\n    <dimen name=\"gallery_page_info_interval\">24dp</dimen>\n    <dimen name=\"gallery_progress_size\">56dp</dimen>\n    <dimen name=\"gallery_page_text_size\">56sp</dimen>\n    <dimen name=\"gallery_error_text_size\">24sp</dimen>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/drawables.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2015 Hippo Seven\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<resources>\n\n    <drawable name=\"transparent\">@android:color/transparent</drawable>\n    <drawable name=\"tile_background_activated\">#7f9c27b0</drawable>\n    <drawable name=\"sadpanda_low_poly\">@color/colorPrimary</drawable>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <item name=\"tag\" type=\"id\" />\n    <item name=\"index\" type=\"id\" />\n\n    <item name=\"fragment_tag\" type=\"id\" />\n\n    <item name=\"copy\" type=\"id\" />\n    <item name=\"block_commenter\" type=\"id\" />\n    <item name=\"vote_up\" type=\"id\" />\n    <item name=\"vote_down\" type=\"id\" />\n    <item name=\"check_vote_status\" type=\"id\" />\n    <item name=\"edit_comment\" type=\"id\" />\n    <item name=\"show_definition\" type=\"id\" />\n    <item name=\"add_filter\" type=\"id\" />\n    <item name=\"copy_trans\" type=\"id\" />\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/pathdata.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <string name=\"pd_close\" translatable=\"false\">M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z</string>\n    <string name=\"pd_pause\" translatable=\"false\">M14,19.14H18V5.14H14M6,19.14H10V5.14H6V19.14Z</string>\n    <string name=\"pd_play\" translatable=\"false\">M8,5.14V19.14L19,12.14L8,5.14Z</string>\n    <string name=\"pd_check_all\" translatable=\"false\">M0.41,13.41L6,19L7.41,17.58L1.83,12M22.24,5.58L11.66,16.17L7.5,12L6.07,13.41L11.66,19L23.66,7M18,7L16.59,5.58L10.24,11.93L11.66,13.34L18,7Z</string>\n    <string name=\"pd_folder_move\" translatable=\"false\">M9,18V15H5V11H9V8L14,13M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z</string>\n    <string name=\"pd_settings\" translatable=\"false\">M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z</string>\n    <string name=\"pd_plus\" translatable=\"false\">M21,13H13V21H11V13H3V11H11V3H13V11H21V13Z</string>\n    <string name=\"pd_download\" translatable=\"false\">M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z</string>\n    <string name=\"pd_homepage\" translatable=\"false\">M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z</string>\n    <string name=\"pd_fire\" translatable=\"false\">M11.71,19C9.93,19 8.5,17.59 8.5,15.86C8.5,14.24 9.53,13.1 11.3,12.74C13.07,12.38 14.9,11.53 15.92,10.16C16.31,11.45 16.5,12.81 16.5,14.2C16.5,16.84 14.36,19 11.71,19M13.5,0.67C13.5,0.67 14.24,3.32 14.24,5.47C14.24,7.53 12.89,9.2 10.83,9.2C8.76,9.2 7.2,7.53 7.2,5.47L7.23,5.1C5.21,7.5 4,10.61 4,14A8,8 0 0,0 12,22A8,8 0 0,0 20,14C20,8.6 17.41,3.8 13.5,0.67Z</string>\n    <string name=\"pd_heart\" translatable=\"false\">M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z</string>\n    <string name=\"pd_heart_broken\" translatable=\"false\">M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C8.17,3 8.82,3.12 9.44,3.33L13,9.35L9,14.35L12,21.35V21.35M16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35L11,14.35L15.5,9.35L12.85,4.27C13.87,3.47 15.17,3 16.5,3Z</string>\n    <string name=\"pd_history\" translatable=\"false\">M11,7V12.11L15.71,14.9L16.5,13.62L12.5,11.25V7M12.5,2C8.97,2 5.91,3.92 4.27,6.77L2,4.5V11H8.5L5.75,8.25C6.96,5.73 9.5,4 12.5,4A7.5,7.5 0 0,1 20,11.5A7.5,7.5 0 0,1 12.5,19C9.23,19 6.47,16.91 5.44,14H3.34C4.44,18.03 8.11,21 12.5,21C17.74,21 22,16.75 22,11.5A9.5,9.5 0 0,0 12.5,2Z</string>\n    <string name=\"pd_book_open\" translatable=\"false\">M21,4C19.9,3.7,18.7,3.5,17.5,3.5C15.6,3.5,13.4,3.9,12,5C10.6,3.9,8.4,3.5,6.5,3.5S2.5,3.9,1,5V19.6C1,19.9,1.3,20.1,1.5,20.1C1.6,20.1,1.6,20.1,1.8,20.1C3.1,19.5,5.1,19,6.5,19C8.4,19,10.6,19.4,12,20.5C13.4,19.6,15.8,19,17.5,19C19.1,19,20.9,19.3,22.3,20C22.4,20.1,22.4,20.1,22.6,20.1C22.9,20.1,23.1,19.8,23.1,19.6V5C22.4,4.6,21.8,4.3,21,4M21,17.5C19.9,17.1,18.7,17,17.5,17C15.8,17,13.4,17.6,12,18.5V7C13.4,6.2,15.8,5.5,17.5,5.5C18.7,5.5,19.9,5.7,21,6V17.5Z</string>\n    <string name=\"pd_delete\" translatable=\"false\">M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z</string>\n    <string name=\"pd_go_to\" translatable=\"false\">M13,13L17,17L21,13H18V10C18,6.1,14.9,3,11,3S4,6.1,4,10V17H6V10C6,7.2,8.2,5,11,5S16,7.2,16,10V13H13M,16,19H18V21H16V19Z</string>\n    <string name=\"pd_alert\" translatable=\"false\">M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z</string>\n    <string name=\"pd_magnify\" translatable=\"false\">M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z</string>\n    <string name=\"pd_help_circle\" translatable=\"false\">M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z</string>\n    <string name=\"pd_dots_vertical\" translatable=\"false\">M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z</string>\n    <string name=\"pd_heart_outline\" translatable=\"false\">M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z</string>\n    <string name=\"pd_arrow_left\" translatable=\"false\">M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z</string>\n    <string name=\"pd_reply\" translatable=\"false\">M10,9V5L3,12L10,19V14.9C15,14.9 18.5,16.5 21,20C20,15 17,10 10,9Z</string>\n    <string name=\"pd_send\" translatable=\"false\">M2,21L23,12L2,3V10L17,12L2,14V21Z</string>\n    <string name=\"pd_share\" translatable=\"false\">M21,11L14,4V8C7,9 4,14 3,19C5.5,15.5 9,13.9 14,13.9V18L21,11Z</string>\n    <string name=\"pd_utorrent\" translatable=\"false\">M2,11.5C2.2,6.3,7.2,1.7,12.5,2C18.1,2.2,23,7.9,21.9,13.5C21,13.5,20.1,13.2,19.6,12.4C18.3,10.4,17.7,8.1,16.5,6.1C15.6,6.2,14.8,6.4,14,6.6C14.3,8.9,15.8,10.8,16.3,13C16.1,14.9,13.6,16,11.9,15.1C10.5,14.6,9.9,13.1,9.4,11.8C8.9,10.5,8.4,9.1,7.7,7.9C6.6,7.9,5.5,7.9,4.5,8.4C5.4,12.9,7.9,16.9,8.9,21.4C4.8,20.1,1.8,15.8,2,11.5M14.8,17.1C15.8,16.5,16.6,15.6,17.5,14.8C18.8,15.1,20,15.3,21.3,15.4C19.9,18.7,16.9,21.2,13.3,21.9C12.3,20.5,11.7,18.9,11.3,17.3C12.5,17.4,13.7,17.5,14.8,17.1Z</string>\n    <string name=\"pd_star\" translatable=\"false\">M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z</string>\n    <string name=\"pd_star_half\" translatable=\"false\">M12,15.89V6.59L13.71,10.63L18.09,11L14.77,13.88L15.76,18.16M22,9.74L14.81,9.13L12,2.5L9.19,9.13L2,9.74L7.45,14.47L5.82,21.5L12,17.77L18.18,21.5L16.54,14.47L22,9.74Z</string>\n    <string name=\"pd_star_outline\" translatable=\"false\">M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z</string>\n    <string name=\"pd_info\" translatable=\"false\">M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z</string>\n    <string name=\"pd_heart_box\" translatable=\"false\">M5,3H19A2,2 0 0,1 21,5V19A2,2 0 0,1 19,21H5A2,2 0 0,1 3,19V5A2,2 0 0,1 5,3M12,17L12.72,16.34C15.3,14 17,12.46 17,10.57C17,9.03 15.79,7.82 14.25,7.82C13.38,7.82 12.55,8.23 12,8.87C11.45,8.23 10.62,7.82 9.75,7.82C8.21,7.82 7,9.03 7,10.57C7,12.46 8.7,14 11.28,16.34L12,17Z</string>\n    <string name=\"pd_download_box\" translatable=\"false\">M19,3H5C3.9,3,3,3.9,3,5V19C3,20.1,3.9,21,5,21H19C20.1,21,21,20.1,21,19V5C21,3.9,20.1,3,19,3ZM12,17L7,12H10V8H14V12H17L12,17Z</string>\n    <string name=\"pd_cookie\" translatable=\"false\">M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A9,9 0 0,0 21,12C21,11.5 20.96,11 20.87,10.5C20.6,10 20,10 20,10H18V9C18,8 17,8 17,8H15V7C15,6 14,6 14,6H13V4C13,3 12,3 12,3M9.5,6A1.5,1.5 0 0,1 11,7.5A1.5,1.5 0 0,1 9.5,9A1.5,1.5 0 0,1 8,7.5A1.5,1.5 0 0,1 9.5,6M6.5,10A1.5,1.5 0 0,1 8,11.5A1.5,1.5 0 0,1 6.5,13A1.5,1.5 0 0,1 5,11.5A1.5,1.5 0 0,1 6.5,10M11.5,11A1.5,1.5 0 0,1 13,12.5A1.5,1.5 0 0,1 11.5,14A1.5,1.5 0 0,1 10,12.5A1.5,1.5 0 0,1 11.5,11M16.5,13A1.5,1.5 0 0,1 18,14.5A1.5,1.5 0 0,1 16.5,16H16.5A1.5,1.5 0 0,1 15,14.5H15A1.5,1.5 0 0,1 16.5,13M11,16A1.5,1.5 0 0,1 12.5,17.5A1.5,1.5 0 0,1 11,19A1.5,1.5 0 0,1 9.5,17.5A1.5,1.5 0 0,1 11,16Z</string>\n    <string name=\"pd_adb\" translatable=\"false\">M15,9A1,1 0 0,1 14,8A1,1 0 0,1 15,7A1,1 0 0,1 16,8A1,1 0 0,1 15,9M9,9A1,1 0 0,1 8,8A1,1 0 0,1 9,7A1,1 0 0,1 10,8A1,1 0 0,1 9,9M16.12,4.37L18.22,2.27L17.4,1.44L15.09,3.75C14.16,3.28 13.11,3 12,3C10.88,3 9.84,3.28 8.91,3.75L6.6,1.44L5.78,2.27L7.88,4.37C6.14,5.64 5,7.68 5,10V11H19V10C19,7.68 17.86,5.64 16.12,4.37M5,16C5,19.86 8.13,23 12,23A7,7 0 0,0 19,16V12H5V16Z</string>\n    <string name=\"pd_info_outline\" translatable=\"false\">M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z</string>\n    <string name=\"pd_clear_all\" translatable=\"false\">M5,13H19V11H5M3,17H17V15H3M7,7V9H21V7</string>\n    <string name=\"pd_similar\" translatable=\"false\">M2,20L17,10L22,22M14,9L2,8L11,2Z</string>\n    <string name=\"pd_refresh\" translatable=\"false\">M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z</string>\n    <string name=\"pd_file_find\" translatable=\"false\">M9,13A3,3 0 0,0 12,16A3,3 0 0,0 15,13A3,3 0 0,0 12,10A3,3 0 0,0 9,13M20,19.59V8L14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18C18.45,22 18.85,21.85 19.19,21.6L14.76,17.17C13.96,17.69 13,18 12,18A5,5 0 0,1 7,13A5,5 0 0,1 12,8A5,5 0 0,1 17,13C17,14 16.69,14.96 16.17,15.75L20,19.59Z</string>\n    <string name=\"pd_check\" translatable=\"false\">M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z</string>\n    <string name=\"pd_file_image\" translatable=\"false\">M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z</string>\n    <string name=\"pd_eh_subscription\" translatable=\"false\">M20,8H4V6h16V8 M18,2H6v2h12V2 M20,10H4c-1.1,0-2,0.9-2,2v8c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2v-8C22,10.9,21.1,10,20,10zM11,14H7v1h4v2H7v1h4v2H5v-8h6V14z M19,20h-2v-3h-2v3h-2v-8h2v3h2v-3h2V20z</string>\n    <string name=\"pd_pencil\" translatable=\"false\">M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z</string>\n\n    <string name=\"pd_big_history\" translatable=\"false\">M5.5,2H22V0H5C3.3,0,2,1.3,2,3V21C2,22.7,3.3,24,5,24H22V4H5.5C4.1,4,4.1,2,5.5,2ZM20,6V22H6V6H20M12.5,11.8V14.4L14.9,15.8L15.3,15.2L13.3,14V11.9M13.3,9.3C11.5,9.3,10,10.3,9.2,11.7L8,10.5V13.8H11.3L9.9,12.4C10.5,11.1,11.8,10.3,13.3,10.3C15.4,10.3,17.1,12,17.1,14.1S15.4,17.9,13.3,17.9C11.7,17.9,10.3,16.9,9.8,15.4H8.7C9.3,17.4,11.1,18.9,13.3,18.9C15.9,18.9,18.1,16.8,18.1,14.1C18,11.4,15.9,9.3,13.3,9.3Z</string>\n    <string name=\"pd_big_download\" translatable=\"false\">M0,19V24H24V19H0ZM16,22H15V21H16V22ZM19,22H18V21H19V22ZM22,22H21V21H22V22M15,11L15,0L9,0L9,11L5,11L12,18L19,11Z</string>\n    <string name=\"pd_big_filter\" translatable=\"false\">M22.9,2.84C24,0.44,15.08,0,12,0C9.06,0,0,0.43,1.1,2.84C1.6,3.94,10,15.76,10,15.76V21L14,24V15.76S22.4,3.96,22.9,2.84ZM12,1.94C16.25,1.94,20.15,2.51,20.15,3.17S16.25,4.34,12,4.34S3.97,3.8,3.97,3.17S7.76,1.94,12,1.94Z</string>\n\n    <string name=\"pd_slider_bubble\" translatable=\"false\">M13,0C20.2,0,26,5.9,26,13.2,26,16.9,24,20.8,18.7,26.1L13,32,7.2,26.2C2,20.8,0,16.9,0,13.3,0,5.9,5.8,0,13,0Z</string>\n\n    <string name=\"pd_sad_panda\" translatable=\"false\">M21.1,7.8C22.2,7.4,23,6.3,23,5C23,3.3,21.7,2,20,2C18.9,2,17.9,2.6,17.4,3.6L21.1,7.8M6.6,3.6C6.1,2.6,5.1,2,4,2C2.3,2,1,3.3,1,5C1,6.3,1.8,7.4,2.9,7.8L6.6,3.6M10.1,13L8,15.1C7.4,15.7,6.5,15.7,5.9,15.1L5.9,15.1C5.3,14.5,5.3,13.6,5.9,13L8,10.9C8.6,10.3,9.5,10.3,10.1,10.9L10.1,10.9C10.7,11.5,10.7,12.4,10.1,13M16,15.1L13.9,13C13.3,12.4,13.3,11.5,13.9,10.9L13.9,10.9C14.5,10.3,15.4,10.3,16,10.9L18.1,13C18.7,13.6,18.7,14.5,18.1,15.1L18.1,15.1C17.5,15.7,16.6,15.7,16,15.1M12,17C12.8,17,13.5,17.4,13.5,18S12.8,19,12,19S10.5,18.6,10.5,18S11.2,17,12,17Z</string>\n    <string name=\"pd_archive\" translatable=\"false\">M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM10,17l-3.5,-3.5 1.41,-1.41L10,14.17 15.18,9l1.41,1.41L10,17z</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n    <!-- App Name -->\n    <string name=\"app_name\" translatable=\"false\">EhViewer</string>\n\n    <!-- Login Scene -->\n    <string name=\"app_waring\">The content of this application is from the Internet. Some of it may do physical or mental harm to you. You have learnt the risks above and would like to undertake them.\\nBy continuing to use it, you agree to the above terms.</string>\n    <string name=\"username\">Username</string>\n    <string name=\"password\">Password</string>\n    <string name=\"sign_in\">Sign in</string>\n    <string name=\"register\">Register</string>\n    <string name=\"sign_in_via_webview\">Sign in via WebView</string>\n    <string name=\"sign_in_via_cookies\">Sign in via cookies</string>\n    <string name=\"tourist_mode\">Continue without signing in</string>\n    <string name=\"error_username_cannot_empty\">Username cannot be an empty string</string>\n    <string name=\"error_password_cannot_empty\">Password cannot be an empty string</string>\n    <string name=\"sign_in_failed\">Sign in failed</string>\n    <string name=\"sign_in_failed_tip\">If this issue continues, try \\\"Sign in via WebView\\\".</string>\n    <string name=\"sign_in_failed_tip_2\">If you are sure your cookies are correct, you can choose to ignore the error and continue, but this may cause some issues.</string>\n    <string name=\"ignore\">Ignore</string>\n    <string name=\"get_it\">Got it</string>\n    <!-- Cookie Sign In Scene -->\n    <string name=\"cookie_explain\">A cookie is a small piece of data stored in web browser. Google it if you don\\'t know what it is or how to get it.\\n\\nPlease enter special cookies to sign in.</string>\n    <string name=\"from_clipboard\">From clipboard</string>\n    <string name=\"text_is_empty\">Text is empty</string>\n    <string name=\"from_clipboard_error\">No cookies found in clipboard.</string>\n\n    <!-- Select Site Scene -->\n    <string name=\"select_scene\">Which gallery site do you want to visit?</string>\n    <string name=\"site_e\" translatable=\"false\">E-Hentai</string>\n    <string name=\"site_ex\" translatable=\"false\">ExHentai</string>\n    <string name=\"select_scene_explain\">E-Hentai: available for anyone\\nExHentai: available for who has signed in</string>\n\n    <!-- Guide -->\n    <string name=\"guide_gallery_left\">Turn Page</string>\n    <string name=\"guide_gallery_right\">Turn Page</string>\n    <string name=\"guide_gallery_menu\">Menu</string>\n    <string name=\"guide_gallery_progress\">Progress</string>\n    <string name=\"guide_gallery_long_click\">Long click to open page menu</string>\n\n    <!-- Main Activity -->\n    <string name=\"metered_network_warning\">Connected to metered networks</string>\n    <string name=\"waring\">Warning</string>\n    <string name=\"invalid_download_location\">It seems download location is not available. Please set it in Settings.</string>\n    <string name=\"press_twice_exit\">Press twice to exit</string>\n    <string name=\"clipboard_gallery_url_snack_message\">There is a gallery URL in the clipboard</string>\n    <string name=\"clipboard_gallery_url_snack_action\">View</string>\n    <string name=\"please_wait\">Please wait…</string>\n    <string name=\"app_link_not_verified_title\">App links not verified</string>\n    <string name=\"app_link_not_verified_message\">For Android 12 and newer, you need to manually add link to verified links in order to open E-Hentai links in EhViewer.</string>\n    <string name=\"open_settings\">Open settings</string>\n    <string name=\"dont_show_again\">Don\\'t show again</string>\n\n    <!-- Gallery Activity -->\n    <string name=\"archive_need_passwd\">Archive need password</string>\n    <string name=\"archive_passwd\">Password</string>\n    <string name=\"passwd_cannot_be_empty\">Password can\\'t be empty</string>\n    <string name=\"passwd_wrong\">Password Wrong</string>\n    <string name=\"page_menu_title\">Page %d</string>\n    <string name=\"page_menu_refresh\">@string/refresh</string>\n    <string name=\"page_menu_share\">@string/share</string>\n    <string name=\"page_menu_save\">Save</string>\n    <string name=\"page_menu_save_to\">Save to…</string>\n    <string name=\"page_menu_download_original\">Download original</string>\n    <string name=\"gallery_menu_title\">Menu</string>\n    <string name=\"share_image\">Share image</string>\n    <string name=\"start_download_original\">Downloading, please wait…</string>\n    <string name=\"image_saved\">Image saved to %s</string>\n\n    <!-- Navigation Bar -->\n    <string name=\"default_display_name\" translatable=\"false\">Little Hippo</string>\n    <string name=\"homepage\">Homepage</string>\n    <string name=\"subscription\">Subscription</string>\n    <string name=\"whats_hot\">What\\'s hot</string>\n    <string name=\"toplist\">Toplist</string>\n    <string name=\"favourite\">Favourite</string>\n    <string name=\"history\">History</string>\n    <string name=\"downloads\">Downloads</string>\n    <string name=\"settings\">Settings</string>\n    <string name=\"stub\" translatable=\"false\">Stub</string>\n\n    <!-- Gallery List Scene -->\n    <string name=\"search\">Search</string>\n    <string name=\"gallery_list_search_bar_hint_exhentai\">Search ExHentai</string>\n    <string name=\"gallery_list_search_bar_hint_e_hentai\">Search E-Hentai</string>\n    <string name=\"gallery_list_search_bar_open_gallery\">Open the gallery</string>\n    <string name=\"toplist_yesterday\">Yesterday</string>\n    <string name=\"toplist_pastmonth\">Past Month</string>\n    <string name=\"toplist_pastyear\">Past Year</string>\n    <string name=\"toplist_alltime\">All-Time</string>\n    <string name=\"gallery_list_empty_hit\">The World is Big and the panda sit alone</string>\n    <string name=\"gallery_list_empty_hit_subscription\">Subscribe to tags in Settings-&gt;EH-&gt;My tags</string>\n    <!-- Gallery List Action -->\n    <string name=\"read\">Read</string>\n    <string name=\"download\">Download</string>\n    <string name=\"delete_downloads\">Delete downloads</string>\n    <string name=\"add_to_favourites\">Add to favourites</string>\n    <string name=\"remove_from_favourites\">Remove from favourites</string>\n    <string name=\"download_move_dialog_title\">Move</string>\n    <string name=\"default_download_label_name\">Default</string>\n    <string name=\"remember_download_label\">Remember download label</string>\n    <string name=\"added_to_download_list\">Added to download list</string>\n    <string name=\"download_remove_dialog_title\">Remove Download Item</string>\n    <string name=\"download_remove_dialog_message\">Remove %s from download list ?</string>\n    <string name=\"download_remove_dialog_check_text\">Delete image files</string>\n    <string name=\"add_favorites_dialog_title\">Add to favorites</string>\n    <string name=\"local_favorites\">Local Favorites</string>\n    <string name=\"remember_favorite_collection\">Remember favorite collection</string>\n    <string name=\"add_favorite_note_dialog_title\">Add favorite note</string>\n    <string name=\"favorite_note\">Favorite Note</string>\n    <string name=\"favorite_note_never_show\">Don\\'t show this again</string>\n    <string name=\"add_to_favorite_success\">Added to favorites</string>\n    <string name=\"add_to_favorite_failure\">Failed to add to favorites</string>\n    <string name=\"remove_from_favorite_success\">Removed from favorites</string>\n    <string name=\"remove_from_favorite_failure\">Failed to remove from favorites</string>\n    <!-- Gallery List Fab Action -->\n    <string name=\"go_to\">Go to</string>\n    <string name=\"go_to_gid\">GID, leave blank for last page</string>\n    <string name=\"go_to_hint\">Page %1$d, total %2$d pages</string>\n    <!-- Gallery List Quick Search -->\n    <string name=\"quick_search\">Quick search</string>\n    <string name=\"quick_search_tip\">Tap \\\"+\\\" to add Quick Search</string>\n    <string name=\"readme\">README</string>\n    <string name=\"add_quick_search_tip\">The state of gallery list will be saved as quick search. Perform a search first to save the state of search panel.</string>\n    <string name=\"add_quick_search_dialog_title\">Add Quick Search</string>\n    <string name=\"save_progress\">Save reading progress</string>\n    <string name=\"name_is_empty\">Name is empty</string>\n    <string name=\"duplicate_name\">This name is already in use.</string>\n    <string name=\"image_search_not_quick_search\">Can\\'t add image search as quick search</string>\n    <string name=\"duplicate_quick_search\">A duplicate quick search exists. The name is \\\"%s\\\".</string>\n    <string name=\"delete\">Delete</string>\n    <string name=\"delete_quick_search_title\">Delete Quick Search</string>\n    <string name=\"delete_quick_search_message\">Delete \\\"%s\\\"?</string>\n\n    <!-- Gallery Search -->\n    <string name=\"search_normal\">Normal Search</string>\n    <string name=\"search_normal_search\">Normal search</string>\n    <string name=\"search_subscription_search\">Subscription search</string>\n    <string name=\"search_specify_uploader\">Specify uploader</string>\n    <string name=\"search_specify_tag\">Specify tag</string>\n    <string name=\"search_tip\">Normal search: Simply search.\\n\\nSubscription search: Search in subscriptions.\\n\\nSpecify uploader: Search for galleries uploaded by the specified uploader. All other options will be ignored.\\n\\nSpecify tag: Search for galleries that contain the specified tag. Only one tag accepted. Other options will be ignored.</string>\n    <string name=\"search_enable_advance\">Enable advance options</string>\n    <string name=\"search_advance\">Advanced Options</string>\n    <string name=\"search_sh\">Only Show Expunged Galleries</string>\n    <string name=\"search_sto\">Only Show Galleries With Torrents</string>\n    <string name=\"search_sr\">Minimum Rating:</string>\n    <string name=\"star_2\">2 stars</string>\n    <string name=\"star_3\">3 stars</string>\n    <string name=\"star_4\">4 stars</string>\n    <string name=\"star_5\">5 stars</string>\n    <string name=\"search_sp\">Pages:</string>\n    <string name=\"search_sp_to\">to</string>\n    <!-- A placeholder for other languages -->\n    <string name=\"search_sp_suffix\" />\n    <string name=\"search_sp_err0\">The page range minimum cannot be above 1000</string>\n    <string name=\"search_sp_err1\">The page range maximum cannot be below 10</string>\n    <string name=\"search_sp_err2\">The page range must be at least 20</string>\n    <string name=\"search_sp_err3\">The page range ratio cannot be above 0.5</string>\n    <string name=\"search_sf\">Disable default filters for:</string>\n    <string name=\"search_sfl\">Language</string>\n    <string name=\"search_sfu\">Uploader</string>\n    <string name=\"search_sft\">Tags</string>\n    <string name=\"search_image\">Image Search</string>\n    <string name=\"select_image\">Select image</string>\n    <string name=\"select_image_first\">Please select image first</string>\n    <string name=\"keyword_search\">Keyword search</string>\n    <string name=\"image_search\">Image search</string>\n    <!-- Category -->\n    <string name=\"doujinshi\" translatable=\"false\">DOUJINSHI</string>\n    <string name=\"manga\" translatable=\"false\">MANGA</string>\n    <string name=\"artist_cg\" translatable=\"false\">ARTIST CG</string>\n    <string name=\"game_cg\" translatable=\"false\">GAME CG</string>\n    <string name=\"western\" translatable=\"false\">WESTERN</string>\n    <string name=\"non_h\" translatable=\"false\">NON-H</string>\n    <string name=\"image_set\" translatable=\"false\">IMAGE SET</string>\n    <string name=\"cosplay\" translatable=\"false\">COSPLAY</string>\n    <string name=\"asian_porn\" translatable=\"false\">ASIAN PORN</string>\n    <string name=\"misc\" translatable=\"false\">MISC</string>\n\n    <!-- Gallery Detail Scene -->\n    <string name=\"download_upgradeable\">Upgradeable</string>\n    <string name=\"download_upgrade_existed\">The gallery already exists, please delete it and try again</string>\n    <string name=\"download_upgrade_service_failed\">Due to system limitations, the download service cannot be started from the background. Please click OK to start.</string>\n    <string name=\"read_from\">Read page %d</string>\n    <plurals name=\"page_count\">\n        <item quantity=\"one\">%d page</item>\n        <item quantity=\"other\">%d pages</item>\n    </plurals>\n    <string name=\"favored_times\">\\u2665 %d</string>\n    <string name=\"more_information\">More information</string>\n    <string name=\"newer_version_avaliable\">There are newer versions of this gallery available</string>\n    <string name=\"newer_version_title\">%1$s, added %2$s</string>\n    <string name=\"favorited\">Favorited</string>\n    <string name=\"not_favorited\">Not favorited</string>\n    <string name=\"share\">Share</string>\n    <string name=\"torrent_count\">Torrent (%d)</string>\n    <string name=\"archive\">Archive</string>\n    <string name=\"similar_gallery\">Similar</string>\n    <string name=\"rating_text\" translatable=\"false\">%1$s (%2$.2f, %3$d)</string>\n    <string name=\"no_tags\">No tags</string>\n    <string name=\"no_comments\">No comments</string>\n    <string name=\"more_comment\">More comments</string>\n    <string name=\"no_more_comments\">No more comments</string>\n    <string name=\"no_previews\">No previews</string>\n    <string name=\"more_previews\">More previews</string>\n    <string name=\"no_more_previews\">No more previews</string>\n    <!-- Gallery Actions -->\n    <string name=\"refresh\">Refresh</string>\n    <string name=\"action_add_tag\">Add tag</string>\n    <string name=\"action_add_tag_tip\">Enter new tags, separated with comma</string>\n    <string name=\"action_clear_image_cache\">Clear image cache</string>\n    <string name=\"action_image_cache_cleared\">Image cache cleared</string>\n    <string name=\"open_in_other_app\">Open with other app</string>\n    <string name=\"filter_the_uploader\">Block the uploader \\\"%s\\\"?</string>\n    <string name=\"filter_added\">Blocker added</string>\n    <string name=\"copy_trans\">Copy translation</string>\n    <string name=\"show_definition\">Show definition</string>\n    <string name=\"filter_the_tag\">Block the tag \\\"%s\\\"?</string>\n    <string name=\"tag_vote_up\">Vote up</string>\n    <string name=\"tag_vote_down\">Vote down</string>\n    <string name=\"tag_vote_up_cancel\">Cancel up-vote</string>\n    <string name=\"tag_vote_down_cancel\">Cancel down-vote</string>\n    <string name=\"tag_vote_successfully\">Vote successfully</string>\n    <string name=\"vote_failed\">Vote failed</string>\n    <!-- Gallery Info -->\n    <string name=\"gallery_info\">Gallery Info</string>\n    <string name=\"header_key\">Key</string>\n    <string name=\"header_value\">Value</string>\n    <string name=\"key_gid\" translatable=\"false\">GID</string>\n    <string name=\"key_token\" translatable=\"false\">Token</string>\n    <string name=\"key_url\">URL</string>\n    <string name=\"key_title\">Title</string>\n    <string name=\"key_title_jpn\">Jpn Title</string>\n    <string name=\"key_thumb\">Thumb</string>\n    <string name=\"key_category\">Category</string>\n    <string name=\"key_uploader\">Uploader</string>\n    <string name=\"key_posted\">Posted</string>\n    <string name=\"key_parent\">Parent</string>\n    <string name=\"key_visible\">Visible</string>\n    <string name=\"key_language\">Language</string>\n    <string name=\"key_pages\">Pages</string>\n    <string name=\"key_size\">Size</string>\n    <string name=\"key_favorite_count\">Favorite count</string>\n    <string name=\"key_favorited\">Favorited</string>\n    <string name=\"key_favorite_name\">Favorite</string>\n    <string name=\"key_rating_count\">Rating count</string>\n    <string name=\"key_rating\">Rating</string>\n    <string name=\"key_torrents\">Torrents</string>\n    <string name=\"key_torrent_url\">Torrent URL</string>\n    <string name=\"copied_to_clipboard\">Copied to clipboard</string>\n    <!-- Gallery Torrents -->\n    <string name=\"torrents\">Torrents</string>\n    <string name=\"no_torrents\">No torrents</string>\n    <string name=\"download_torrent_started\">Torrent download started</string>\n    <string name=\"download_torrent_failure\">Failed to download torrent</string>\n    <!-- Gallery Archives -->\n    <string name=\"no_archives\">No Archives</string>\n    <string name=\"archive_original\">Original archive</string>\n    <string name=\"archive_resample\">Resample archive</string>\n    <string name=\"archive_free\">Free</string>\n    <string name=\"current_funds\">Funds: %1$s GP, %2$d Credits</string>\n    <string name=\"insufficient_funds\">Insufficient GP</string>\n    <string name=\"download_archive_started\">Archive download started</string>\n    <string name=\"download_archive_failure\">Failed to download archive</string>\n    <string name=\"download_archive_failure_no_hath\">Need H@H client for archive download</string>\n    <!-- Gallery Rating -->\n    <string name=\"rate\">Rate</string>\n    <string name=\"rate_successfully\">Rate successfully</string>\n    <string name=\"rate_failed\">Rate failed</string>\n    <string name=\"rating10\">MASTERPIECE</string>\n    <string name=\"rating9\">AMAZING</string>\n    <string name=\"rating8\">GREAT</string>\n    <string name=\"rating7\">GOOD</string>\n    <string name=\"rating6\">OKAY</string>\n    <string name=\"rating5\">MEDIOCRE</string>\n    <string name=\"rating4\">BAD</string>\n    <string name=\"rating3\">AWFUL</string>\n    <string name=\"rating2\">PAINFUL</string>\n    <string name=\"rating1\">UNBEARABLE</string>\n    <string name=\"rating0\">DISASTER</string>\n    <string name=\"rating_none\" translatable=\"false\">(´_ゝ`)</string>\n    <!-- Gallery Comments -->\n    <string name=\"gallery_comments\">Gallery Comments</string>\n    <string name=\"no_one_comments_gallery\">No comment.</string>\n    <string name=\"comment_successfully\">Comment post successfully</string>\n    <string name=\"comment_failed\">Failed to post the comment</string>\n    <string name=\"comment_user_uploader\">%s (Uploader)</string>\n    <string name=\"last_edited\">Last edited: %s</string>\n    <string name=\"click_more_comments\">Click to load more comments</string>\n    <string name=\"copy_comment_text\">Copy comment text</string>\n    <string name=\"edit_comment\">Edit comment</string>\n    <string name=\"edit_comment_successfully\">The comment has been edited</string>\n    <string name=\"edit_comment_failed\">Failed to edit the comment</string>\n    <string name=\"block_commenter\">Block the commenter</string>\n    <string name=\"filter_the_commenter\">Block the commenter \\\"%s\\\"?</string>\n    <string name=\"vote_up\">Vote up</string>\n    <string name=\"cancel_vote_up\">Cancel up-vote</string>\n    <string name=\"vote_down\">Vote down</string>\n    <string name=\"cancel_vote_down\">Cancel down-vote</string>\n    <string name=\"vote_up_successfully\">Voted up successfully</string>\n    <string name=\"cancel_vote_up_successfully\">Cancel up-vote successfully</string>\n    <string name=\"vote_down_successfully\">Voted down successfully</string>\n    <string name=\"cancel_vote_down_successfully\">Down-vote cancelled successfully</string>\n    <string name=\"check_vote_status\">View vote details</string>\n    <string name=\"format_bold\"><b>Bold</b></string>\n    <string name=\"format_italic\"><i>Italic</i></string>\n    <string name=\"format_strikethrough\"><strike>Strikethrough</strike></string>\n    <string name=\"format_underline\"><u>Underline</u></string>\n    <string name=\"format_url\">URL</string>\n    <string name=\"format_plain\">Plain text</string>\n    <!-- Gallery Previews -->\n    <string name=\"gallery_previews\">Gallery Previews</string>\n\n    <!-- Favorites Scene -->\n    <string name=\"collections\">Collections</string>\n    <string name=\"favorites_search_bar_hint\">Search %s</string>\n    <string name=\"favorites_title\">%s</string>\n    <string name=\"favorites_title_2\">%1$s - %2$s</string>\n    <string name=\"need_sign_in\">Signing in is required</string>\n    <!-- Favorites Collection -->\n    <string name=\"default_favorites_collection\">Default favorites collection</string>\n    <string name=\"cloud_favorites\">Cloud Favorites</string>\n    <string name=\"let_me_select\">Let me select</string>\n    <!-- Favorites Fab Action -->\n    <string name=\"delete_favorites_dialog_title\">Delete from favorites</string>\n    <string name=\"delete_favorites_dialog_message\">Delete %d items from favorites?</string>\n    <string name=\"move_favorites_dialog_title\">Move favorites</string>\n\n    <!-- History Scene -->\n    <string name=\"no_history\">Viewed galleries will be shown here</string>\n    <string name=\"clear_all\">Clear all</string>\n    <string name=\"clear_all_history\">Clear all history?</string>\n\n    <!-- Download Scene -->\n    <string name=\"scene_download_title\">Download - %s</string>\n    <string name=\"no_download_info\">Download items will be shown here</string>\n    <string name=\"download_state_none\">Idle</string>\n    <string name=\"download_state_wait\">Waiting</string>\n    <string name=\"download_state_downloading\">Downloading</string>\n    <string name=\"download_state_downloaded\">Downloaded</string>\n    <string name=\"download_state_failed\">Failed</string>\n    <string name=\"download_state_failed_2\">%d incomplete</string>\n    <string name=\"download_state_finish\">Done</string>\n    <!-- Download Action Bar -->\n    <string name=\"download_filter\">Filter</string>\n    <string name=\"download_filter_title\">Filter title</string>\n    <string name=\"download_start_all\">Start all</string>\n    <string name=\"download_stop_all\">Stop all</string>\n    <string name=\"download_start_all_reversed\">Start all (reversed)</string>\n    <string name=\"download_sort_by\">Sort by</string>\n    <string name=\"download_sort_added_time_desc\">Added time (descending)</string>\n    <string name=\"download_sort_added_time_asc\">Added time (ascending)</string>\n    <string name=\"download_sort_title_asc\">Gallery title (ascending)</string>\n    <string name=\"download_sort_title_desc\">Gallery title (descending)</string>\n    <string name=\"download_sort_author_asc\">Gallery author (ascending)</string>\n    <string name=\"download_sort_author_desc\">Gallery author (descending)</string>\n    <string name=\"download_sort_name_asc\">Gallery name (ascending)</string>\n    <string name=\"download_sort_name_desc\">Gallery name (descending)</string>\n    <string name=\"download_sort_category_asc\">Gallery category (ascending)</string>\n    <string name=\"download_sort_category_desc\">Gallery category (descending)</string>\n    <string name=\"download_sort_shuffle\">Random</string>\n    <string name=\"download_reset_reading_progress\">Reset reading progress</string>\n    <string name=\"reset_reading_progress_message\">Reset the reading progress of all downloaded galleries?</string>\n    <!-- Download Fab Action -->\n    <string name=\"download_remove_dialog_message_2\">Remove %d items from download list ?</string>\n    <!-- Download Labels -->\n    <string name=\"download_labels\">Download labels</string>\n    <string name=\"download_all\">All</string>\n    <string name=\"add\">Add</string>\n    <string name=\"new_label_title\">New label</string>\n    <string name=\"label_text_is_empty\">Label text is empty</string>\n    <string name=\"label_text_is_invalid\">\\\"All\\\" or \\\"Default\\\" is an invalid label</string>\n    <string name=\"label_text_exist\">Label exists</string>\n    <string name=\"rename_label\">Rename</string>\n    <string name=\"rename_label_title\">Rename label</string>\n    <string name=\"delete_label_title\">Delete label</string>\n    <string name=\"delete_label_message\">Delete \\\"%s\\\"?</string>\n    <string name=\"default_download_label\">Default download label</string>\n    <!-- Download Service -->\n    <string name=\"download_service\">Download Service</string>\n    <string name=\"download_service_label\">EhViewer Download Service</string>\n    <string name=\"download_speed_text\" translatable=\"false\">%s</string>\n    <string name=\"download_speed_text_2\">%1$s, %2$s left</string>\n    <string name=\"stat_download_action_stop_all\">Stop all</string>\n    <string name=\"stat_509_alert_title\">509 Alert</string>\n    <string name=\"stat_509_alert_text\">Image limit has been reached. Please stop download and have a relax.</string>\n    <string name=\"stat_download_done_title\">Download Finished</string>\n    <string name=\"stat_download_done_text_succeeded\">%d succeeded</string>\n    <string name=\"stat_download_done_text_failed\">%d failed</string>\n    <string name=\"stat_download_done_text_mix\">%1$d succeeded, %2$d failed</string>\n    <string name=\"stat_download_done_line_succeeded\">Succeeded: %s</string>\n    <string name=\"stat_download_done_line_failed\">Failed: %s</string>\n\n    <!-- Settings -->\n    <string name=\"settings_eh\">EH</string>\n    <string name=\"settings_eh_account_name\">Account</string>\n    <string name=\"settings_eh_account_name_tourist\">Not signed in.</string>\n    <string name=\"settings_eh_account_refresh_igneous\">Refresh igneous</string>\n    <string name=\"settings_eh_account_sign_out\">Sign out</string>\n    <string name=\"settings_eh_account_sign_out_tip\">Signed out. You can sign in again shortly</string>\n    <string name=\"settings_eh_account_identity_cookies\">Identity cookies can be used to sign in to this account.\\nKEEP IT SAFE\\n\\n%s</string>\n    <string name=\"settings_eh_account_igneous_expire\">\"igneous auto refresh: \"</string>\n    <string name=\"settings_eh_account_identity_cookies_copy\">Copy</string>\n    <string name=\"settings_eh_gallery_site\">Gallery site</string>\n    <string name=\"settings_eh_image_limits\">Image Limits</string>\n    <string name=\"settings_eh_image_limits_summary\">Loading…</string>\n    <string name=\"settings_eh_image_limits_summary_ip\">\"IP-based limits: \"</string>\n    <string name=\"settings_eh_image_limits_summary_ip_ok\">no restrictions</string>\n    <string name=\"settings_eh_image_limits_summary_ip_restricted\">lower-resolution images</string>\n    <string name=\"settings_eh_image_limits_summary_acc\">Current: %1$d / %2$d</string>\n    <string name=\"settings_eh_unlock_cost\">You can unlock a high-resolution quota for 24 hours by spending %1$d GP</string>\n    <string name=\"settings_eh_unlock\">Unlock</string>\n    <string name=\"settings_eh_reset_cost\">Reset Cost: %1$d GP</string>\n    <string name=\"settings_eh_reset\">Reset</string>\n    <string name=\"settings_eh_reset_limits_succeed\">Image limit was successfully reset</string>\n    <string name=\"settings_eh_u_config\">E-Hentai settings</string>\n    <string name=\"settings_eh_u_config_summary\">Settings on E-Hentai website</string>\n    <string name=\"settings_eh_my_tags\">My tags</string>\n    <string name=\"settings_eh_my_tags_summary\">My tags on E-Hentai website</string>\n    <string name=\"settings_eh_black_dark_theme\">Black dark theme</string>\n    <string name=\"settings_eh_launch_page\">Launch page</string>\n    <string name=\"settings_eh_list_mode\">List mode</string>\n    <string name=\"settings_eh_list_mode_detail\">Detail</string>\n    <string name=\"settings_eh_list_mode_thumb\">Thumb</string>\n    <string name=\"settings_eh_detail_size\">Item width in detail mode</string>\n    <string name=\"settings_eh_list_tile_thumb_size\">Thumb size in detail mode</string>\n    <string name=\"settings_eh_thumb_size\">Thumb size in thumb mode</string>\n    <string name=\"settings_eh_thumb_show_title\">Show title in thumb mode</string>\n    <string name=\"settings_eh_show_jpn_title\">Show Japanese title</string>\n    <string name=\"settings_eh_show_jpn_title_summary\">Require enabling Japanese Title in Settings on E-Hentai website</string>\n    <string name=\"settings_eh_show_gallery_pages\">Show gallery pages</string>\n    <string name=\"settings_eh_show_gallery_pages_summary\">Display the number of pages in the gallery list</string>\n    <string name=\"settings_eh_show_gallery_comments\">Show gallery comments</string>\n    <string name=\"settings_eh_show_gallery_comments_summary\">Show comments on the gallery details page</string>\n    <string name=\"settings_eh_comment_threshold\">Comment score threshold</string>\n    <string name=\"settings_eh_comment_threshold_summary\">Hide comments at or below this score (-101 disables)</string>\n    <string name=\"settings_eh_preview_num\">The maximum number of previews on the gallery details page</string>\n    <string name=\"settings_eh_preview_size\">Preview image size on the gallery details page</string>\n    <string name=\"settings_eh_show_tag_translations\">Show tag translations</string>\n    <string name=\"settings_eh_show_tag_translations_summary\">Show tag translations instead of the original text (It takes time to download the data file)</string>\n    <string name=\"settings_eh_tag_translations_source\">Placeholder</string>\n    <string name=\"settings_eh_tag_translations_source_url\">https://placeholder</string>\n    <string name=\"settings_eh_filter\">Blockers</string>\n    <string name=\"settings_eh_filter_summary\">Block gallery or comment by title, uploader, tags and commenter</string>\n    <string name=\"settings_eh_metered_network_warning\">Metered network warning</string>\n    <string name=\"settings_eh_request_news\">Request news page on start</string>\n    <string name=\"settings_eh_hide_hv_events\">Hide HV event notifications</string>\n    <string name=\"settings_read\">Read</string>\n    <string name=\"settings_read_screen_rotation\">Screen orientation</string>\n    <string name=\"settings_read_screen_rotation_default\">Default</string>\n    <string name=\"settings_read_screen_rotation_portrait\">Portrait</string>\n    <string name=\"settings_read_screen_rotation_landscape\">Landscape</string>\n    <string name=\"settings_read_screen_rotation_sensor\">Auto rotate</string>\n    <string name=\"settings_read_reading_direction\">Reading direction</string>\n    <string name=\"settings_read_reading_direction_left_to_right\">Left to right</string>\n    <string name=\"settings_read_reading_direction_right_to_Left\">Right to left</string>\n    <string name=\"settings_read_reading_direction_top_to_bottom\">Top to bottom</string>\n    <string name=\"settings_read_page_scaling\">Page scaling</string>\n    <string name=\"settings_read_page_scaling_actual_size\">Actual size</string>\n    <string name=\"settings_read_page_scaling_fit_to_width\">Fit to width</string>\n    <string name=\"settings_read_page_scaling_fit_to_height\">Fit to height</string>\n    <string name=\"settings_read_page_scaling_fit_to_screen\">Fit to screen</string>\n    <string name=\"settings_read_page_scaling_fixed_scale\">Fixed scale</string>\n    <string name=\"settings_read_start_position\">Start position</string>\n    <string name=\"settings_read_start_position_top_left\">Top left</string>\n    <string name=\"settings_read_start_position_top_right\">Top right</string>\n    <string name=\"settings_read_start_position_bottom_left\">Bottom left</string>\n    <string name=\"settings_read_start_position_bottom_right\">Bottom right</string>\n    <string name=\"settings_read_start_position_center\">Center</string>\n    <string name=\"settings_read_theme\">Color theme</string>\n    <string name=\"settings_read_theme_follow_app\">Follow app</string>\n    <string name=\"settings_read_theme_dark\">Dark</string>\n    <string name=\"settings_read_theme_light\">Light</string>\n    <string name=\"settings_read_keep_screen_on\">Keep screen on</string>\n    <string name=\"settings_read_show_clock\">Show clock</string>\n    <string name=\"settings_read_show_progress\">Show progress</string>\n    <string name=\"settings_read_show_battery\">Show battery</string>\n    <string name=\"settings_read_show_page_interval\">Show page interval</string>\n    <string name=\"settings_read_turn_page_interval\">Turn page interval (in seconds)</string>\n    <string name=\"settings_read_volume_page\">Use volume key to turn pages</string>\n    <string name=\"settings_read_volume_page_interval\">Volume key turn page interval</string>\n    <string name=\"settings_read_reverse_volume\">Reverse volume key</string>\n    <string name=\"settings_read_reading_fullscreen\">Fullscreen</string>\n    <string name=\"settings_read_custom_screen_lightness\">Custom screen lightness</string>\n    <string name=\"settings_read_screen_lightness\">Screen lightness</string>\n    <string name=\"settings_download\">Download</string>\n    <string name=\"settings_download_download_location\">Download location</string>\n    <string name=\"settings_download_pick_new_location\">Pick a new location</string>\n    <string name=\"settings_download_reset_location\">Reset to default</string>\n    <string name=\"settings_download_invalid_download_location\">Invalid download location</string>\n    <string name=\"settings_download_cant_get_download_location\">Can\\'t get download location</string>\n    <string name=\"settings_download_media_scan\">Allow media scan</string>\n    <string name=\"settings_download_media_scan_summary_on\">Please hide your gallery apps away from other people</string>\n    <string name=\"settings_download_media_scan_summary_off\">Most gallery apps will ignore pictures in the download path</string>\n    <string name=\"settings_download_concurrency\">Concurrency download</string>\n    <string name=\"settings_download_concurrency_summary\">Up to %s images</string>\n    <string name=\"settings_download_download_delay\">Download delay</string>\n    <string name=\"settings_download_download_delay_summary\">Delay %s ms per download</string>\n    <string name=\"settings_download_download_timeout\">Download timeout (in seconds)</string>\n    <string name=\"settings_download_preload_image\">Preload image</string>\n    <string name=\"settings_download_preload_image_summary\">Preload next %s image</string>\n    <string name=\"settings_download_download_origin_image\">Load original image</string>\n    <string name=\"settings_download_download_origin_image_summary\">%s, Caution! May require GP</string>\n    <string name=\"settings_download_download_origin_image_never\">Never</string>\n    <string name=\"settings_download_download_origin_image_force\">Always</string>\n    <string name=\"settings_download_download_origin_image_only\">Download only</string>\n    <string name=\"settings_download_task_confirm\">Are you sure you want to perform this action?</string>\n    <string name=\"settings_download_restore_download_items\">Restore download items</string>\n    <string name=\"settings_download_restore_download_items_summary\">Restore all download items in download location</string>\n    <string name=\"settings_download_restore_not_found\">Not found download items to restore</string>\n    <string name=\"settings_download_restore_failed\">Restore failed</string>\n    <string name=\"settings_download_restore_successfully\">Restore %d items successfully</string>\n    <string name=\"settings_download_clean_redundancy\">Clear download redundancy</string>\n    <string name=\"settings_download_clean_redundancy_summary\">Remove gallery images which are not in download list but in download location</string>\n    <string name=\"settings_download_clean_redundancy_no_redundancy\">No redundancy</string>\n    <string name=\"settings_download_clean_redundancy_done\">Redundancy cleaning completed, clean-up %d items totally</string>\n    <string name=\"settings_privacy\">Privacy</string>\n    <string name=\"settings_privacy_pattern_protection_title\">Pattern protection</string>\n    <string name=\"settings_privacy_pattern_protection_not_set\">Pattern protection has not been set</string>\n    <string name=\"settings_privacy_pattern_protection_set\">Pattern protection has been set</string>\n    <string name=\"settings_privacy_secure\">Prevent screenshots</string>\n    <string name=\"settings_privacy_secure_summary\">Prevent the content of the app from being taken screenshots of or shown in the \\\"Recent Apps\\\" list</string>\n    <string name=\"settings_privacy_clear_search_history\">Clear device search history</string>\n    <string name=\"settings_privacy_clear_search_history_summary\">Remove searches you have performed from this device</string>\n    <string name=\"settings_privacy_clear_search_history_cleared\">Search history cleared</string>\n    <string name=\"settings_advanced\">Advanced</string>\n    <string name=\"settings_advanced_save_parse_error_body\">Save HTML content when parsing error</string>\n    <string name=\"settings_advanced_save_parse_error_body_summary\">Html content may be privacy-sensitive</string>\n    <string name=\"settings_advanced_save_crash_log\">Save crash log when app crashes</string>\n    <string name=\"settings_advanced_save_crash_log_summary\">Crash logs help developers fix bugs</string>\n    <string name=\"settings_advanced_dump_logcat\">Dump logcat</string>\n    <string name=\"settings_advanced_dump_logcat_summary\">Save logcat to external storage</string>\n    <string name=\"settings_advanced_dump_logcat_failed\">Dump logcat failed</string>\n    <string name=\"settings_advanced_dump_logcat_to\">Logcat dumped to %s</string>\n    <string name=\"settings_advanced_read_cache_size\">Read cache size</string>\n    <string name=\"settings_advanced_app_language_title\">App language</string>\n    <string name=\"settings_advanced_proxy\">Proxy</string>\n    <string name=\"settings_advanced_proxy_summary_1\" translatable=\"false\">%1$s %2$s:%3$d</string>\n    <string name=\"settings_advanced_proxy_summary_2\" translatable=\"false\">%1$s</string>\n    <string name=\"settings_advanced_user_agent_title\" translatable=\"false\">User Agent</string>\n    <string name=\"settings_advanced_backup_favorite\">Backup favorite list</string>\n    <string name=\"settings_advanced_backup_favorite_summary\">Backup remote favorite list to local</string>\n    <string name=\"settings_advanced_backup_favorite_start\">Backing up favorite list %s</string>\n    <string name=\"settings_advanced_backup_favorite_nothing\">Nothing to backup</string>\n    <string name=\"settings_advanced_backup_favorite_success\">Backup favorite list success</string>\n    <string name=\"settings_advanced_backup_favorite_failed\">Backup favorite list failed</string>\n    <string name=\"settings_advanced_export_data\">Export data</string>\n    <string name=\"settings_advanced_export_data_summary\">Save data like download list, quick search list, to external storage</string>\n    <string name=\"settings_advanced_export_data_to\">Exported data to %s</string>\n    <string name=\"settings_advanced_export_data_failed\">Failed to export data</string>\n    <string name=\"settings_advanced_import_data\">Import data</string>\n    <string name=\"settings_advanced_import_data_summary\">Load data which were previously saved</string>\n    <string name=\"settings_advanced_import_data_successfully\">Data imported</string>\n    <string name=\"settings_advanced_import_data_cant_read\">Can\\'t read the file</string>\n    <string name=\"settings_advanced_open_by_default\">Open by default</string>\n    <string name=\"settings_about\">About</string>\n    <string name=\"settings_about_declaration\" translatable=\"false\">EhViewer</string>\n    <string name=\"settings_about_declaration_summary\">EhViewer is not affiliated with E-Hentai.org in any way</string>\n    <string name=\"settings_about_author\">Author</string>\n    <string name=\"settings_about_author_summary\" translatable=\"false\">&lt;del&gt;Hippo &amp;lt;ehviewersu$gmail.com&amp;gt;&lt;/del&gt;&lt;br&gt;&lt;del&gt;NekoInverter&lt;/del&gt;&lt;br&gt;小白-白</string>\n    <string name=\"settings_about_telegram\" translatable=\"false\">Telegram</string>\n    <string name=\"settings_about_issues\">FAQ</string>\n    <string name=\"settings_about_latest_release\">Latest release</string>\n    <string name=\"settings_about_source\">Source</string>\n    <string name=\"settings_about_version\">Build version</string>\n    <string name=\"settings_about_build_time\">Built at %s</string>\n    <!-- UConfig Activity -->\n    <string name=\"u_config\">EHentai settings</string>\n    <string name=\"apply\">Apply</string>\n    <string name=\"apply_tip\">Tap the check mark to save the settings</string>\n    <!-- My Tags Activity -->\n    <string name=\"my_tags\">My tags</string>\n    <!-- Filter Activity -->\n    <string name=\"filter\">Blockers</string>\n    <string name=\"tip\">Tip</string>\n    <string name=\"filter_tip\">This blocking system filters the result of EHentai website blocking system.\\n\\nTitle Blocker: exclude the gallery whose title contains the word.\\n\\nUploader Blocker: exclude the gallery which was uploaded by the uploader.\\n\\nTag Blocker: exclude the gallery which contain the tag, it takes more time to get gallery list.\\n\\nTag Namespace Blocker: exclude the gallery which contain the tag namespace, it takes more time to get gallery list.\\n\\nCommenter Blocker: exclude the comments posted by the commenter.\\n\\nComment Blocker: exclude the comments matching the regex.</string>\n    <string name=\"no_filter\">Blockers will be shown here</string>\n    <string name=\"add_filter\">Add blocker</string>\n    <string name=\"filter_title\">Title</string>\n    <string name=\"filter_uploader\">Uploader</string>\n    <string name=\"filter_tag\">Tag</string>\n    <string name=\"filter_tag_namespace\">Tag namespace</string>\n    <string name=\"filter_commenter\">Commenter</string>\n    <string name=\"filter_comment\">Comment Regex</string>\n    <string name=\"filter_text\">Blocker text</string>\n    <string name=\"delete_filter\">Delete blocker \\\"%s\\\"?</string>\n    <!-- Set Security -->\n    <string name=\"set_pattern_protection\">Set pattern protection</string>\n    <string name=\"set_pattern_protection_tip\">Draw a pattern to set pattern protection\\nLeave blank to clear pattern protection</string>\n    <string name=\"enable_biometric\">Also allow biometric unlock</string>\n    <string name=\"set\">Set</string>\n    <!-- Languages -->\n    <string name=\"app_language_system\">System Language (Default)</string>\n    <string name=\"app_language_en\" translatable=\"false\">English</string>\n    <string name=\"app_language_ja\" translatable=\"false\">日本語</string>\n    <string name=\"app_language_zh_cn\" translatable=\"false\">中文（大陆）</string>\n    <string name=\"app_language_zh_hk\" translatable=\"false\">中文（香港）</string>\n    <string name=\"app_language_zh_tw\" translatable=\"false\">中文（臺灣）</string>\n    <!-- Proxy -->\n    <string name=\"proxy_direct\">Direct</string>\n    <string name=\"proxy_system\">System Proxy</string>\n    <string name=\"proxy_host_or_ip\">Host or IP</string>\n    <string name=\"proxy_port\">Port</string>\n    <string name=\"proxy_invalid_port\">Invalid port</string>\n\n    <!-- Errors -->\n    <string name=\"error_status_code_400\" translatable=\"false\">Bad Request</string>\n    <string name=\"error_status_code_401\" translatable=\"false\">Unauthorized</string>\n    <string name=\"error_status_code_402\" translatable=\"false\">Payment Required</string>\n    <string name=\"error_status_code_403\" translatable=\"false\">Forbidden</string>\n    <string name=\"error_status_code_404\" translatable=\"false\">Not Found</string>\n    <string name=\"error_status_code_405\" translatable=\"false\">Method Not Allowed</string>\n    <string name=\"error_status_code_406\" translatable=\"false\">Not Acceptable</string>\n    <string name=\"error_status_code_407\" translatable=\"false\">Proxy Authentication Required</string>\n    <string name=\"error_status_code_408\" translatable=\"false\">Request Timeout</string>\n    <string name=\"error_status_code_409\" translatable=\"false\">Conflict</string>\n    <string name=\"error_status_code_410\" translatable=\"false\">Gone</string>\n    <string name=\"error_status_code_411\" translatable=\"false\">Length Required</string>\n    <string name=\"error_status_code_412\" translatable=\"false\">Precondition Failed</string>\n    <string name=\"error_status_code_413\" translatable=\"false\">Request Entity Too Large</string>\n    <string name=\"error_status_code_414\" translatable=\"false\">Request-URI Too Long</string>\n    <string name=\"error_status_code_415\" translatable=\"false\">Unsupported Media Type</string>\n    <string name=\"error_status_code_416\" translatable=\"false\">Requested Range Not Satisfiable</string>\n    <string name=\"error_status_code_417\" translatable=\"false\">Expectation Failed</string>\n    <string name=\"error_status_code_500\" translatable=\"false\">Internal Server Error</string>\n    <string name=\"error_status_code_501\" translatable=\"false\">Not Implemented</string>\n    <string name=\"error_status_code_502\" translatable=\"false\">Bad Gateway</string>\n    <string name=\"error_status_code_503\" translatable=\"false\">Service Unavailable</string>\n    <string name=\"error_status_code_504\" translatable=\"false\">Gateway Timeout</string>\n    <string name=\"error_status_code_505\" translatable=\"false\">HTTP Version Not Supported</string>\n    <string name=\"error_bad_status_code\">Bad status code: %d</string>\n    <string name=\"error_timeout\">Timeout</string>\n    <string name=\"error_unknown_host\">Unknown host</string>\n    <string name=\"error_redirection\">Too many redirections</string>\n    <string name=\"error_socket\">Network error</string>\n    <string name=\"error_unknown\">Weird</string>\n    <string name=\"error_cant_find_activity\">Can\\'t find the application</string>\n    <string name=\"error_cannot_parse_the_url\">Can\\'t parse the URL</string>\n    <string name=\"error_decoding_failed\">Decoding failed</string>\n    <string name=\"error_empty\">Empty</string>\n    <string name=\"error_reading_failed\">Reading Failed</string>\n    <string name=\"error_out_of_range\">Out of range</string>\n    <string name=\"error_write_failed\">Write failed</string>\n    <string name=\"error_parse_error\">Parse error</string>\n    <string name=\"error_509\" translatable=\"false\">509</string>\n    <string name=\"error_invalid_url\">Invalid URL</string>\n    <string name=\"error_get_ptoken_error\">Get pToken error</string>\n    <string name=\"error_cant_create_temp_file\">Couldn\\'t create temp file</string>\n    <string name=\"error_cant_save_image\">Can\\'t save image</string>\n    <string name=\"error_invalid_number\">Invalid number</string>\n    <string name=\"error_please_login_first\">Please log in first</string>\n    <string name=\"error_cannot_find_gallery\">Couldn\\'t find the gallery</string>\n    <string name=\"error_something_wrong_happened\">Something wrong happened</string>\n    <string name=\"kokomade_tip\">Settings-&gt;Eh-&gt;Gallery Site-&gt;E-Hentai</string>\n    <string name=\"no_browser_installed\">Just install a browser please.</string>\n    <string name=\"cloudflare_bypass_failed\">Failed to bypass Cloudflare challenge</string>\n    <string name=\"open_in_webview\">Open URL in WebView to finish the challenge?</string>\n    <string name=\"information_webview_outdated\">Please update the WebView app for better compatibility</string>\n\n    <!-- Readable Time -->\n    <string name=\"from_the_future\">From the future</string>\n    <string name=\"just_now\">Just now</string>\n    <plurals name=\"some_minutes_ago\">\n        <item quantity=\"one\">A minute ago</item>\n        <item quantity=\"other\">%d minutes ago</item>\n    </plurals>\n    <plurals name=\"some_hours_ago\">\n        <item quantity=\"one\">An hour ago</item>\n        <item quantity=\"other\">%d hours ago</item>\n    </plurals>\n    <string name=\"yesterday\">Yesterday</string>\n    <string name=\"some_days_ago\">%d days ago</string>\n    <plurals name=\"second\">\n        <item quantity=\"one\">sec</item>\n        <item quantity=\"other\">secs</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">min</item>\n        <item quantity=\"other\">mins</item>\n    </plurals>\n    <plurals name=\"hour\">\n        <item quantity=\"one\">hour</item>\n        <item quantity=\"other\">hours</item>\n    </plurals>\n    <plurals name=\"day\">\n        <item quantity=\"one\">day</item>\n        <item quantity=\"other\">days</item>\n    </plurals>\n    <plurals name=\"year\">\n        <item quantity=\"one\">year</item>\n        <item quantity=\"other\">years</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n    <style name=\"AppTheme\" parent=\"Base.AppTheme.Light\" />\n\n    <!-- TextAppearance -->\n    <style name=\"TextAppearance\" parent=\"TextAppearance.AppCompat\" />\n\n    <!-- Progress View -->\n    <style name=\"ProgressView\">\n        <item name=\"indicatorColor\">?attr/progressColor</item>\n        <item name=\"android:indeterminate\">true</item>\n        <item name=\"android:minWidth\">48dp</item>\n        <item name=\"android:minHeight\">48dp</item>\n    </style>\n\n    <style name=\"TextAppearance.CardTitle\">\n        <item name=\"android:textSize\">@dimen/text_little_small</item>\n        <item name=\"android:textColor\">?android:attr/textColorPrimary</item>\n    </style>\n\n    <style name=\"CardTitle\">\n        <item name=\"android:textAppearance\">@style/TextAppearance.CardTitle</item>\n        <item name=\"android:ellipsize\">end</item>\n        <item name=\"android:maxLines\">2</item>\n        <item name=\"android:lineSpacingExtra\">0sp</item>\n    </style>\n\n    <style name=\"TextAppearance.CardMessage\">\n        <item name=\"android:textSize\">@dimen/text_small</item>\n        <item name=\"android:textColor\">?android:attr/textColorSecondary</item>\n    </style>\n\n    <style name=\"CardMessage\">\n        <item name=\"android:textAppearance\">@style/TextAppearance.CardMessage</item>\n        <item name=\"android:ellipsize\">end</item>\n        <item name=\"android:singleLine\">true</item>\n        <item name=\"android:maxLines\">1</item>\n    </style>\n\n    <!-- CategoryText -->\n    <style name=\"CategoryText\">\n        <item name=\"android:textAppearance\">@style/TextAppearance.AppCompat.Subhead</item>\n        <item name=\"android:textStyle\">bold</item>\n        <item name=\"android:gravity\">center</item>\n        <item name=\"android:minHeight\">@dimen/category_table_item_height</item>\n        <item name=\"android:foreground\">@drawable/check_text_view_foreground</item>\n        <item name=\"android:theme\">@style/Theme.MaterialComponents</item>\n    </style>\n\n    <style name=\"TextAppearance.CategoryTitle\" parent=\"TextAppearance.AppCompat.Small\">\n        <item name=\"android:textColor\">?attr/textColorThemeAccent</item>\n        <item name=\"android:textStyle\">bold</item>\n    </style>\n\n    <style name=\"CategoryTitle\">\n        <item name=\"android:textAppearance\">@style/TextAppearance.CategoryTitle</item>\n        <item name=\"android:singleLine\">true</item>\n        <item name=\"android:ellipsize\">marquee</item>\n        <item name=\"android:gravity\">center_vertical</item>\n    </style>\n\n    <style name=\"ButtonInCard\">\n        <item name=\"android:textAppearance\">@style/TextAppearance.AppCompat.Button</item>\n        <item name=\"android:textStyle\">bold</item>\n        <item name=\"android:gravity\">center</item>\n        <item name=\"android:minHeight\">48dp</item>\n        <item name=\"android:minWidth\">88dp</item>\n        <item name=\"android:padding\">8dp</item>\n        <item name=\"android:textColor\">?attr/colorPrimary</item>\n    </style>\n\n    <style name=\"Slider\">\n        <item name=\"thickness\">2dp</item>\n        <item name=\"radius\">6dp</item>\n        <item name=\"color\">?attr/widgetColorThemeAccent</item>\n        <item name=\"textColor\">@android:color/white</item>\n        <item name=\"textSize\">12sp</item>\n        <item name=\"dark\">false</item>\n    </style>\n\n    <style name=\"Indicating\">\n        <item name=\"indicatorHeight\">1dp</item>\n        <item name=\"indicatorColor\">?attr/dividerColor</item>\n    </style>\n\n    <style name=\"Guide.Title\" parent=\"TextAppearance.AppCompat.Title\">\n        <item name=\"android:textColor\">?attr/guideTitleColor</item>\n    </style>\n\n    <style name=\"CardView.Normal\">\n        <item name=\"cardBackgroundColor\">?attr/contentColorPrimary</item>\n    </style>\n\n    <style name=\"CardView.Reactive\" parent=\"CardView.Normal\">\n        <item name=\"cardBackgroundColor\">?attr/contentColorReactive</item>\n    </style>\n\n    <style name=\"ShapeAppearanceOverlay.App.CornerSize50Percent\" parent=\"\">\n        <item name=\"cornerSize\">50%</item>\n    </style>\n\n    <style name=\"ShapeAppearanceOverlay.MaterialComponents.NavigationView\" parent=\"\">\n        <item name=\"cornerSizeTopLeft\">0dp</item>\n        <item name=\"cornerSizeBottomLeft\">0dp</item>\n        <item name=\"cornerSize\">50%</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <!-- ========== -->\n    <!-- Base Theme -->\n    <!-- ========== -->\n    <style name=\"Base.AppTheme.Light\" parent=\"Theme.Material.Light\">\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n        <item name=\"android:colorBackground\">@color/colorBackground</item>\n        <item name=\"android:windowSplashScreenIconBackgroundColor\" tools:targetApi=\"s\">@color/ic_launcher_background</item>\n\n        <item name=\"dividerColor\">@color/divider</item>\n        <item name=\"toolbarColor\">@color/colorPrimary</item>\n        <item name=\"textColorThemePrimary\">@color/colorPrimary</item>\n        <item name=\"textColorThemeAccent\">@color/colorAccent</item>\n        <item name=\"colorControlNormal\">@color/primary_drawable</item>\n        <item name=\"drawableColorSecondary\">@color/secondary_drawable</item>\n        <item name=\"drawableColorThemePrimary\">@color/colorPrimary</item>\n        <item name=\"contentColorPrimary\">@color/content</item>\n        <item name=\"contentColorThemePrimary\">@color/colorPrimary</item>\n        <item name=\"contentColorThemeAccent\">@color/colorAccent</item>\n        <item name=\"contentColorReactive\">@color/content_reactive</item>\n        <item name=\"widgetColorThemePrimary\">@color/colorPrimary</item>\n        <item name=\"widgetColorThemeAccent\">@color/colorAccent</item>\n        <item name=\"progressColor\">@color/progress</item>\n        <item name=\"guideBackgroundColor\">@color/guide_bg</item>\n        <item name=\"guideTitleColor\">?android:attr/textColorPrimaryInverse</item>\n        <item name=\"guideTextColor\">?android:attr/textColorSecondaryInverse</item>\n        <item name=\"tagGroupBackgroundColor\">@color/tag_group_background</item>\n        <item name=\"tagBackgroundColor\">@color/tag_background</item>\n        <item name=\"galleryDetailHeaderTitleColor\">?android:attr/textColorPrimaryInverse</item>\n        <item name=\"galleryDetailHeaderBackgroundColor\">?attr/contentColorThemePrimary</item>\n        <item name=\"galleryDetailButtonBackgroundColor\">@color/gallery_detail_button_background\n        </item>\n\n        <item name=\"toolbarPopupTheme\">@style/ThemeOverlay.MaterialComponents.Light</item>\n        <item name=\"galleryDetailDivider\">@drawable/divider_gallery_detail</item>\n\n        <item name=\"android:windowActionBar\">false</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n\n    <style name=\"Base.AppTheme\" parent=\"Theme.Material\">\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/grey_875</item>\n        <item name=\"colorAccent\">@color/colorPrimary</item>\n        <item name=\"colorControlActivated\">@color/colorPrimary</item>\n        <item name=\"android:colorBackground\">@color/grey_850</item>\n        <item name=\"android:windowSplashScreenIconBackgroundColor\" tools:targetApi=\"31\">@color/ic_launcher_background</item>\n\n        <!-- Non-overhead workaround for ROMs enable it by default, MIUI etc. -->\n        <item name=\"android:forceDarkAllowed\" tools:targetApi=\"29\">false</item>\n\n        <item name=\"dividerColor\">@color/divider</item>\n        <item name=\"toolbarColor\">?android:attr/colorBackground</item>\n        <item name=\"textColorThemePrimary\">?android:attr/textColorSecondary</item>\n        <item name=\"textColorThemeAccent\">?android:attr/textColorSecondary</item>\n        <item name=\"colorControlNormal\">@color/primary_drawable</item>\n        <item name=\"drawableColorSecondary\">@color/secondary_drawable</item>\n        <item name=\"drawableColorThemePrimary\">@color/primary_drawable</item>\n        <item name=\"contentColorPrimary\">@color/content</item>\n        <item name=\"contentColorThemePrimary\">?attr/contentColorPrimary</item>\n        <item name=\"contentColorThemeAccent\">?attr/contentColorPrimary</item>\n        <item name=\"contentColorReactive\">@color/content_reactive</item>\n        <item name=\"widgetColorThemePrimary\">@color/widget</item>\n        <item name=\"widgetColorThemeAccent\">@color/widget</item>\n        <item name=\"progressColor\">@color/progress</item>\n        <item name=\"guideBackgroundColor\">#e5303030</item>\n        <item name=\"guideTitleColor\">?android:attr/textColorPrimary</item>\n        <item name=\"guideTextColor\">?android:attr/textColorSecondary</item>\n        <item name=\"tagGroupBackgroundColor\">@color/tag_group_background</item>\n        <item name=\"tagBackgroundColor\">@color/tag_background</item>\n        <item name=\"galleryDetailHeaderTitleColor\">?android:attr/textColorPrimary</item>\n        <item name=\"galleryDetailHeaderBackgroundColor\">?attr/contentColorThemePrimary</item>\n        <item name=\"galleryDetailButtonBackgroundColor\">@color/gallery_detail_button_background</item>\n\n        <item name=\"toolbarPopupTheme\">@style/ThemeOverlay.MaterialComponents.Dark</item>\n        <item name=\"galleryDetailDivider\">@drawable/divider_gallery_detail_dark</item>\n\n        <item name=\"android:windowActionBar\">false</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n\n    <!-- =============== -->\n    <!-- Base Black Theme -->\n    <!-- =============== -->\n\n    <style name=\"ThemeOverlay\" />\n\n    <style name=\"ThemeOverlay.Black\">\n        <item name=\"colorPrimaryDark\">@android:color/black</item>\n\n        <item name=\"android:colorBackground\">@android:color/black</item>\n\n        <item name=\"android:textColorPrimary\">@color/primary_text_material_black</item>\n\n        <item name=\"dividerColor\">@color/divider_black</item>\n        <item name=\"toolbarColor\">@android:color/black</item>\n        <item name=\"colorControlNormal\">@color/primary_drawable_black</item>\n        <item name=\"drawableColorSecondary\">@color/secondary_drawable_black</item>\n        <item name=\"drawableColorThemePrimary\">@color/primary_drawable_black</item>\n        <item name=\"contentColorPrimary\">@color/content_black</item>\n        <item name=\"contentColorReactive\">@color/content_reactive_black</item>\n        <item name=\"widgetColorThemePrimary\">@color/widget_black</item>\n        <item name=\"widgetColorThemeAccent\">@color/widget_black</item>\n        <item name=\"progressColor\">@color/progress_black</item>\n        <item name=\"guideBackgroundColor\">#e5000000</item>\n        <item name=\"tagGroupBackgroundColor\">@color/tag_group_background_black</item>\n        <item name=\"tagBackgroundColor\">@color/tag_background_black</item>\n        <item name=\"galleryDetailHeaderBackgroundColor\">@android:color/black</item>\n        <item name=\"galleryDetailButtonBackgroundColor\">\n            @color/gallery_detail_button_background_black\n        </item>\n\n        <item name=\"galleryDetailDivider\">@drawable/divider_gallery_detail_dark</item>\n    </style>\n\n    <style name=\"AppTheme.Gallery\" parent=\"AppTheme\">\n        <item name=\"gallerySliderBackgroundColor\">@color/gallery_slider_background</item>\n    </style>\n\n    <!-- ============ -->\n    <!-- Widget Theme -->\n    <!-- ============ -->\n    <style name=\"CommentEditText\" parent=\"ThemeOverlay.MaterialComponents\">\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@android:color/white</item>\n    </style>\n\n    <style name=\"RatingBarTheme\" parent=\"ThemeOverlay.MaterialComponents\">\n        <item name=\"colorControlNormal\">@color/yellow_800</item>\n        <item name=\"colorControlActivated\">@color/yellow_800</item>\n    </style>\n\n    <style name=\"Widget.FloatingActionButton\" parent=\"Widget.MaterialComponents.FloatingActionButton\">\n        <item name=\"android:outlineAmbientShadowColor\" tools:targetApi=\"28\">?attr/widgetColorThemePrimary</item>\n        <item name=\"android:outlineSpotShadowColor\" tools:targetApi=\"28\">?attr/widgetColorThemePrimary</item>\n        <item name=\"backgroundTint\">?attr/widgetColorThemePrimary</item>\n        <item name=\"tint\">#fff</item>\n    </style>\n\n    <style name=\"Widget.FloatingActionButton.Accent\" parent=\"Widget.MaterialComponents.FloatingActionButton\">\n        <item name=\"android:outlineAmbientShadowColor\" tools:targetApi=\"28\">?attr/widgetColorThemeAccent</item>\n        <item name=\"android:outlineSpotShadowColor\" tools:targetApi=\"28\">?attr/widgetColorThemeAccent</item>\n        <item name=\"backgroundTint\">?attr/widgetColorThemeAccent</item>\n        <item name=\"tint\">#fff</item>\n    </style>\n\n    <style name=\"Widget.FloatingActionButton.Accent.Mini\" parent=\"Widget.FloatingActionButton.Accent\">\n        <item name=\"fabSize\">mini</item>\n    </style>\n\n    <style name=\"FixedLineHeightEditText\" parent=\"Widget.AppCompat.EditText\">\n        <item name=\"android:useLocalePreferredLineHeightForMinimum\" tools:targetApi=\"35\">false</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/themes_override.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <!-- Add android:colorControlNormal -->\n    <style name=\"Base.ThemeOverlay.AppCompat.ActionBar\" tools:override=\"true\">\n        <item name=\"android:colorControlNormal\">?android:attr/textColorPrimary</item>\n        <item name=\"colorControlNormal\">?android:attr/textColorPrimary</item>\n        <item name=\"searchViewStyle\">@style/Widget.AppCompat.SearchView.ActionBar</item>\n    </style>\n\n    <style name=\"Base.ThemeOverlay.AppCompat.Dark.ActionBar\" tools:override=\"true\">\n        <item name=\"android:colorControlNormal\">?android:attr/textColorPrimary</item>\n        <item name=\"colorControlNormal\">?android:attr/textColorPrimary</item>\n        <item name=\"searchViewStyle\">@style/Widget.AppCompat.SearchView.ActionBar</item>\n    </style>\n\n    <style name=\"Widget.MaterialComponents.CompoundButton.CheckBox\" parent=\"Widget.AppCompat.CompoundButton.CheckBox\" tools:override=\"true\">\n        <item name=\"enforceMaterialTheme\">true</item>\n        <item name=\"useMaterialThemeColors\">true</item>\n    </style>\n\n    <style name=\"Widget.MaterialComponents.CompoundButton.RadioButton\" parent=\"Widget.AppCompat.CompoundButton.RadioButton\" tools:override=\"true\">\n        <item name=\"enforceMaterialTheme\">true</item>\n        <item name=\"useMaterialThemeColors\">true</item>\n    </style>\n\n    <style name=\"PreferenceSummaryTextStyle\" tools:override=\"true\">\n        <item name=\"android:fontFamily\">sans-serif</item>\n        <item name=\"android:textStyle\">normal</item>\n        <item name=\"android:textAllCaps\">false</item>\n        <item name=\"android:textSize\">14sp</item>\n        <item name=\"android:letterSpacing\">0.0178571429</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n    <!-- App Name -->\n    <!-- Login Scene -->\n    <string name=\"app_waring\">このアプリの内容はインターネットから取得したものです。その内容の一部は心身に悪影響を与える可能性があります。\\n使用を続行することで規約に同意したとみなされます。</string>\n    <string name=\"username\">ユーザー名</string>\n    <string name=\"password\">パスワード</string>\n    <string name=\"sign_in\">ログイン</string>\n    <string name=\"register\">登録</string>\n    <string name=\"sign_in_via_webview\">WebView でログイン</string>\n    <string name=\"sign_in_via_cookies\">Cookie でログイン</string>\n    <string name=\"tourist_mode\">ログインをせずに続行</string>\n    <string name=\"error_username_cannot_empty\">ユーザー名は空欄にできません</string>\n    <string name=\"error_password_cannot_empty\">パスワードは空欄にできません</string>\n    <string name=\"sign_in_failed\">ログインに失敗しました</string>\n    <string name=\"sign_in_failed_tip\">この問題が解決しない場合は「WebView でログイン」をお試しください。</string>\n    <string name=\"sign_in_failed_tip_2\">Cookie が正しいことを確認している場合は、エラーを無視して続行することもできますが、いくつかの問題が発生する可能性があります。</string>\n    <string name=\"ignore\">無視</string>\n    <string name=\"get_it\">了解</string>\n    <!-- Cookie Sign In Scene -->\n    <string name=\"cookie_explain\">Cookie とは、ウェブブラウザに保存される小さなデータのことです。Cookie が何なのか、または取得方法がわからない場合は Google で検索をしてください。\\n\\nログインをするには特別な Cookie を入力してください。</string>\n    <string name=\"from_clipboard\">クリップボードから</string>\n    <string name=\"text_is_empty\">テキストが空です</string>\n    <string name=\"from_clipboard_error\">クリップボードから Cookie が見つかりませんでした。</string>\n    <!-- Select Site Scene -->\n    <string name=\"select_scene\">どのギャラリーサイトにアクセスしますか？</string>\n    <string name=\"select_scene_explain\">E-Hentai: 誰でも利用可能\\nExHentai: ログインをしている場合に利用可能</string>\n    <!-- Guide -->\n    <string name=\"guide_gallery_left\">ページをめくる</string>\n    <string name=\"guide_gallery_right\">ページをめくる</string>\n    <string name=\"guide_gallery_menu\">メニュー</string>\n    <string name=\"guide_gallery_progress\">進捗</string>\n    <string name=\"guide_gallery_long_click\">長押しでページメニューを開きます</string>\n    <!-- Main Activity -->\n    <string name=\"metered_network_warning\">従量制ネットワークに接続中です</string>\n    <string name=\"waring\">警告</string>\n    <string name=\"invalid_download_location\">ダウンロードパスは現在利用できません。ダウンロードパスを設定してください。</string>\n    <string name=\"press_twice_exit\">再度押して終了します</string>\n    <string name=\"clipboard_gallery_url_snack_message\">クリップボードにギャラリーの URL があります</string>\n    <string name=\"clipboard_gallery_url_snack_action\">表示</string>\n    <string name=\"please_wait\">お待ちください…</string>\n    <string name=\"app_link_not_verified_title\">アプリのリンクが確認されていません</string>\n    <string name=\"app_link_not_verified_message\">Android 12 以降の場合、EhViewer で E-Hentai のリンクを開くには確認済みのリンクを手動で追加する必要があります。</string>\n    <string name=\"open_settings\">設定を開く</string>\n    <string name=\"dont_show_again\">今後表示しない</string>\n    <!-- Gallery Activity -->\n    <string name=\"archive_need_passwd\">アーカイブにはパスワードが必要です</string>\n    <string name=\"archive_passwd\">パスワード</string>\n    <string name=\"passwd_cannot_be_empty\">パスワードは空欄にできません</string>\n    <string name=\"passwd_wrong\">パスワードが違います</string>\n    <string name=\"page_menu_title\">ページ: %d</string>\n    <string name=\"page_menu_refresh\">@string/refresh</string>\n    <string name=\"page_menu_share\">@string/share</string>\n    <string name=\"page_menu_save\">保存</string>\n    <string name=\"page_menu_save_to\">名前を付けて保存…</string>\n    <string name=\"page_menu_download_original\">オリジナルをダウンロード</string>\n    <string name=\"gallery_menu_title\">メニュー</string>\n    <string name=\"share_image\">画像を共有</string>\n    <string name=\"start_download_original\">ダウンロード中、お待ちください…</string>\n    <string name=\"image_saved\">画像は「%s」に保存されました</string>\n    <!-- Navigation Bar -->\n    <string name=\"homepage\">ホーム</string>\n    <string name=\"subscription\">購読</string>\n    <string name=\"whats_hot\">人気</string>\n    <string name=\"toplist\">トップリスト</string>\n    <string name=\"favourite\">お気に入り</string>\n    <string name=\"history\">履歴</string>\n    <string name=\"downloads\">ダウンロード</string>\n    <string name=\"settings\">設定</string>\n    <!-- Gallery List Scene -->\n    <string name=\"search\">検索</string>\n    <string name=\"gallery_list_search_bar_hint_exhentai\">ExHentai を検索</string>\n    <string name=\"gallery_list_search_bar_hint_e_hentai\">E-Hentai を検索</string>\n    <string name=\"gallery_list_search_bar_open_gallery\">ギャラリーを開く</string>\n    <string name=\"toplist_yesterday\">昨日</string>\n    <string name=\"toplist_pastmonth\">先月</string>\n    <string name=\"toplist_pastyear\">昨年</string>\n    <string name=\"toplist_alltime\">すべての期間</string>\n    <string name=\"gallery_list_empty_hit\">何も見つかりません</string>\n    <string name=\"gallery_list_empty_hit_subscription\">リストは空です。\\n設定 -&gt; EH -&gt; マイタグでタグを購読できます。</string>\n    <!-- Gallery List Action -->\n    <string name=\"read\">読む</string>\n    <string name=\"download\">ダウンロード</string>\n    <string name=\"delete_downloads\">ダウンロードを削除</string>\n    <string name=\"add_to_favourites\">お気に入りに追加</string>\n    <string name=\"remove_from_favourites\">お気に入りから削除</string>\n    <string name=\"download_move_dialog_title\">移動</string>\n    <string name=\"default_download_label_name\">デフォルト</string>\n    <string name=\"remember_download_label\">ダウンロードラベルを記憶</string>\n    <string name=\"added_to_download_list\">ダウンロードリストに追加しました</string>\n    <string name=\"download_remove_dialog_title\">ダウンロードを削除</string>\n    <string name=\"download_remove_dialog_message\">ダウンロードリストから「%s」を削除しますか？</string>\n    <string name=\"download_remove_dialog_check_text\">画像ファイルを削除</string>\n    <string name=\"add_favorites_dialog_title\">お気に入りに追加</string>\n    <string name=\"local_favorites\">ローカルのお気に入り</string>\n    <string name=\"remember_favorite_collection\">お気に入りのコレクションを記憶する</string>\n    <string name=\"add_favorite_note_dialog_title\">お気に入りのメモを追加</string>\n    <string name=\"favorite_note\">お気に入りのメモ</string>\n    <string name=\"favorite_note_never_show\">次回から表示しない</string>\n    <string name=\"add_to_favorite_success\">お気に入りに追加しました</string>\n    <string name=\"add_to_favorite_failure\">お気に入りの追加に失敗しました</string>\n    <string name=\"remove_from_favorite_success\">お気に入りから削除しました</string>\n    <string name=\"remove_from_favorite_failure\">お気に入りからの削除に失敗しました</string>\n    <!-- Gallery List Fab Action -->\n    <string name=\"go_to\">ページに移動</string>\n    <string name=\"go_to_gid\">GID、最後のページは空白にします</string>\n    <string name=\"go_to_hint\">ページ: %1$d - 合計: %2$d ページ</string>\n    <!-- Gallery List Quick Search -->\n    <string name=\"quick_search\">クイック検索</string>\n    <string name=\"quick_search_tip\">「+」をタップしてクイック検索を追加します</string>\n    <string name=\"readme\">README</string>\n    <string name=\"add_quick_search_tip\">ギャラリー検索結果の設定などはクイック検索として保存されます。検索の設定などを保存するには、まず検索を実行してください。</string>\n    <string name=\"add_quick_search_dialog_title\">クイック検索を追加</string>\n    <string name=\"save_progress\">進捗を保存</string>\n    <string name=\"name_is_empty\">タイトルが空欄です</string>\n    <string name=\"duplicate_name\">この名前は既に使用されています。</string>\n    <string name=\"image_search_not_quick_search\">画像検索をクイック検索として追加できませんでした</string>\n    <string name=\"duplicate_quick_search\">「%s」のクイック検索はすでに存在しています。</string>\n    <string name=\"delete\">削除</string>\n    <string name=\"delete_quick_search_title\">クイック検索を削除</string>\n    <string name=\"delete_quick_search_message\">「%s」を削除しますか？</string>\n    <!-- Gallery Search -->\n    <string name=\"search_normal\">通常検索</string>\n    <string name=\"search_normal_search\">通常検索</string>\n    <string name=\"search_subscription_search\">購読の検索</string>\n    <string name=\"search_specify_uploader\">アップローダーを指定する</string>\n    <string name=\"search_specify_tag\">タグを指定する</string>\n    <string name=\"search_tip\">通常検索: 通常通りに検索します。\\n\\n購読を検索: 購読内を検索します。\\n\\nアップローダーを指定: 指定したアップローダーによってアップロードされたギャラリーを検索します(その他のオプションは無視されます)。\\n\\nタグを指定: 指定したタグを含むギャラリーを検索します(受け入れられるタグは 1 つのみでその他のオプションは無視されます)。</string>\n    <string name=\"search_enable_advance\">高度なオプションを有効化する</string>\n    <string name=\"search_advance\">高度なオプション</string>\n    <string name=\"search_sh\">削除済みのギャラリーを表示</string>\n    <string name=\"search_sto\">Torrent のあるギャラリーのみを表示</string>\n    <string name=\"search_sr\">評価の下限</string>\n    <string name=\"star_2\">2 つ星</string>\n    <string name=\"star_3\">3 つ星</string>\n    <string name=\"star_4\">4 つ星</string>\n    <string name=\"star_5\">5 つ星</string>\n    <string name=\"search_sp\">ページ:</string>\n    <string name=\"search_sp_to\">から</string>\n    <!-- A placeholder for other languages -->\n    <string name=\"search_sp_suffix\"></string>\n    <string name=\"search_sp_err0\">ページ範囲の最大値を 1000 以下にすることはできません</string>\n    <string name=\"search_sp_err1\">ページ範囲の最大値を 10 以下にすることはできません</string>\n    <string name=\"search_sp_err2\">ページ範囲が狭すぎます</string>\n    <string name=\"search_sp_err3\">ページ範囲の比率を 0.5 以上にすることはできません</string>\n    <string name=\"search_sf\">デフォルトのフィルターで無効:</string>\n    <string name=\"search_sfl\">言語</string>\n    <string name=\"search_sfu\">アップローダー</string>\n    <string name=\"search_sft\">タグ</string>\n    <string name=\"search_image\">画像検索</string>\n    <string name=\"select_image\">画像を選択</string>\n    <string name=\"select_image_first\">画像を選択してください</string>\n    <string name=\"keyword_search\">キーワードで検索</string>\n    <string name=\"image_search\">画像で検索</string>\n    <!-- Category -->\n    <!-- Gallery Detail Scene -->\n    <string name=\"download_upgradeable\">アップグレードができます</string>\n    <string name=\"download_upgrade_existed\">ギャラリーはすでに存在します。削除をして再度やり直してください。</string>\n    <string name=\"download_upgrade_service_failed\">システムの制限により、ダウンロードサービスをバックグラウンドから開始できません。開始をするには「OK」をタップしてください。</string>\n    <string name=\"read_from\">%d ページを読む</string>\n    <plurals name=\"page_count\">\n        <item quantity=\"other\">%d ページ</item>\n    </plurals>\n    <string name=\"favored_times\">\\u2665 %d</string>\n    <string name=\"more_information\">その他の情報</string>\n    <string name=\"newer_version_avaliable\">このギャラリーの新しいバージョンが利用可能です</string>\n    <string name=\"newer_version_title\">%1$s、%2$s を追加しました</string>\n    <string name=\"favorited\">お気に入りに追加済み</string>\n    <string name=\"not_favorited\">お気に入り未追加</string>\n    <string name=\"share\">共有</string>\n    <string name=\"torrent_count\">Torrent (%d)</string>\n    <string name=\"archive\">アーカイブ</string>\n    <string name=\"similar_gallery\">類似のギャラリー</string>\n    <string name=\"no_tags\">タグなし</string>\n    <string name=\"no_comments\">コメントなし</string>\n    <string name=\"more_comment\">他のコメントを見る</string>\n    <string name=\"no_more_comments\">コメントはこれ以上ありません</string>\n    <string name=\"no_previews\">プレビューなし</string>\n    <string name=\"more_previews\">他のプレビューを見る</string>\n    <string name=\"no_more_previews\">プレビューはこれ以上ありません</string>\n    <!-- Gallery Actions -->\n    <string name=\"refresh\">更新</string>\n    <string name=\"action_add_tag\">タグを追加</string>\n    <string name=\"action_add_tag_tip\">新しいタグをコンマで区切って入力してください</string>\n    <string name=\"action_clear_image_cache\">画像キャッシュを消去</string>\n    <string name=\"action_image_cache_cleared\">画像キャッシュを消去しました</string>\n    <string name=\"open_in_other_app\">他のアプリで開く</string>\n    <string name=\"filter_the_uploader\">アップローダーの「%s」をブロックしますか？</string>\n    <string name=\"filter_added\">ブロックを追加しました</string>\n    <string name=\"copy_trans\">翻訳をコピー</string>\n    <string name=\"show_definition\">ヘルプを表示</string>\n    <string name=\"filter_the_tag\">「%s」のタグをブロックしますか？</string>\n    <string name=\"tag_vote_up\">評価を上げる</string>\n    <string name=\"tag_vote_down\">評価を下げる</string>\n    <string name=\"tag_vote_up_cancel\">高評価を取り消す</string>\n    <string name=\"tag_vote_down_cancel\">低評価を取り消す</string>\n    <string name=\"tag_vote_successfully\">評価が成功しました</string>\n    <string name=\"vote_failed\">評価に失敗しました</string>\n    <!-- Gallery Info -->\n    <string name=\"gallery_info\">ギャラリー情報</string>\n    <string name=\"header_key\">キー</string>\n    <string name=\"header_value\">値</string>\n    <string name=\"key_url\">URL</string>\n    <string name=\"key_title\">タイトル</string>\n    <string name=\"key_title_jpn\">日本語のタイトル</string>\n    <string name=\"key_thumb\">サムネイル</string>\n    <string name=\"key_category\">カテゴリー</string>\n    <string name=\"key_uploader\">アップローダー</string>\n    <string name=\"key_posted\">アップロード日時</string>\n    <string name=\"key_parent\">親ギャラリー</string>\n    <string name=\"key_visible\">可視性</string>\n    <string name=\"key_language\">言語</string>\n    <string name=\"key_pages\">ページ数</string>\n    <string name=\"key_size\">サイズ</string>\n    <string name=\"key_favorite_count\">お気に入りの数</string>\n    <string name=\"key_favorited\">お気に入りに追加済み</string>\n    <string name=\"key_favorite_name\">お気に入り</string>\n    <string name=\"key_rating_count\">評価の数</string>\n    <string name=\"key_rating\">評価</string>\n    <string name=\"key_torrents\">Torrent</string>\n    <string name=\"key_torrent_url\">Torrent URL</string>\n    <string name=\"copied_to_clipboard\">クリップボードにコピーしました</string>\n    <!-- Gallery Torrents -->\n    <string name=\"torrents\">Torrent</string>\n    <string name=\"no_torrents\">Torrent なし</string>\n    <string name=\"download_torrent_started\">Torrent のダウンロードを開始しました</string>\n    <string name=\"download_torrent_failure\">Torrent でのダウンロードに失敗しました</string>\n    <!-- Gallery Archives -->\n    <string name=\"no_archives\">アーカイブなし</string>\n    <string name=\"archive_original\">オリジナル</string>\n    <string name=\"archive_resample\">リサンプル</string>\n    <string name=\"archive_free\">自由</string>\n    <string name=\"current_funds\">資金: %1$s GP、%2$d Credits</string>\n    <string name=\"insufficient_funds\">GP が不足しています</string>\n    <string name=\"download_archive_started\">アーカイブのダウンロードを開始しました</string>\n    <string name=\"download_archive_failure\">アーカイブをダウンロードできませんでした</string>\n    <string name=\"download_archive_failure_no_hath\">アーカイブのダウンロードは H@H クライアントが必要です</string>\n    <!-- Gallery Rating -->\n    <string name=\"rate\">評価</string>\n    <string name=\"rate_successfully\">評価しました</string>\n    <string name=\"rate_failed\">評価できませんでした</string>\n    <string name=\"rating10\">ものすごくいい</string>\n    <string name=\"rating9\">すごくいい</string>\n    <string name=\"rating8\">とてもいい</string>\n    <string name=\"rating7\">いい</string>\n    <string name=\"rating6\">まあまあ</string>\n    <string name=\"rating5\">普通</string>\n    <string name=\"rating4\">悪い</string>\n    <string name=\"rating3\">とても悪い</string>\n    <string name=\"rating2\">目の障害になる</string>\n    <string name=\"rating1\">悪くて呼吸できない</string>\n    <string name=\"rating0\">…</string>\n    <!-- Gallery Comments -->\n    <string name=\"gallery_comments\">ギャラリーのコメント</string>\n    <string name=\"no_one_comments_gallery\">コメントはありません。</string>\n    <string name=\"comment_successfully\">コメントを送信しました</string>\n    <string name=\"comment_failed\">コメントに失敗しました</string>\n    <string name=\"comment_user_uploader\">%s (アップローダー)</string>\n    <string name=\"last_edited\">最終更新: %s</string>\n    <string name=\"click_more_comments\">タップしてさらにコメントを読み込む</string>\n    <string name=\"copy_comment_text\">コメントをコピー</string>\n    <string name=\"edit_comment\">コメントを編集</string>\n    <string name=\"edit_comment_successfully\">コメントが編集されました</string>\n    <string name=\"edit_comment_failed\">コメントの編集に失敗しました</string>\n    <string name=\"block_commenter\">コメントをしたユーザーをブロック</string>\n    <string name=\"filter_the_commenter\">「%s」をブロックしますか？</string>\n    <string name=\"vote_up\">高評価</string>\n    <string name=\"cancel_vote_up\">高評価を取り消す</string>\n    <string name=\"vote_down\">低評価</string>\n    <string name=\"cancel_vote_down\">低評価を取り消す</string>\n    <string name=\"vote_up_successfully\">高評価しました</string>\n    <string name=\"cancel_vote_up_successfully\">高評価を取り消しました</string>\n    <string name=\"vote_down_successfully\">低評価しました</string>\n    <string name=\"cancel_vote_down_successfully\">低評価を取り消しました</string>\n    <string name=\"check_vote_status\">評価の詳細を表示</string>\n    <string name=\"format_bold\"><b>太字</b></string>\n    <string name=\"format_italic\"><i>斜体</i></string>\n    <string name=\"format_strikethrough\"><strike>打ち消し線</strike></string>\n    <string name=\"format_underline\"><u>下線</u></string>\n    <string name=\"format_url\">URL</string>\n    <string name=\"format_plain\">プレーンテキスト</string>\n    <!-- Gallery Previews -->\n    <string name=\"gallery_previews\">ギャラリーのプレビュー</string>\n    <!-- Favorites Scene -->\n    <string name=\"collections\">コレクション</string>\n    <string name=\"favorites_search_bar_hint\">%s を検索</string>\n    <string name=\"favorites_title\">%s</string>\n    <string name=\"favorites_title_2\">%1$s - %2$s</string>\n    <string name=\"need_sign_in\">ログインが必要です</string>\n    <!-- Favorites Collection -->\n    <string name=\"default_favorites_collection\">デフォルトのお気に入りリスト</string>\n    <string name=\"cloud_favorites\">クラウドのお気に入り</string>\n    <string name=\"let_me_select\">手動で選択</string>\n    <!-- Favorites Fab Action -->\n    <string name=\"delete_favorites_dialog_title\">お気に入りから削除</string>\n    <string name=\"delete_favorites_dialog_message\">%d 件の項目をお気に入りリストから削除しますか？</string>\n    <string name=\"move_favorites_dialog_title\">お気に入りを移動</string>\n    <!-- History Scene -->\n    <string name=\"no_history\">閲覧したギャラリーはここに表示されます</string>\n    <string name=\"clear_all\">すべて消去</string>\n    <string name=\"clear_all_history\">履歴をすべて消去しますか？</string>\n    <!-- Download Scene -->\n    <string name=\"scene_download_title\">ダウンロード - %s</string>\n    <string name=\"no_download_info\">ダウンロードタスクはここに表示されます</string>\n    <string name=\"download_state_none\">一時停止中</string>\n    <string name=\"download_state_wait\">待機中</string>\n    <string name=\"download_state_downloading\">ダウンロード中</string>\n    <string name=\"download_state_downloaded\">ダウンロード済み</string>\n    <string name=\"download_state_failed\">ダウンロードエラー</string>\n    <string name=\"download_state_failed_2\">%d 件が未完成</string>\n    <string name=\"download_state_finish\">ダウンロード完了</string>\n    <!-- Download Action Bar -->\n    <string name=\"download_filter\">フィルター</string>\n    <string name=\"download_filter_title\">フィルターのタイトル</string>\n    <string name=\"download_start_all\">すべて開始</string>\n    <string name=\"download_stop_all\">すべて停止</string>\n    <string name=\"download_start_all_reversed\">すべて開始 (逆順)</string>\n    <string name=\"download_sort_by\">並べ替え</string>\n    <string name=\"download_sort_added_time_desc\">追加された時間 (降順)</string>\n    <string name=\"download_sort_added_time_asc\">追加された時間 (昇順)</string>\n    <string name=\"download_sort_title_asc\">ギャラリーのタイトル (昇順)</string>\n    <string name=\"download_sort_title_desc\">ギャラリーのタイトル (降順)</string>\n    <string name=\"download_sort_author_asc\">ギャラリーの作者 (昇順)</string>\n    <string name=\"download_sort_author_desc\">ギャラリーの作者 (降順)</string>\n    <string name=\"download_sort_name_asc\">ギャラリーの名前 (昇順)</string>\n    <string name=\"download_sort_name_desc\">ギャラリーの名前 (降順)</string>\n    <string name=\"download_sort_category_asc\">ギャラリーのカテゴリー (昇順)</string>\n    <string name=\"download_sort_category_desc\">ギャラリーのカテゴリー (降順)</string>\n    <string name=\"download_sort_shuffle\">ランダム</string>\n    <string name=\"download_reset_reading_progress\">閲覧進捗をリセット</string>\n    <string name=\"reset_reading_progress_message\">すべてのダウンロード済みのギャラリーの閲覧進捗をリセットしますか？</string>\n    <!-- Download Fab Action -->\n    <string name=\"download_remove_dialog_message_2\">ダウンロードリストから %d 件のタスクを削除しますか？</string>\n    <!-- Download Labels -->\n    <string name=\"download_labels\">ダウンロードラベル</string>\n    <string name=\"download_all\">すべて</string>\n    <string name=\"add\">追加</string>\n    <string name=\"new_label_title\">ラベルを作成</string>\n    <string name=\"label_text_is_empty\">ラベルのテキストが空です</string>\n    <string name=\"label_text_is_invalid\">「すべて」または「デフォルト」は無効なラベルです</string>\n    <string name=\"label_text_exist\">ラベルはすでに存在しています</string>\n    <string name=\"rename_label\">名前を変更</string>\n    <string name=\"rename_label_title\">ラベルの名前を変更</string>\n    <string name=\"delete_label_title\">ラベルを削除</string>\n    <string name=\"delete_label_message\">「%s」を削除しますか？</string>\n    <string name=\"default_download_label\">デフォルトのダウンロードラベル</string>\n    <!-- Download Service -->\n    <string name=\"download_service\">ダウンロードサービス</string>\n    <string name=\"download_service_label\">EhViewer ダウンロードサービス</string>\n    <string name=\"download_speed_text_2\">%1$s - 残り: %2$s</string>\n    <string name=\"stat_download_action_stop_all\">すべて停止</string>\n    <string name=\"stat_509_alert_title\">509 アラート</string>\n    <string name=\"stat_509_alert_text\">ダウンロード制限に達しました。しばらくしてからもう一度やり直してください。</string>\n    <string name=\"stat_download_done_title\">ダウンロード完了</string>\n    <string name=\"stat_download_done_text_succeeded\">%d 件が成功しました</string>\n    <string name=\"stat_download_done_text_failed\">%d 件が失敗しました</string>\n    <string name=\"stat_download_done_text_mix\">%1$d 件のダウンロードが成功、%2$d 件が失敗しました</string>\n    <string name=\"stat_download_done_line_succeeded\">成功: %s</string>\n    <string name=\"stat_download_done_line_failed\">失敗: %s</string>\n    <!-- Settings -->\n    <string name=\"settings_eh\">EH</string>\n    <string name=\"settings_eh_account_name\">アカウント</string>\n    <string name=\"settings_eh_account_name_tourist\">ログインしていません。</string>\n    <string name=\"settings_eh_account_refresh_igneous\">Igneous を更新</string>\n    <string name=\"settings_eh_account_sign_out\">ログアウト</string>\n    <string name=\"settings_eh_account_sign_out_tip\">ログアウトしました。しばらくしてから再度ログインできます。</string>\n    <string name=\"settings_eh_account_identity_cookies\">このアカウントは Identity Cookie を使用してログインできます。\\nこれは安全に保管してください\\n\\n%s</string>\n    <string name=\"settings_eh_account_igneous_expire\">\"Igneous の自動更新: \"</string>\n    <string name=\"settings_eh_account_identity_cookies_copy\">コピー</string>\n    <string name=\"settings_eh_gallery_site\">ギャラリーサイト</string>\n    <string name=\"settings_eh_image_limits\">画像の制限</string>\n    <string name=\"settings_eh_image_limits_summary\">読み込み中…</string>\n    <string name=\"settings_eh_image_limits_summary_ip\">\"IP ベースの制限: \"</string>\n    <string name=\"settings_eh_image_limits_summary_ip_ok\">制限なし</string>\n    <string name=\"settings_eh_image_limits_summary_ip_restricted\">低解像度の画像</string>\n    <string name=\"settings_eh_image_limits_summary_acc\">現在: %1$d / %2$d</string>\n    <string name=\"settings_eh_unlock_cost\">%1$d GP を消費することで、24 時間の高解像度クォータのロックを解除できます。</string>\n    <string name=\"settings_eh_unlock\">ロックを解除</string>\n    <string name=\"settings_eh_reset_cost\">コストのリセット: %1$d GP</string>\n    <string name=\"settings_eh_reset\">リセット</string>\n    <string name=\"settings_eh_reset_limits_succeed\">画像の制限が正常にリセットされました</string>\n    <string name=\"settings_eh_u_config\">E-Hentai の設定</string>\n    <string name=\"settings_eh_u_config_summary\">E-Hentai のウェブサイトで設定します</string>\n    <string name=\"settings_eh_my_tags\">マイタグ</string>\n    <string name=\"settings_eh_my_tags_summary\">E-Hentai のウェブサイトでマイタグを管理します</string>\n    <string name=\"settings_eh_black_dark_theme\">ブラックダークテーマ</string>\n    <string name=\"settings_eh_launch_page\">起動ページ</string>\n    <string name=\"settings_eh_list_mode\">リストモード</string>\n    <string name=\"settings_eh_list_mode_detail\">詳細</string>\n    <string name=\"settings_eh_list_mode_thumb\">サムネイル</string>\n    <string name=\"settings_eh_detail_size\">詳細情報の幅</string>\n    <string name=\"settings_eh_list_tile_thumb_size\">詳細モードのサムネイルのサイズ</string>\n    <string name=\"settings_eh_thumb_size\">サムネイルモードのサムネイルのサイズ</string>\n    <string name=\"settings_eh_thumb_show_title\">タイトル表示のサムネイルモード</string>\n    <string name=\"settings_eh_show_jpn_title\">日本語のタイトルを表示</string>\n    <string name=\"settings_eh_show_jpn_title_summary\">E-Hentai ウェブサイトの設定で日本語のタイトルを有効化する必要があります</string>\n    <string name=\"settings_eh_show_gallery_pages\">ギャラリーページ数を表示</string>\n    <string name=\"settings_eh_show_gallery_pages_summary\">リストにギャラリーのページ数を表示します</string>\n    <string name=\"settings_eh_show_gallery_comments\">ギャラリーのコメントを表示</string>\n    <string name=\"settings_eh_show_gallery_comments_summary\">ギャラリーの詳細ページにコメントを表示します</string>\n    <string name=\"settings_eh_comment_threshold\">コメントスコアのしきい値</string>\n    <string name=\"settings_eh_comment_threshold_summary\">このスコア以下のコメントを非表示にします (-101 は無効)</string>\n    <string name=\"settings_eh_preview_num\">ギャラリー詳細ページのプレビューの最大数</string>\n    <string name=\"settings_eh_preview_size\">ギャラリー詳細ページ内のプレビュー画像サイズ</string>\n    <string name=\"settings_eh_show_tag_translations\">タグの翻訳を表示</string>\n    <string name=\"settings_eh_show_tag_translations_summary\">元のテキストの代わりに翻訳したタグを表示します (データファイルのダウンロードに時間がかかります)</string>\n    <string name=\"settings_eh_tag_translations_source\">プレースホルダー</string>\n    <string name=\"settings_eh_tag_translations_source_url\">https://placeholder</string>\n    <string name=\"settings_eh_filter\">ブロッカー</string>\n    <string name=\"settings_eh_filter_summary\">タイトル、アップローダー、タグ、コメントの投稿者またはギャラリーをブロックします</string>\n    <string name=\"settings_eh_metered_network_warning\">従量制ネットワークの警告</string>\n    <string name=\"settings_eh_request_news\">時限リクエストのニュースページ</string>\n    <string name=\"settings_eh_hide_hv_events\">HV イベント通知を隠す</string>\n    <string name=\"settings_read\">読む</string>\n    <string name=\"settings_read_screen_rotation\">画面の方向</string>\n    <string name=\"settings_read_screen_rotation_default\">デフォルト</string>\n    <string name=\"settings_read_screen_rotation_portrait\">縦方向</string>\n    <string name=\"settings_read_screen_rotation_landscape\">横方向</string>\n    <string name=\"settings_read_screen_rotation_sensor\">自動回転</string>\n    <string name=\"settings_read_reading_direction\">読む方向</string>\n    <string name=\"settings_read_reading_direction_left_to_right\">左から右</string>\n    <string name=\"settings_read_reading_direction_right_to_Left\">右から左</string>\n    <string name=\"settings_read_reading_direction_top_to_bottom\">上から下</string>\n    <string name=\"settings_read_page_scaling\">ページの拡大</string>\n    <string name=\"settings_read_page_scaling_actual_size\">実際のサイズ</string>\n    <string name=\"settings_read_page_scaling_fit_to_width\">幅に合わせる</string>\n    <string name=\"settings_read_page_scaling_fit_to_height\">高さに合わせる</string>\n    <string name=\"settings_read_page_scaling_fit_to_screen\">画面に合わせる</string>\n    <string name=\"settings_read_page_scaling_fixed_scale\">固定スケール</string>\n    <string name=\"settings_read_start_position\">開始の位置</string>\n    <string name=\"settings_read_start_position_top_left\">左上</string>\n    <string name=\"settings_read_start_position_top_right\">右上</string>\n    <string name=\"settings_read_start_position_bottom_left\">左下</string>\n    <string name=\"settings_read_start_position_bottom_right\">右下</string>\n    <string name=\"settings_read_start_position_center\">中央</string>\n    <string name=\"settings_read_theme\">カラーテーマ</string>\n    <string name=\"settings_read_theme_follow_app\">アプリに従う</string>\n    <string name=\"settings_read_theme_dark\">ダーク</string>\n    <string name=\"settings_read_theme_light\">ライト</string>\n    <string name=\"settings_read_keep_screen_on\">常に画面を ON にする</string>\n    <string name=\"settings_read_show_clock\">時計を表示する</string>\n    <string name=\"settings_read_show_progress\">進捗を表示する</string>\n    <string name=\"settings_read_show_battery\">バッテリーを表示する</string>\n    <string name=\"settings_read_show_page_interval\">ページ間隔を表示する</string>\n    <string name=\"settings_read_turn_page_interval\">ページ遷移の間隔 (秒単位)</string>\n    <string name=\"settings_read_volume_page\">音量ボタンでページをめくる</string>\n    <string name=\"settings_read_volume_page_interval\">音量ボタンでのページ送りの間隔</string>\n    <string name=\"settings_read_reverse_volume\">音量ボタンを入れ替える</string>\n    <string name=\"settings_read_reading_fullscreen\">全画面</string>\n    <string name=\"settings_read_custom_screen_lightness\">画面の明るさをカスタマイズ</string>\n    <string name=\"settings_read_screen_lightness\">画面の明るさ</string>\n    <string name=\"settings_download\">ダウンロード</string>\n    <string name=\"settings_download_download_location\">ダウンロード先</string>\n    <string name=\"settings_download_pick_new_location\">新しい場所を選択</string>\n    <string name=\"settings_download_reset_location\">デフォルトにリセット</string>\n    <string name=\"settings_download_invalid_download_location\">ダウンロード先が無効です</string>\n    <string name=\"settings_download_cant_get_download_location\">ダウンロード先を取得できません</string>\n    <string name=\"settings_download_media_scan\">メディアスキャンを許可する</string>\n    <string name=\"settings_download_media_scan_summary_on\">ギャラリーアプリで他の人に見せないようにします</string>\n    <string name=\"settings_download_media_scan_summary_off\">ほとんどのギャラリーアプリでダウンロード先のパスを無視します</string>\n    <string name=\"settings_download_concurrency\">ダウンロードのスレッド数</string>\n    <string name=\"settings_download_concurrency_summary\">同時に最大 %s 枚の画像をダウンロードします</string>\n    <string name=\"settings_download_download_delay\">ダウンロードの遅延</string>\n    <string name=\"settings_download_download_delay_summary\">ダウンロードで %s ミリ秒の遅延をさせます</string>\n    <string name=\"settings_download_download_timeout\">ダウンロードのタイムアウト (秒単位)</string>\n    <string name=\"settings_download_preload_image\">画像をプリロード</string>\n    <string name=\"settings_download_preload_image_summary\">%s 枚の画像をプリロードします</string>\n    <string name=\"settings_download_download_origin_image\">オリジナルの画像をダウンロードする</string>\n    <string name=\"settings_download_download_origin_image_summary\">%s、注意！GP が必要になる可能性があります</string>\n    <string name=\"settings_download_download_origin_image_never\">しない</string>\n    <string name=\"settings_download_download_origin_image_force\">常にする</string>\n    <string name=\"settings_download_download_origin_image_only\">ダウンロードのみ</string>\n    <string name=\"settings_download_task_confirm\">このアクションを実行しても良いですか？</string>\n    <string name=\"settings_download_restore_download_items\">ダウンロードタスクを復元</string>\n    <string name=\"settings_download_restore_download_items_summary\">ダウンロードディレクトリのダウンロードタスクを復元します</string>\n    <string name=\"settings_download_restore_not_found\">復元可能なダウンロードが見つかりません</string>\n    <string name=\"settings_download_restore_failed\">復元できませんでした</string>\n    <string name=\"settings_download_restore_successfully\">%d 件のタスクが復元されました</string>\n    <string name=\"settings_download_clean_redundancy\">ダウンロードフォルダの不要なファイルを整理</string>\n    <string name=\"settings_download_clean_redundancy_summary\">ダウンロードディレクトリからダウンロードタスクにない画像ファイルを削除します</string>\n    <string name=\"settings_download_clean_redundancy_no_redundancy\">不要なファイルが見つかりませんでした</string>\n    <string name=\"settings_download_clean_redundancy_done\">%d 件のファイルを削除しました</string>\n    <string name=\"settings_privacy\">プライバシー</string>\n    <string name=\"settings_privacy_pattern_protection_title\">パターン保護</string>\n    <string name=\"settings_privacy_pattern_protection_not_set\">パターン保護が設定されていません</string>\n    <string name=\"settings_privacy_pattern_protection_set\">パターン保護が設定されています</string>\n    <string name=\"settings_privacy_secure\">スクリーンショットを抑制する</string>\n    <string name=\"settings_privacy_secure_summary\">アプリのコンテンツがスクリーンショットで撮影されたり「最近使用したアプリ」のリストに表示されないようにします</string>\n    <string name=\"settings_privacy_clear_search_history\">デバイスの検索履歴を消去</string>\n    <string name=\"settings_privacy_clear_search_history_summary\">このデバイスから検索履歴を消去します</string>\n    <string name=\"settings_privacy_clear_search_history_cleared\">検索履歴を消去しました</string>\n    <string name=\"settings_advanced\">その他の設定</string>\n    <string name=\"settings_advanced_save_parse_error_body\">解析の失敗時に HTML ファイルを保存</string>\n    <string name=\"settings_advanced_save_parse_error_body_summary\">HTML ファイルに個人情報が含まれている場合があります</string>\n    <string name=\"settings_advanced_save_crash_log\">アプリのクラッシュ時にレポートを保存</string>\n    <string name=\"settings_advanced_save_crash_log_summary\">クラッシュレポートはバグの修正に役立ちます</string>\n    <string name=\"settings_advanced_dump_logcat\">Logcat をダンプ</string>\n    <string name=\"settings_advanced_dump_logcat_summary\">Logcat のログを内部ストレージに保存します</string>\n    <string name=\"settings_advanced_dump_logcat_failed\">Logcat のダンプに失敗しました</string>\n    <string name=\"settings_advanced_dump_logcat_to\">Logcat のログが「%s」にダンプされました</string>\n    <string name=\"settings_advanced_read_cache_size\">読書用キャッシュのサイズ</string>\n    <string name=\"settings_advanced_app_language_title\">アプリの言語</string>\n    <string name=\"settings_advanced_proxy\">プロキシ</string>\n    <string name=\"settings_advanced_backup_favorite\">お気に入りリストをバックアップ</string>\n    <string name=\"settings_advanced_backup_favorite_summary\">リモートのお気に入りリストをローカルにバックアップします</string>\n    <string name=\"settings_advanced_backup_favorite_start\">お気に入りリスト「%s」をバックアップ中です</string>\n    <string name=\"settings_advanced_backup_favorite_nothing\">バックアップするものがありません</string>\n    <string name=\"settings_advanced_backup_favorite_success\">お気に入りリストのバックアップに成功しました</string>\n    <string name=\"settings_advanced_backup_favorite_failed\">お気に入りリストのバックアップに失敗しました</string>\n    <string name=\"settings_advanced_export_data\">データをエクスポート</string>\n    <string name=\"settings_advanced_export_data_summary\">ダウンロードリストやクイック検索などのデータを内部ストレージに保存します</string>\n    <string name=\"settings_advanced_export_data_to\">データを「%s」にエクスポート</string>\n    <string name=\"settings_advanced_export_data_failed\">データをエクスポートできませんでした</string>\n    <string name=\"settings_advanced_import_data\">データをインポート</string>\n    <string name=\"settings_advanced_import_data_summary\">以前に保存したデータを読み込みます</string>\n    <string name=\"settings_advanced_import_data_successfully\">データをインポートしました</string>\n    <string name=\"settings_advanced_import_data_cant_read\">ファイルを読み取れませんでした</string>\n    <string name=\"settings_advanced_open_by_default\">デフォルトで開く</string>\n    <string name=\"settings_about\">このアプリについて</string>\n    <string name=\"settings_about_declaration_summary\">EhViewer は E-Hentai.org と一切関係はありません</string>\n    <string name=\"settings_about_author\">開発者</string>\n    <string name=\"settings_about_issues\">FAQ</string>\n    <string name=\"settings_about_latest_release\">最新のリリース</string>\n    <string name=\"settings_about_source\">ソースコード</string>\n    <string name=\"settings_about_version\">ビルドバージョン</string>\n    <string name=\"settings_about_build_time\">%s にビルドされました</string>\n    <!-- UConfig Activity -->\n    <string name=\"u_config\">E-Hentai の設定</string>\n    <string name=\"apply\">適用</string>\n    <string name=\"apply_tip\">右上隅のチェックマークをタップして設定を保存します</string>\n    <!-- My Tags Activity -->\n    <string name=\"my_tags\">マイタグ</string>\n    <!-- Filter Activity -->\n    <string name=\"filter\">ブロッカー</string>\n    <string name=\"tip\">ヒント</string>\n    <string name=\"filter_tip\">E-Hentai のギャラリーリストからブロックしたものを除外します。\\n\\nタイトルをブロック: ブロックしたテキストを含むギャラリーを除外します。\\n\\nアップローダーをブロック: 該当するアップローダーを除外します。\\n\\nタグをブロック: 該当するタグを含むギャラリーを除外します。ギャラリーリストの取得に時間がかかる可能性があります。\\n\\nタグ名前空間をブロック: 該当するタグ名前空間を含むギャラリーを除外します。ギャラリーリストの取得に時間がかかる可能性があります。\\n\\nコメント投稿者をブロック: 該当するコメント投稿者が投稿したコメントを除外します。\\n\\nコメントをブロック: 正規表現に一致するコメントを除外します。</string>\n    <string name=\"no_filter\">ブロッカーはここに表示されます</string>\n    <string name=\"add_filter\">ブロックを追加</string>\n    <string name=\"filter_title\">タイトル</string>\n    <string name=\"filter_uploader\">アップローダー</string>\n    <string name=\"filter_tag\">タグ</string>\n    <string name=\"filter_tag_namespace\">タグの名前空間</string>\n    <string name=\"filter_commenter\">コメント</string>\n    <string name=\"filter_comment\">コメントの正規表現</string>\n    <string name=\"filter_text\">テキストをブロック</string>\n    <string name=\"delete_filter\">「%s」のブロックを削除しますか？</string>\n    <!-- Set Security -->\n    <string name=\"set_pattern_protection\">パターン保護を設定</string>\n    <string name=\"set_pattern_protection_tip\">パターンを描いてパターン保護を設定します。\\nパターン保護を消去するには空白のままにします。</string>\n    <string name=\"enable_biometric\">生体認証のロック解除も許可する</string>\n    <string name=\"set\">設定</string>\n    <!-- Languages -->\n    <string name=\"app_language_system\">システム言語 (デフォルト)</string>\n    <!-- Proxy -->\n    <string name=\"proxy_direct\">直接接続</string>\n    <string name=\"proxy_system\">システムのプロキシ</string>\n    <string name=\"proxy_host_or_ip\">ホストまたは IP</string>\n    <string name=\"proxy_port\">ポート</string>\n    <string name=\"proxy_invalid_port\">無効なポート</string>\n    <!-- Errors -->\n    <string name=\"error_bad_status_code\">バッドステータスコード: %d</string>\n    <string name=\"error_timeout\">タイムアウト</string>\n    <string name=\"error_unknown_host\">不明なホスト</string>\n    <string name=\"error_redirection\">リダイレクトが多すぎます</string>\n    <string name=\"error_socket\">ネットワークエラー</string>\n    <string name=\"error_unknown\">不明なエラー</string>\n    <string name=\"error_cant_find_activity\">アクティビティが見つかりません。</string>\n    <string name=\"error_cannot_parse_the_url\">リンクを解析できません。</string>\n    <string name=\"error_decoding_failed\">デコードに失敗しました</string>\n    <string name=\"error_empty\">空欄</string>\n    <string name=\"error_reading_failed\">読み込めませんでした</string>\n    <string name=\"error_out_of_range\">範囲外</string>\n    <string name=\"error_write_failed\">書き込みに失敗しました</string>\n    <string name=\"error_parse_error\">ファイルの解析に失敗しました</string>\n    <string name=\"error_invalid_url\">無効なリンクです</string>\n    <string name=\"error_get_ptoken_error\">pToken 取得エラー</string>\n    <string name=\"error_cant_create_temp_file\">一時ファイルを作成できませんでした</string>\n    <string name=\"error_cant_save_image\">画像を保存できませんでした</string>\n    <string name=\"error_invalid_number\">無効な数字です</string>\n    <string name=\"error_please_login_first\">ログインをしてください</string>\n    <string name=\"error_cannot_find_gallery\">ギャラリーが見つかりませんでした</string>\n    <string name=\"error_something_wrong_happened\">エラーが発生しました</string>\n    <string name=\"kokomade_tip\">設定 -&gt; Eh -&gt; ギャラリーサイト -&gt; E-Hentai</string>\n    <string name=\"no_browser_installed\">ブラウザをインストールしてください。</string>\n    <string name=\"cloudflare_bypass_failed\">Cloudflare チャレンジのバイパスに失敗しました</string>\n    <string name=\"open_in_webview\">WebView で URL を開いてチャレンジを完了しますか？</string>\n    <string name=\"information_webview_outdated\">WebViewアプリを更新して互換性を向上させてください</string>\n    <!-- Readable Time -->\n    <string name=\"from_the_future\">未来から</string>\n    <string name=\"just_now\">たった今</string>\n    <plurals name=\"some_minutes_ago\">\n        <item quantity=\"other\">%d 分前</item>\n    </plurals>\n    <plurals name=\"some_hours_ago\">\n        <item quantity=\"other\">%d 時間前</item>\n    </plurals>\n    <string name=\"yesterday\">昨日</string>\n    <string name=\"some_days_ago\">%d 日前</string>\n    <plurals name=\"second\">\n        <item quantity=\"other\">秒</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">分</item>\n    </plurals>\n    <plurals name=\"hour\">\n        <item quantity=\"other\">時</item>\n    </plurals>\n    <plurals name=\"day\">\n        <item quantity=\"other\">日</item>\n    </plurals>\n    <plurals name=\"year\">\n        <item quantity=\"other\">年</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/color_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<resources>\n    <color name=\"ic_launcher_background\">#34353B</color>\n    <color name=\"ic_launcher_foreground\">#DDDDDD</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <color name=\"primary_drawable\">#ffffffff</color>\n    <color name=\"secondary_drawable\">#4dffffff</color>\n\n    <color name=\"content\">@color/grey_825</color>\n    <color name=\"content_activated\">@color/grey_600</color>\n    <color name=\"widget\">@color/grey_700</color>\n    <color name=\"progress\">@color/grey_600</color>\n    <color name=\"tag_group_background\">@color/grey_775</color>\n    <color name=\"tag_background\">@color/grey_775</color>\n    <color name=\"gallery_detail_button_background\">@color/grey_775</color>\n\n    <!-- divider -->\n    <color name=\"divider\">#20ffffff</color>\n\n    <!-- shadow -->\n\n    <!-- Gallery Activity -->\n    <color name=\"gallery_slider_background\">@color/grey_900</color>\n\n    <!-- Lock pattern view -->\n    <color name=\"lock_pattern_view_regular_color\">#8affffff</color>\n\n    <!--Shortcut background-->\n    <color name=\"colorBackground\">#303030</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n    <style name=\"AppTheme\" parent=\"Base.AppTheme\" />\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-sw600dp/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <dimen name=\"keyline_margin\">24dp</dimen>\n    <dimen name=\"single_max_width\">480dp</dimen>\n\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_list_interval\">4dp</dimen>\n    <!-- 24dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_list_margin_h\">20dp</dimen>\n    <!-- 24dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_grid_margin_h\">20dp</dimen>\n\n    <!-- 24dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_search_bar_margin_h\">20dp</dimen>\n\n    <!-- 8dp - 4dp, 4dp for card padding -->\n    <dimen name=\"search_layout_interval\">4dp</dimen>\n    <!-- 24dp - 4dp, 4dp for card padding -->\n    <dimen name=\"search_layout_margin_h\">20dp</dimen>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-sw720dp-land/dimens.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n\n    <!-- 80dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_list_margin_h\">76dp</dimen>\n    <!-- 80dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_grid_margin_h\">76dp</dimen>\n\n    <!-- 80dp - 4dp, 4dp for card padding -->\n    <dimen name=\"gallery_search_bar_margin_h\">76dp</dimen>\n\n    <!-- 80dp - 4dp, 4dp for card padding -->\n    <dimen name=\"search_layout_margin_h\">76dp</dimen>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v24/arrays.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string-array name=\"proxy_types\" tools:ignore=\"InconsistentArrays\">\n        <item>@string/proxy_direct</item>\n        <item>@string/proxy_system</item>\n        <item>HTTP</item>\n        <item>SOCKS</item>\n    </string-array>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/bools.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<resources>\n\n    <bool name=\"tag_translatable\">true</bool>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n    <!-- Login Scene -->\n    <string name=\"app_waring\">本应用中的内容均来自互联网，部分内容可能对您的生理及心理造成难以恢复的伤害。本应用作者不会对因使用本应用所造成的任何后果负责。\\n未成年人应在监护人指导下使用本应用。\\n继续使用即代表您同意上述条款。</string>\n    <string name=\"username\">用户名</string>\n    <string name=\"password\">密码</string>\n    <string name=\"sign_in\">登录</string>\n    <string name=\"register\">注册</string>\n    <string name=\"sign_in_via_webview\">通过网页登录</string>\n    <string name=\"sign_in_via_cookies\">通过 Cookie 登录</string>\n    <string name=\"tourist_mode\">游客模式</string>\n    <string name=\"error_username_cannot_empty\">用户名不可为空</string>\n    <string name=\"error_password_cannot_empty\">密码不可为空</string>\n    <string name=\"sign_in_failed\">登录失败</string>\n    <string name=\"sign_in_failed_tip\">若持续出错，请尝试“通过网页登录”。</string>\n    <string name=\"sign_in_failed_tip_2\">若您确认您的 Cookie 是正确的，也可以选择忽略错误并继续，但可能会导致一些问题。</string>\n    <string name=\"ignore\">忽略</string>\n    <string name=\"get_it\">知道了</string>\n    <!-- Cookie Sign In Scene -->\n    <string name=\"cookie_explain\">Cookie 是存储在浏览器里的一小块数据。如果您不清楚 Cookie 是什么或者怎么获取 Cookie，最好搜索一下。\\n\\n请输入特定的 Cookie 来登录。</string>\n    <string name=\"from_clipboard\">从剪贴板导入</string>\n    <string name=\"text_is_empty\">文本为空</string>\n    <string name=\"from_clipboard_error\">未在剪贴板中发现 Cookies。</string>\n\n    <!-- Select Site Scene -->\n    <string name=\"select_scene\">您希望访问哪个画廊站点？</string>\n    <string name=\"select_scene_explain\">E-Hentai：对任何人开放\\nExHentai：仅对登录用户开放</string>\n\n    <!-- Guide -->\n    <string name=\"guide_gallery_left\">翻页</string>\n    <string name=\"guide_gallery_right\">翻页</string>\n    <string name=\"guide_gallery_menu\">菜单</string>\n    <string name=\"guide_gallery_progress\">进度条</string>\n    <string name=\"guide_gallery_long_click\">长按打开页面菜单</string>\n\n    <!-- Main Activity -->\n    <string name=\"metered_network_warning\">正在使用流量计费网络</string>\n    <string name=\"waring\">警告</string>\n    <string name=\"invalid_download_location\">下载路径似乎不可用。请重新设置下载路径。</string>\n    <string name=\"press_twice_exit\">再按一次退出</string>\n    <string name=\"clipboard_gallery_url_snack_message\">剪切板里有画廊链接</string>\n    <string name=\"clipboard_gallery_url_snack_action\">查看</string>\n    <string name=\"please_wait\">请稍候…</string>\n    <string name=\"app_link_not_verified_title\">应用链接未验证</string>\n    <string name=\"app_link_not_verified_message\">对于 Android 12 及更高版本，您需要手动添加链接到已验证链接才能在 EhViewer 中打开 E-Hentai 链接。</string>\n    <string name=\"open_settings\">打开设置</string>\n    <string name=\"dont_show_again\">不再显示</string>\n\n    <!-- Gallery Activity -->\n    <string name=\"archive_need_passwd\">归档文件需要密码</string>\n    <string name=\"archive_passwd\">请输入密码</string>\n    <string name=\"passwd_cannot_be_empty\">密码不能为空</string>\n    <string name=\"passwd_wrong\">密码错误</string>\n    <string name=\"page_menu_title\">第 %d 页</string>\n    <string name=\"page_menu_refresh\">@string/refresh</string>\n    <string name=\"page_menu_share\">@string/share</string>\n    <string name=\"page_menu_save\">保存</string>\n    <string name=\"page_menu_save_to\">保存到…</string>\n    <string name=\"page_menu_download_original\">下载原图</string>\n    <string name=\"gallery_menu_title\">菜单</string>\n    <string name=\"share_image\">分享图片</string>\n    <string name=\"start_download_original\">开始下载原图，请稍候…</string>\n    <string name=\"image_saved\">图片已保存至 %s</string>\n\n    <!-- Navigation Bar -->\n    <string name=\"homepage\">主页</string>\n    <string name=\"subscription\">订阅</string>\n    <string name=\"whats_hot\">热门</string>\n    <string name=\"toplist\">排行</string>\n    <string name=\"favourite\">收藏</string>\n    <string name=\"history\">历史</string>\n    <string name=\"downloads\">下载</string>\n    <string name=\"settings\">设置</string>\n\n    <!-- Gallery List Scene -->\n    <string name=\"search\">搜索</string>\n    <string name=\"gallery_list_search_bar_hint_exhentai\">搜索 ExHentai</string>\n    <string name=\"gallery_list_search_bar_hint_e_hentai\">搜索 E-Hentai</string>\n    <string name=\"gallery_list_search_bar_open_gallery\">打开画廊</string>\n    <string name=\"toplist_yesterday\">过去一天</string>\n    <string name=\"toplist_pastmonth\">过去一月</string>\n    <string name=\"toplist_pastyear\">过去一年</string>\n    <string name=\"toplist_alltime\">有史以来</string>\n    <string name=\"gallery_list_empty_hit\">什么都没有找到</string>\n    <string name=\"gallery_list_empty_hit_subscription\">请至 设置-&gt;EH-&gt;我的标签 订阅标签</string>\n    <!-- Gallery List Action -->\n    <string name=\"read\">阅读</string>\n    <string name=\"download\">下载</string>\n    <string name=\"delete_downloads\">删除下载</string>\n    <string name=\"add_to_favourites\">收藏</string>\n    <string name=\"remove_from_favourites\">移除收藏</string>\n    <string name=\"download_move_dialog_title\">移动</string>\n    <string name=\"default_download_label_name\">默认</string>\n    <string name=\"remember_download_label\">记住下载标签</string>\n    <string name=\"added_to_download_list\">已添加至下载列表</string>\n    <string name=\"download_remove_dialog_title\">移除下载项</string>\n    <string name=\"download_remove_dialog_message\">从下载列表移除 %s ？</string>\n    <string name=\"download_remove_dialog_check_text\">同时删除图片文件</string>\n    <string name=\"add_favorites_dialog_title\">添加收藏</string>\n    <string name=\"local_favorites\">本地收藏</string>\n    <string name=\"remember_favorite_collection\">记住收藏夹</string>\n    <string name=\"add_favorite_note_dialog_title\">添加收藏备注</string>\n    <string name=\"favorite_note\">收藏备注</string>\n    <string name=\"favorite_note_never_show\">不再显示此窗口</string>\n    <string name=\"add_to_favorite_success\">已添加至收藏</string>\n    <string name=\"add_to_favorite_failure\">添加收藏失败</string>\n    <string name=\"remove_from_favorite_success\">移除收藏成功</string>\n    <string name=\"remove_from_favorite_failure\">移除收藏失败</string>\n    <!-- Gallery List Fab Action -->\n    <string name=\"go_to\">跳页</string>\n    <string name=\"go_to_gid\">GID，留空跳转末页</string>\n    <string name=\"go_to_hint\">第 %1$d 页，共 %2$d 页</string>\n    <!-- Gallery List Quick Search -->\n    <string name=\"quick_search\">快速搜索</string>\n    <string name=\"quick_search_tip\">点击“+”来添加快速搜索</string>\n    <string name=\"readme\">读我</string>\n    <string name=\"add_quick_search_tip\">画廊列表的状态将被保存为快速搜索。如果您希望保存搜索面板的状态，请先执行搜索。</string>\n    <string name=\"add_quick_search_dialog_title\">添加快速搜索</string>\n    <string name=\"save_progress\">同时保存阅读进度</string>\n    <string name=\"name_is_empty\">名称为空</string>\n    <string name=\"duplicate_name\">已存在相同名称</string>\n    <string name=\"image_search_not_quick_search\">无法添加图片搜索为快速搜索</string>\n    <string name=\"duplicate_quick_search\">已存在相同的快速搜索，名称为“%s”。</string>\n    <string name=\"delete\">删除</string>\n    <string name=\"delete_quick_search_title\">删除快速搜索</string>\n    <string name=\"delete_quick_search_message\">删除“%s”？</string>\n\n    <!-- Gallery Search -->\n    <string name=\"search_normal\">普通搜索</string>\n    <string name=\"search_normal_search\">一般搜索</string>\n    <string name=\"search_subscription_search\">订阅搜索</string>\n    <string name=\"search_specify_uploader\">指定上传者</string>\n    <string name=\"search_specify_tag\">指定标签</string>\n    <string name=\"search_tip\">一般搜索：就是搜索。\\n\\n订阅搜素：在订阅中搜索。\\n\\n指定上传者：列出该上传者上传的画廊。其他选项会被忽略。\\n\\n指定标签：列出含有该标签的画廊。其他选项会被忽略。</string>\n    <string name=\"search_enable_advance\">启用高级选项</string>\n    <string name=\"search_advance\">高级选项</string>\n    <string name=\"search_sh\">仅显示被删除的画廊</string>\n    <string name=\"search_sto\">仅显示有种子的画廊</string>\n    <string name=\"search_sr\">最低评分：</string>\n    <string name=\"star_2\">2 星</string>\n    <string name=\"star_3\">3 星</string>\n    <string name=\"star_4\">4 星</string>\n    <string name=\"star_5\">5 星</string>\n    <string name=\"search_sp\">页数：</string>\n    <string name=\"search_sp_to\">到</string>\n    <string name=\"search_sp_err0\">页数最小值至多为 1000</string>\n    <string name=\"search_sp_err1\">页数最大值至少为 10</string>\n    <string name=\"search_sp_err2\">页数范围差至少为 20</string>\n    <string name=\"search_sp_err3\">页数范围比至多为 0.5</string>\n    <string name=\"search_sf\">禁用排除项：</string>\n    <string name=\"search_sfl\">语言</string>\n    <string name=\"search_sfu\">上传者</string>\n    <string name=\"search_sft\">标签</string>\n    <string name=\"search_image\">图片搜索</string>\n    <string name=\"select_image\">选择图片</string>\n    <string name=\"select_image_first\">请先选择图片</string>\n    <string name=\"keyword_search\">关键字搜索</string>\n    <string name=\"image_search\">图片搜索</string>\n\n    <!-- Gallery Detail Scene -->\n    <string name=\"download_upgradeable\">可升级</string>\n    <string name=\"download_upgrade_existed\">目标画廊已存在，请删除后重试</string>\n    <string name=\"download_upgrade_service_failed\">由于系统限制，无法从后台启动下载服务，请点击确定开始下载</string>\n    <string name=\"read_from\">从第 %d 页阅读</string>\n    <plurals name=\"page_count\">\n        <item quantity=\"other\">%d 页</item>\n    </plurals>\n    <string name=\"favored_times\">\\u2665 %d</string>\n    <string name=\"more_information\">查看更多信息</string>\n    <string name=\"newer_version_avaliable\">此画廊有新版本可用</string>\n    <string name=\"newer_version_title\">%1$s, 添加于 %2$s</string>\n    <string name=\"favorited\">已收藏</string>\n    <string name=\"not_favorited\">未收藏</string>\n    <string name=\"share\">分享</string>\n    <string name=\"torrent_count\">种子 (%d)</string>\n    <string name=\"archive\">压缩包</string>\n    <string name=\"similar_gallery\">相似画廊</string>\n    <string name=\"no_tags\">暂无标签</string>\n    <string name=\"no_comments\">暂无评论</string>\n    <string name=\"more_comment\">查看更多评论</string>\n    <string name=\"no_more_comments\">已显示所有评论</string>\n    <string name=\"no_previews\">没有预览</string>\n    <string name=\"more_previews\">查看更多预览</string>\n    <string name=\"no_more_previews\">已显示所有预览</string>\n    <!-- Gallery Actions -->\n    <string name=\"refresh\">刷新</string>\n    <string name=\"action_add_tag\">添加标签</string>\n    <string name=\"action_add_tag_tip\">添加新标签，用逗号「,」分割</string>\n    <string name=\"action_clear_image_cache\">清除图片缓存</string>\n    <string name=\"action_image_cache_cleared\">已清除图片缓存</string>\n    <string name=\"open_in_other_app\">在其他应用中打开</string>\n    <string name=\"filter_the_uploader\">屏蔽上传者“%s”？</string>\n    <string name=\"filter_added\">已添加屏蔽项</string>\n    <string name=\"copy_trans\">复制翻译</string>\n    <string name=\"show_definition\">查看定义</string>\n    <string name=\"filter_the_tag\">屏蔽标签“%s”？</string>\n    <string name=\"tag_vote_up\">是的，这很正确</string>\n    <string name=\"tag_vote_down\">不，这不是</string>\n    <string name=\"tag_vote_up_cancel\">取消赞成</string>\n    <string name=\"tag_vote_down_cancel\">取消反对</string>\n    <string name=\"tag_vote_successfully\">投票成功</string>\n    <string name=\"vote_failed\">投票失败</string>\n    <!-- Gallery Info -->\n    <string name=\"gallery_info\">画廊信息</string>\n    <string name=\"header_key\">键</string>\n    <string name=\"header_value\">值</string>\n    <string name=\"key_url\">链接</string>\n    <string name=\"key_title\">标题</string>\n    <string name=\"key_title_jpn\">日文标题</string>\n    <string name=\"key_thumb\">缩略图</string>\n    <string name=\"key_category\">分类</string>\n    <string name=\"key_uploader\">上传者</string>\n    <string name=\"key_posted\">上传时间</string>\n    <string name=\"key_parent\">父画廊</string>\n    <string name=\"key_visible\">可见性</string>\n    <string name=\"key_language\">语言</string>\n    <string name=\"key_pages\">页数</string>\n    <string name=\"key_size\">大小</string>\n    <string name=\"key_favorite_count\">收藏次数</string>\n    <string name=\"key_favorited\">是否收藏</string>\n    <string name=\"key_favorite_name\">收藏分类</string>\n    <string name=\"key_rating_count\">评价次数</string>\n    <string name=\"key_rating\">评分</string>\n    <string name=\"key_torrents\">种子个数</string>\n    <string name=\"key_torrent_url\">种子</string>\n    <string name=\"copied_to_clipboard\">已复制到剪切板</string>\n    <!-- Gallery Torrents -->\n    <string name=\"torrents\">种子</string>\n    <string name=\"no_torrents\">没有种子</string>\n    <string name=\"download_torrent_started\">开始下载种子</string>\n    <string name=\"download_torrent_failure\">无法下载种子</string>\n    <!-- Gallery Archives -->\n    <string name=\"no_archives\">没有压缩包</string>\n    <string name=\"archive_original\">原始档案</string>\n    <string name=\"archive_resample\">重采样档案</string>\n    <string name=\"archive_free\">免费</string>\n    <string name=\"current_funds\">资金：%1$s GP，%2$d Credits</string>\n    <string name=\"insufficient_funds\">余额不足 (GP)</string>\n    <string name=\"download_archive_started\">开始下载压缩包</string>\n    <string name=\"download_archive_failure\">无法下载压缩包</string>\n    <string name=\"download_archive_failure_no_hath\">下载压缩包需要 H@H 客户端</string>\n    <!-- Gallery Rating -->\n    <string name=\"rate\">评分</string>\n    <string name=\"rate_successfully\">评分成功</string>\n    <string name=\"rate_failed\">评分失败</string>\n    <string name=\"rating10\">根本把持不住</string>\n    <string name=\"rating9\">好极了</string>\n    <string name=\"rating8\">很棒</string>\n    <string name=\"rating7\">不错</string>\n    <string name=\"rating6\">还行</string>\n    <string name=\"rating5\">一般般</string>\n    <string name=\"rating4\">不行</string>\n    <string name=\"rating3\">糟糕</string>\n    <string name=\"rating2\">瞎眼</string>\n    <string name=\"rating1\">快要窒息了</string>\n    <string name=\"rating0\">…</string>\n    <!-- Gallery Comments -->\n    <string name=\"gallery_comments\">画廊评论</string>\n    <string name=\"no_one_comments_gallery\">暂时没有评论</string>\n    <string name=\"comment_successfully\">评论成功</string>\n    <string name=\"comment_failed\">评论失败</string>\n    <string name=\"comment_user_uploader\">%s （上传者）</string>\n    <string name=\"last_edited\">上次修改时间：%s</string>\n    <string name=\"click_more_comments\">点击加载更多评论</string>\n    <string name=\"copy_comment_text\">复制评论文字</string>\n    <string name=\"edit_comment\">修改评论</string>\n    <string name=\"edit_comment_successfully\">评论已修改</string>\n    <string name=\"edit_comment_failed\">评论修改失败</string>\n    <string name=\"block_commenter\">屏蔽评论者</string>\n    <string name=\"filter_the_commenter\">屏蔽评论者“%s”？</string>\n    <string name=\"vote_up\">深表赞同</string>\n    <string name=\"cancel_vote_up\">不再深表赞同</string>\n    <string name=\"vote_down\">垃圾评论</string>\n    <string name=\"cancel_vote_down\">不是垃圾评论</string>\n    <string name=\"vote_up_successfully\">已深表赞同</string>\n    <string name=\"cancel_vote_up_successfully\">已不再深表赞同</string>\n    <string name=\"vote_down_successfully\">已钦定为垃圾评论</string>\n    <string name=\"cancel_vote_down_successfully\">已不再钦定为垃圾评论</string>\n    <string name=\"check_vote_status\">查看投票情况</string>\n    <string name=\"format_bold\"><b>加粗</b></string>\n    <string name=\"format_italic\"><i>倾斜</i></string>\n    <string name=\"format_strikethrough\"><strike>删除线</strike></string>\n    <string name=\"format_underline\"><u>下划线</u></string>\n    <string name=\"format_url\">链接</string>\n    <string name=\"format_plain\">纯文本</string>\n    <!-- Gallery Previews -->\n    <string name=\"gallery_previews\">画廊预览</string>\n\n    <!-- Favorites Scene -->\n    <string name=\"collections\">收藏夹</string>\n    <string name=\"favorites_search_bar_hint\">搜索 %s</string>\n    <string name=\"favorites_title\">%s</string>\n    <string name=\"favorites_title_2\">%1$s - %2$s</string>\n    <string name=\"need_sign_in\">需要登录</string>\n    <!-- Favorites Collection -->\n    <string name=\"default_favorites_collection\">默认收藏夹</string>\n    <string name=\"cloud_favorites\">云端收藏</string>\n    <string name=\"let_me_select\">让我选择</string>\n    <!-- Favorites Fab Action -->\n    <string name=\"delete_favorites_dialog_title\">删除收藏</string>\n    <string name=\"delete_favorites_dialog_message\">删除 %d 项收藏？</string>\n    <string name=\"move_favorites_dialog_title\">移动收藏</string>\n\n    <!-- History Scene -->\n    <string name=\"no_history\">这里是阅读历史</string>\n    <string name=\"clear_all\">全部清除</string>\n    <string name=\"clear_all_history\">清除所有阅读历史？</string>\n\n    <!-- Download Scene -->\n    <string name=\"scene_download_title\">下载 - %s</string>\n    <string name=\"no_download_info\">这里是下载项目</string>\n    <string name=\"download_state_none\">未启动</string>\n    <string name=\"download_state_wait\">等待中</string>\n    <string name=\"download_state_downloading\">下载中</string>\n    <string name=\"download_state_downloaded\">已下载</string>\n    <string name=\"download_state_failed\">失败</string>\n    <string name=\"download_state_failed_2\">%d 未完成</string>\n    <string name=\"download_state_finish\">已完成</string>\n    <!-- Download Action Bar -->\n    <string name=\"download_filter\">筛选</string>\n    <string name=\"download_filter_title\">筛选标题</string>\n    <string name=\"download_start_all\">全部开始</string>\n    <string name=\"download_stop_all\">全部停止</string>\n    <string name=\"download_start_all_reversed\">全部开始（倒序）</string>\n    <string name=\"download_sort_by\">排序规则</string>\n    <string name=\"download_sort_added_time_desc\">添加时间（降序）</string>\n    <string name=\"download_sort_added_time_asc\">添加时间（升序）</string>\n    <string name=\"download_sort_title_asc\">画廊标题（升序）</string>\n    <string name=\"download_sort_title_desc\">画廊标题（降序）</string>\n    <string name=\"download_sort_author_asc\">画廊作者（升序）</string>\n    <string name=\"download_sort_author_desc\">画廊作者（降序）</string>\n    <string name=\"download_sort_name_asc\">画廊名称（升序）</string>\n    <string name=\"download_sort_name_desc\">画廊名称（降序）</string>\n    <string name=\"download_sort_category_asc\">画廊分类（升序）</string>\n    <string name=\"download_sort_category_desc\">画廊分类（降序）</string>\n    <string name=\"download_sort_shuffle\">随机排序</string>\n    <string name=\"download_reset_reading_progress\">重置阅读进度</string>\n    <string name=\"reset_reading_progress_message\">重置所有已下载画廊的阅读进度？</string>\n    <!-- Download Fab Action -->\n    <string name=\"download_remove_dialog_message_2\">从下载列表移除 %d 项？</string>\n    <!-- Download Labels -->\n    <string name=\"download_labels\">下载标签</string>\n    <string name=\"download_all\">全部</string>\n    <string name=\"add\">添加</string>\n    <string name=\"new_label_title\">新标签</string>\n    <string name=\"label_text_is_empty\">标签文本为空</string>\n    <string name=\"label_text_is_invalid\">“全部”或“默认”是无效标签</string>\n    <string name=\"label_text_exist\">标签已存在</string>\n    <string name=\"rename_label\">重命名</string>\n    <string name=\"rename_label_title\">重命名标签</string>\n    <string name=\"delete_label_title\">删除标签</string>\n    <string name=\"delete_label_message\">删除“%s”？</string>\n    <string name=\"default_download_label\">默认下载标签</string>\n    <!-- Download Service -->\n    <string name=\"download_service\">下载服务</string>\n    <string name=\"download_service_label\">EhViewer 下载服务</string>\n    <string name=\"download_speed_text_2\">%1$s，剩余 %2$s</string>\n    <string name=\"stat_download_action_stop_all\">全部停止</string>\n    <string name=\"stat_509_alert_title\">509 警告</string>\n    <string name=\"stat_509_alert_text\">图片配额已用尽。请停止下载，休息一下。</string>\n    <string name=\"stat_download_done_title\">下载结束</string>\n    <string name=\"stat_download_done_text_succeeded\">%d 项下载成功</string>\n    <string name=\"stat_download_done_text_failed\">%d 项下载失败</string>\n    <string name=\"stat_download_done_text_mix\">%1$d 项下载成功，%2$d 项下载失败</string>\n    <string name=\"stat_download_done_line_succeeded\">下载成功：%s</string>\n    <string name=\"stat_download_done_line_failed\">下载失败：%s</string>\n\n    <!-- Settings -->\n    <string name=\"settings_eh\">EH</string>\n    <string name=\"settings_eh_account_name\">账户</string>\n    <string name=\"settings_eh_account_name_tourist\">游客模式</string>\n    <string name=\"settings_eh_account_refresh_igneous\">刷新 igneous</string>\n    <string name=\"settings_eh_account_sign_out\">退出登录</string>\n    <string name=\"settings_eh_account_sign_out_tip\">已退出登录，稍后可重新登录</string>\n    <string name=\"settings_eh_account_identity_cookies\">身份 Cookie 可用于登录该账号。\\n注意数据安全\\n\\n%s</string>\n    <string name=\"settings_eh_account_igneous_expire\">igneous 自动刷新：</string>\n    <string name=\"settings_eh_account_identity_cookies_copy\">复制</string>\n    <string name=\"settings_eh_gallery_site\">画廊站点</string>\n    <string name=\"settings_eh_image_limits\">图片配额</string>\n    <string name=\"settings_eh_image_limits_summary\">加载中…</string>\n    <string name=\"settings_eh_image_limits_summary_ip\">基于 IP 的限制：</string>\n    <string name=\"settings_eh_image_limits_summary_ip_ok\">未受限</string>\n    <string name=\"settings_eh_image_limits_summary_ip_restricted\">高分辨率图片受限</string>\n    <string name=\"settings_eh_image_limits_summary_acc\">当前已用：%1$d / %2$d</string>\n    <string name=\"settings_eh_unlock_cost\">您可以花费 %1$d GP 来解锁 24 小时高分辨率配额</string>\n    <string name=\"settings_eh_unlock\">解锁</string>\n    <string name=\"settings_eh_reset_cost\">重置花费：%1$d GP</string>\n    <string name=\"settings_eh_reset\">重置</string>\n    <string name=\"settings_eh_reset_limits_succeed\">成功重置图片配额</string>\n    <string name=\"settings_eh_u_config\">E-Hentai 设置</string>\n    <string name=\"settings_eh_u_config_summary\">E-Hentai 网站上的设置</string>\n    <string name=\"settings_eh_my_tags\">我的标签</string>\n    <string name=\"settings_eh_my_tags_summary\">E-Hentai 网站上的我的标签</string>\n    <string name=\"settings_eh_black_dark_theme\">纯黑深色主题</string>\n    <string name=\"settings_eh_launch_page\">启动页</string>\n    <string name=\"settings_eh_list_mode\">列表模式</string>\n    <string name=\"settings_eh_list_mode_detail\">详情</string>\n    <string name=\"settings_eh_list_mode_thumb\">缩略图</string>\n    <string name=\"settings_eh_detail_size\">详情模式下条目宽度</string>\n    <string name=\"settings_eh_list_tile_thumb_size\">详情模式下缩略图大小</string>\n    <string name=\"settings_eh_thumb_size\">缩略图模式下缩略图大小</string>\n    <string name=\"settings_eh_thumb_show_title\">缩略图模式下显示画廊标题</string>\n    <string name=\"settings_eh_show_jpn_title\">显示日文标题</string>\n    <string name=\"settings_eh_show_jpn_title_summary\">需同时在 E-Hentai 网站设置中启用 Japanese Title</string>\n    <string name=\"settings_eh_show_gallery_pages\">显示画廊页数</string>\n    <string name=\"settings_eh_show_gallery_pages_summary\">在画廊列表中显示页数</string>\n    <string name=\"settings_eh_show_gallery_comments\">显示画廊评论</string>\n    <string name=\"settings_eh_show_gallery_comments_summary\">在画廊详情页中显示评论</string>\n    <string name=\"settings_eh_comment_threshold\">评论分数阈值</string>\n    <string name=\"settings_eh_comment_threshold_summary\">隐藏低于或等于此分数的评论（-101 禁用）</string>\n    <string name=\"settings_eh_preview_num\">画廊详情页预览图最大数量</string>\n    <string name=\"settings_eh_preview_size\">画廊详情页预览图大小</string>\n    <string name=\"settings_eh_show_tag_translations\">显示标签翻译</string>\n    <string name=\"settings_eh_show_tag_translations_summary\">显示翻译后的标签而非原始文字（需花费时间来下载数据文件）</string>\n    <string name=\"settings_eh_tag_translations_source\">补充翻译（由 EhTagTranslation 提供）</string>\n    <string name=\"settings_eh_tag_translations_source_url\">https://github.com/EhTagTranslation/Editor/wiki</string>\n    <string name=\"settings_eh_filter\">屏蔽列表</string>\n    <string name=\"settings_eh_filter_summary\">根据标题、上传者、标签、评论者屏蔽画廊或评论</string>\n    <string name=\"settings_eh_metered_network_warning\">流量计费网络警告</string>\n    <string name=\"settings_eh_request_news\">启动时请求新闻页面</string>\n    <string name=\"settings_eh_hide_hv_events\">隐藏 HV 事件通知</string>\n    <string name=\"settings_read\">阅读</string>\n    <string name=\"settings_read_screen_rotation\">屏幕方向</string>\n    <string name=\"settings_read_screen_rotation_default\">系统默认</string>\n    <string name=\"settings_read_screen_rotation_portrait\">竖屏</string>\n    <string name=\"settings_read_screen_rotation_landscape\">横屏</string>\n    <string name=\"settings_read_screen_rotation_sensor\">自动旋转</string>\n    <string name=\"settings_read_reading_direction\">阅读方向</string>\n    <string name=\"settings_read_reading_direction_left_to_right\">从左至右</string>\n    <string name=\"settings_read_reading_direction_right_to_Left\">从右至左</string>\n    <string name=\"settings_read_reading_direction_top_to_bottom\">从上至下</string>\n    <string name=\"settings_read_page_scaling\">页面缩放</string>\n    <string name=\"settings_read_page_scaling_actual_size\">原始尺寸</string>\n    <string name=\"settings_read_page_scaling_fit_to_width\">匹配宽度</string>\n    <string name=\"settings_read_page_scaling_fit_to_height\">匹配长度</string>\n    <string name=\"settings_read_page_scaling_fit_to_screen\">匹配屏幕</string>\n    <string name=\"settings_read_page_scaling_fixed_scale\">固定缩放</string>\n    <string name=\"settings_read_start_position\">开页位置</string>\n    <string name=\"settings_read_start_position_top_left\">左上角</string>\n    <string name=\"settings_read_start_position_top_right\">右上角</string>\n    <string name=\"settings_read_start_position_bottom_left\">左下角</string>\n    <string name=\"settings_read_start_position_bottom_right\">右下角</string>\n    <string name=\"settings_read_start_position_center\">中心</string>\n    <string name=\"settings_read_theme\">色彩主题</string>\n    <string name=\"settings_read_theme_follow_app\">跟随应用</string>\n    <string name=\"settings_read_theme_dark\">暗色</string>\n    <string name=\"settings_read_theme_light\">亮色</string>\n    <string name=\"settings_read_keep_screen_on\">屏幕常亮</string>\n    <string name=\"settings_read_show_clock\">显示时钟</string>\n    <string name=\"settings_read_show_progress\">显示进度</string>\n    <string name=\"settings_read_show_battery\">显示电量</string>\n    <string name=\"settings_read_show_page_interval\">显示页面间隔</string>\n    <string name=\"settings_read_turn_page_interval\">自动翻页间隔（秒）</string>\n    <string name=\"settings_read_volume_page\">使用音量键翻页</string>\n    <string name=\"settings_read_volume_page_interval\">音量键翻页间隔</string>\n    <string name=\"settings_read_reverse_volume\">翻转音量键</string>\n    <string name=\"settings_read_reading_fullscreen\">全屏</string>\n    <string name=\"settings_read_custom_screen_lightness\">自定义屏幕亮度</string>\n    <string name=\"settings_read_screen_lightness\">屏幕亮度</string>\n    <string name=\"settings_download\">下载</string>\n    <string name=\"settings_download_download_location\">下载路径</string>\n    <string name=\"settings_download_pick_new_location\">选择新路径</string>\n    <string name=\"settings_download_reset_location\">恢复默认路径</string>\n    <string name=\"settings_download_invalid_download_location\">无效的下载路径</string>\n    <string name=\"settings_download_cant_get_download_location\">无法获取下载路径</string>\n    <string name=\"settings_download_media_scan\">允许媒体扫描</string>\n    <string name=\"settings_download_media_scan_summary_on\">请避免他人翻看您的图库应用</string>\n    <string name=\"settings_download_media_scan_summary_off\">大多数图库应用将不会显示下载目录中的图片</string>\n    <string name=\"settings_download_concurrency\">并发下载数</string>\n    <string name=\"settings_download_concurrency_summary\">最多同时下载 %s 张图片</string>\n    <string name=\"settings_download_download_delay\">下载延时</string>\n    <string name=\"settings_download_download_delay_summary\">每次下载延时 %s 毫秒</string>\n    <string name=\"settings_download_download_timeout\">下载超时（秒）</string>\n    <string name=\"settings_download_preload_image\">预载图片</string>\n    <string name=\"settings_download_preload_image_summary\">向后预载 %s 张图片</string>\n    <string name=\"settings_download_download_origin_image\">加载原图</string>\n    <string name=\"settings_download_download_origin_image_summary\">%s，警告！可能需要消耗 GP</string>\n    <string name=\"settings_download_download_origin_image_never\">从不启用</string>\n    <string name=\"settings_download_download_origin_image_force\">总是启用</string>\n    <string name=\"settings_download_download_origin_image_only\">仅下载时启用</string>\n    <string name=\"settings_download_task_confirm\">确定要执行操作？</string>\n    <string name=\"settings_download_restore_download_items\">恢复下载项</string>\n    <string name=\"settings_download_restore_download_items_summary\">恢复下载目录里的所有下载项</string>\n    <string name=\"settings_download_restore_not_found\">未找到可恢复下载项</string>\n    <string name=\"settings_download_restore_failed\">恢复失败</string>\n    <string name=\"settings_download_restore_successfully\">成功恢复 %d 项</string>\n    <string name=\"settings_download_clean_redundancy\">清理下载冗余</string>\n    <string name=\"settings_download_clean_redundancy_summary\">清理下载目录中不在下载列表里的图片文件</string>\n    <string name=\"settings_download_clean_redundancy_no_redundancy\">未发现冗余</string>\n    <string name=\"settings_download_clean_redundancy_done\">完成冗余清理，共清理 %d 项</string>\n    <string name=\"settings_privacy\">隐私</string>\n    <string name=\"settings_privacy_pattern_protection_title\">图案保护</string>\n    <string name=\"settings_privacy_pattern_protection_not_set\">未设置图案保护</string>\n    <string name=\"settings_privacy_pattern_protection_set\">已设置图案保护</string>\n    <string name=\"settings_privacy_secure\">不允许屏幕抓取</string>\n    <string name=\"settings_privacy_secure_summary\">启用后，将不能截取该应用的屏幕截图。同时，也不会在系统任务切换器中显示该应用的内容预览。请重新启动应用以使此更改生效</string>\n    <string name=\"settings_privacy_clear_search_history\">清除设备上的搜索记录</string>\n    <string name=\"settings_privacy_clear_search_history_summary\">移除您在此设备上执行过的搜索查询的记录</string>\n    <string name=\"settings_privacy_clear_search_history_cleared\">已清除搜索记录</string>\n    <string name=\"settings_advanced\">高级</string>\n    <string name=\"settings_advanced_save_parse_error_body\">解析失败时保存页面内容</string>\n    <string name=\"settings_advanced_save_parse_error_body_summary\">页面内容可能含有隐私敏感信息</string>\n    <string name=\"settings_advanced_save_crash_log\">应用崩溃时保存错误日志</string>\n    <string name=\"settings_advanced_save_crash_log_summary\">错误日志可以帮助开发者修正问题</string>\n    <string name=\"settings_advanced_dump_logcat\">导出日志</string>\n    <string name=\"settings_advanced_dump_logcat_summary\">保存日志至外置存储器</string>\n    <string name=\"settings_advanced_dump_logcat_failed\">导出日志失败</string>\n    <string name=\"settings_advanced_dump_logcat_to\">已保存日志至 %s</string>\n    <string name=\"settings_advanced_read_cache_size\">阅读缓存大小</string>\n    <string name=\"settings_advanced_app_language_title\">App 界面语言</string>\n    <string name=\"settings_advanced_proxy\">代理</string>\n    <string name=\"settings_advanced_backup_favorite\">备份收藏列表</string>\n    <string name=\"settings_advanced_backup_favorite_summary\">备份云端收藏列表到本地</string>\n    <string name=\"settings_advanced_backup_favorite_start\">正在备份收藏列表 %s</string>\n    <string name=\"settings_advanced_backup_favorite_nothing\">没有可以备份的收藏列表</string>\n    <string name=\"settings_advanced_backup_favorite_success\">备份收藏列表成功</string>\n    <string name=\"settings_advanced_backup_favorite_failed\">备份收藏列表失败</string>\n    <string name=\"settings_advanced_export_data\">导出数据</string>\n    <string name=\"settings_advanced_export_data_summary\">保存数据至外置存储器，例如下载列表，快速搜索列表</string>\n    <string name=\"settings_advanced_export_data_to\">已导出数据至 %s</string>\n    <string name=\"settings_advanced_export_data_failed\">导出数据失败</string>\n    <string name=\"settings_advanced_import_data\">导入数据</string>\n    <string name=\"settings_advanced_import_data_summary\">从外置存储器导入数据</string>\n    <string name=\"settings_advanced_import_data_successfully\">导入数据成功</string>\n    <string name=\"settings_advanced_import_data_cant_read\">无法读取文件</string>\n    <string name=\"settings_advanced_open_by_default\">默认打开</string>\n    <string name=\"settings_about\">关于</string>\n    <string name=\"settings_about_declaration_summary\">EhViewer 与 E-Hentai.org 无任何联系</string>\n    <string name=\"settings_about_author\">作者</string>\n    <string name=\"settings_about_issues\">常见问题</string>\n    <string name=\"settings_about_latest_release\">最新版本</string>\n    <string name=\"settings_about_source\">源码</string>\n    <string name=\"settings_about_version\">版本号</string>\n    <string name=\"settings_about_build_time\">构建于 %s</string>\n    <!-- UConfig Activity -->\n    <string name=\"u_config\">EHentai 设置</string>\n    <string name=\"apply\">应用</string>\n    <string name=\"apply_tip\">点击右上角的对勾来保存设置</string>\n    <!-- My Tags Activity -->\n    <string name=\"my_tags\">我的标签</string>\n    <!-- Filter Activity -->\n    <string name=\"filter\">屏蔽列表</string>\n    <string name=\"tip\">提示</string>\n    <string name=\"filter_tip\">该屏蔽系统会在 EHentai 网站屏蔽系统的基础上继续屏蔽画廊。\\n\\n标题屏蔽项：排除标题含有该关键字的画廊。\\n\\n上传者屏蔽项：排除该上传者上传的画廊。\\n\\n标签屏蔽项：排除包含该标签的画廊，这会使获取画廊列表花费更多时间。\\n\\n命名空间屏蔽项：排除包含该命名空间的画廊，这会使获取画廊列表花费更多时间。\\n\\n评论者屏蔽项：排除该评论者发布的评论。\\n\\n评论屏蔽项：排除匹配该正则表达式的评论。</string>\n    <string name=\"no_filter\">这里是屏蔽列表</string>\n    <string name=\"add_filter\">添加屏蔽项</string>\n    <string name=\"filter_title\">标题</string>\n    <string name=\"filter_uploader\">上传者</string>\n    <string name=\"filter_tag\">标签</string>\n    <string name=\"filter_tag_namespace\">命名空间</string>\n    <string name=\"filter_commenter\">评论者</string>\n    <string name=\"filter_comment\">评论</string>\n    <string name=\"filter_text\">屏蔽项文本</string>\n    <string name=\"delete_filter\">删除屏蔽项“%s”？</string>\n    <!-- Set Security -->\n    <string name=\"set_pattern_protection\">设置图案保护</string>\n    <string name=\"set_pattern_protection_tip\">绘制图案来设置图案保护\\n留空来取消图案保护</string>\n    <string name=\"enable_biometric\">允许使用生物特征解锁</string>\n    <string name=\"set\">设置</string>\n    <!-- Languages -->\n    <string name=\"app_language_system\">系统语言（默认）</string>\n    <!-- Proxy -->\n    <string name=\"proxy_direct\">直接连接</string>\n    <string name=\"proxy_system\">系统代理</string>\n    <string name=\"proxy_host_or_ip\">主机或 IP</string>\n    <string name=\"proxy_port\">端口</string>\n    <string name=\"proxy_invalid_port\">无效的端口</string>\n\n    <!-- Errors -->\n    <string name=\"error_bad_status_code\">错误状态码：%d</string>\n    <string name=\"error_timeout\">超时</string>\n    <string name=\"error_unknown_host\">未知主机</string>\n    <string name=\"error_redirection\">太多重定向</string>\n    <string name=\"error_socket\">网络错误</string>\n    <string name=\"error_unknown\">奇怪的错误</string>\n    <string name=\"error_cant_find_activity\">找不到相应的应用</string>\n    <string name=\"error_cannot_parse_the_url\">无法解析链接</string>\n    <string name=\"error_decoding_failed\">解码失败</string>\n    <string name=\"error_empty\">空</string>\n    <string name=\"error_reading_failed\">读取失败</string>\n    <string name=\"error_out_of_range\">越界</string>\n    <string name=\"error_write_failed\">写入失败</string>\n    <string name=\"error_parse_error\">解析失败</string>\n    <string name=\"error_invalid_url\">无效链接</string>\n    <string name=\"error_get_ptoken_error\">获取 pToken 错误</string>\n    <string name=\"error_cant_create_temp_file\">无法创建临时文件</string>\n    <string name=\"error_cant_save_image\">无法保存图片</string>\n    <string name=\"error_invalid_number\">非法数字</string>\n    <string name=\"error_please_login_first\">请先登录</string>\n    <string name=\"error_cannot_find_gallery\">找不到画廊</string>\n    <string name=\"error_something_wrong_happened\">被玩坏了</string>\n    <string name=\"kokomade_tip\">设置-&gt;Eh-&gt;画廊站点-&gt;E-Hentai</string>\n    <string name=\"no_browser_installed\">请安装一个浏览器。</string>\n    <string name=\"cloudflare_bypass_failed\">未能绕过 Cloudflare 验证</string>\n    <string name=\"open_in_webview\">在 WebView 中打开链接以通过验证？</string>\n    <string name=\"information_webview_outdated\">请更新 WebView 应用提高兼容性</string>\n\n    <!-- Readable Time -->\n    <string name=\"from_the_future\">来自未来</string>\n    <string name=\"just_now\">刚刚</string>\n    <plurals name=\"some_minutes_ago\">\n        <item quantity=\"other\">%d 分钟前</item>\n    </plurals>\n    <plurals name=\"some_hours_ago\">\n        <item quantity=\"other\">%d 小时前</item>\n    </plurals>\n    <string name=\"yesterday\">昨天</string>\n    <string name=\"some_days_ago\">%d 天前</string>\n    <plurals name=\"second\">\n        <item quantity=\"other\">秒</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">分钟</item>\n    </plurals>\n    <plurals name=\"hour\">\n        <item quantity=\"other\">小时</item>\n    </plurals>\n    <plurals name=\"day\">\n        <item quantity=\"other\">天</item>\n    </plurals>\n    <plurals name=\"year\">\n        <item quantity=\"other\">年</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rHK/bools.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<resources>\n\n    <bool name=\"tag_translatable\">true</bool>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n    <!-- Login Scene -->\n    <string name=\"app_waring\">本應用中的內容均來自互聯網，部分內容可能對您的生理及心理造成難以恢復的傷害。本應用作者不會對因使用本應用所造成的任何後果負責。\\n未成年人應在監護人指導下使用本應用。\\n繼續使用即代表您同意上述條款。</string>\n    <string name=\"username\">用户名</string>\n    <string name=\"password\">密碼</string>\n    <string name=\"sign_in\">登錄</string>\n    <string name=\"register\">註冊</string>\n    <string name=\"sign_in_via_webview\">通過網頁登錄</string>\n    <string name=\"sign_in_via_cookies\">通過 Cookie 登錄</string>\n    <string name=\"tourist_mode\">遊客模式</string>\n    <string name=\"error_username_cannot_empty\">用户名不可為空</string>\n    <string name=\"error_password_cannot_empty\">密碼不可為空</string>\n    <string name=\"sign_in_failed\">登錄失敗</string>\n    <string name=\"sign_in_failed_tip\">若持續出錯，請嘗試“通過網頁登錄”。</string>\n    <string name=\"sign_in_failed_tip_2\">如果你確認 Cookie 正確，你可以選擇忽略錯誤並繼續，但可能會導致一些問題。</string>\n    <string name=\"ignore\">忽略</string>\n    <string name=\"get_it\">知道了</string>\n    <!-- Cookie Sign In Scene -->\n    <string name=\"cookie_explain\">Cookie 是存儲在瀏覽器裏的一小塊數據。如果您不清楚 Cookie 是什麼或者怎麼獲取 Cookie，最好搜索一下。\\n\\n請輸入特定的 Cookie 來登錄。</string>\n    <string name=\"from_clipboard\">從剪貼板導入</string>\n    <string name=\"text_is_empty\">文本為空</string>\n    <string name=\"from_clipboard_error\">未在剪貼板中發現 Cookies。</string>\n\n    <!-- Select Site Scene -->\n    <string name=\"select_scene\">您希望訪問哪個畫廊站點？</string>\n    <string name=\"select_scene_explain\">E-Hentai：對任何人開放\\nExHentai：僅對登錄用户開放</string>\n\n    <!-- Guide -->\n    <string name=\"guide_gallery_left\">翻頁</string>\n    <string name=\"guide_gallery_right\">翻頁</string>\n    <string name=\"guide_gallery_menu\">菜單</string>\n    <string name=\"guide_gallery_progress\">進度條</string>\n    <string name=\"guide_gallery_long_click\">長按打開頁面菜單</string>\n\n    <!-- Main Activity -->\n    <string name=\"metered_network_warning\">正在使用流量計費網絡</string>\n    <string name=\"waring\">警告</string>\n    <string name=\"invalid_download_location\">下載路徑似乎不可用。請重新設置下載路徑。</string>\n    <string name=\"press_twice_exit\">再按一次退出</string>\n    <string name=\"clipboard_gallery_url_snack_message\">剪切板裏有畫廊鏈接</string>\n    <string name=\"clipboard_gallery_url_snack_action\">查看</string>\n    <string name=\"please_wait\">請稍候…</string>\n    <string name=\"app_link_not_verified_title\">應用鏈接未驗證</string>\n    <string name=\"app_link_not_verified_message\">對於 Android 12 及更高版本，您需要手動添加鏈接到已驗證鏈接才能在 EhViewer 中打開 E-Hentai 鏈接。</string>\n    <string name=\"open_settings\">打開設置</string>\n    <string name=\"dont_show_again\">不再顯示</string>\n\n    <!-- Gallery Activity -->\n    <string name=\"archive_need_passwd\">歸檔文件需要密碼</string>\n    <string name=\"archive_passwd\">請輸入密碼</string>\n    <string name=\"passwd_cannot_be_empty\">密碼不能為空</string>\n    <string name=\"passwd_wrong\">密碼錯誤</string>\n    <string name=\"page_menu_title\">第 %d 頁</string>\n    <string name=\"page_menu_refresh\">@string/refresh</string>\n    <string name=\"page_menu_share\">@string/share</string>\n    <string name=\"page_menu_save\">保存</string>\n    <string name=\"page_menu_save_to\">保存到…</string>\n    <string name=\"page_menu_download_original\">下載原圖</string>\n    <string name=\"gallery_menu_title\">菜單</string>\n    <string name=\"share_image\">分享圖片</string>\n    <string name=\"start_download_original\">開始下載原圖，請稍候…</string>\n    <string name=\"image_saved\">圖片已保存至 %s</string>\n\n    <!-- Navigation Bar -->\n    <string name=\"homepage\">主頁</string>\n    <string name=\"subscription\">訂閲</string>\n    <string name=\"whats_hot\">熱門</string>\n    <string name=\"toplist\">排行</string>\n    <string name=\"favourite\">收藏</string>\n    <string name=\"history\">歷史</string>\n    <string name=\"downloads\">下載</string>\n    <string name=\"settings\">設置</string>\n\n    <!-- Gallery List Scene -->\n    <string name=\"search\">搜索</string>\n    <string name=\"gallery_list_search_bar_hint_exhentai\">搜索 ExHentai</string>\n    <string name=\"gallery_list_search_bar_hint_e_hentai\">搜索 E-Hentai</string>\n    <string name=\"gallery_list_search_bar_open_gallery\">打開畫廊</string>\n    <string name=\"toplist_yesterday\">過去一天</string>\n    <string name=\"toplist_pastmonth\">過去一月</string>\n    <string name=\"toplist_pastyear\">過去一年</string>\n    <string name=\"toplist_alltime\">有史以來</string>\n    <string name=\"gallery_list_empty_hit\">什麼都沒有找到</string>\n    <string name=\"gallery_list_empty_hit_subscription\">請至 設置-&gt;EH-&gt;我的標籤 訂閲標籤</string>\n    <!-- Gallery List Action -->\n    <string name=\"read\">閲讀</string>\n    <string name=\"download\">下載</string>\n    <string name=\"delete_downloads\">刪除下載</string>\n    <string name=\"add_to_favourites\">收藏</string>\n    <string name=\"remove_from_favourites\">移除收藏</string>\n    <string name=\"download_move_dialog_title\">移動</string>\n    <string name=\"default_download_label_name\">默認</string>\n    <string name=\"remember_download_label\">記住下載標籤</string>\n    <string name=\"added_to_download_list\">已添加至下載列表</string>\n    <string name=\"download_remove_dialog_title\">移除下載項</string>\n    <string name=\"download_remove_dialog_message\">從下載列表移除 %s ？</string>\n    <string name=\"download_remove_dialog_check_text\">同時刪除圖片文件</string>\n    <string name=\"add_favorites_dialog_title\">添加收藏</string>\n    <string name=\"local_favorites\">本地收藏</string>\n    <string name=\"remember_favorite_collection\">記住收藏夾</string>\n    <string name=\"add_favorite_note_dialog_title\">添加收藏備註</string>\n    <string name=\"favorite_note\">收藏備註</string>\n    <string name=\"favorite_note_never_show\">不再顯示此窗口</string>\n    <string name=\"add_to_favorite_success\">已添加至收藏</string>\n    <string name=\"add_to_favorite_failure\">添加收藏失敗</string>\n    <string name=\"remove_from_favorite_success\">移除收藏成功</string>\n    <string name=\"remove_from_favorite_failure\">移除收藏失敗</string>\n    <!-- Gallery List Fab Action -->\n    <string name=\"go_to\">跳頁</string>\n    <string name=\"go_to_gid\">GID，留空跳轉末頁</string>\n    <string name=\"go_to_hint\">第 %1$d 頁，共 %2$d 頁</string>\n    <!-- Gallery List Quick Search -->\n    <string name=\"quick_search\">快速搜索</string>\n    <string name=\"quick_search_tip\">點擊“+”來添加快速搜索</string>\n    <string name=\"readme\">讀我</string>\n    <string name=\"add_quick_search_tip\">畫廊列表的狀態將被保存為快速搜索。如果您希望保存搜索面板的狀態，請先執行搜索。</string>\n    <string name=\"add_quick_search_dialog_title\">添加快速搜索</string>\n    <string name=\"save_progress\">同時保存閲讀進度</string>\n    <string name=\"name_is_empty\">名稱為空</string>\n    <string name=\"duplicate_name\">已存在相同名稱</string>\n    <string name=\"image_search_not_quick_search\">無法添加圖片搜索為快速搜索</string>\n    <string name=\"duplicate_quick_search\">已存在相同的快速搜索，名稱為“%s”。</string>\n    <string name=\"delete\">刪除</string>\n    <string name=\"delete_quick_search_title\">刪除快速搜索</string>\n    <string name=\"delete_quick_search_message\">刪除“%s”？</string>\n\n    <!-- Gallery Search -->\n    <string name=\"search_normal\">普通搜索</string>\n    <string name=\"search_normal_search\">一般搜索</string>\n    <string name=\"search_subscription_search\">訂閲搜索</string>\n    <string name=\"search_specify_uploader\">指定上傳者</string>\n    <string name=\"search_specify_tag\">指定標籤</string>\n    <string name=\"search_tip\">一般搜索：就是搜索。\\n\\n訂閲搜素：在訂閲中搜索。\\n\\n指定上傳者：列出該上傳者上傳的畫廊。其他選項會被忽略。\\n\\n指定標籤：列出含有該標籤的畫廊。其他選項會被忽略。</string>\n    <string name=\"search_enable_advance\">啓用高級選項</string>\n    <string name=\"search_advance\">高級選項</string>\n    <string name=\"search_sh\">僅顯示被刪除的畫廊</string>\n    <string name=\"search_sto\">僅顯示有種子的畫廊</string>\n    <string name=\"search_sr\">最低評分：</string>\n    <string name=\"star_2\">2 星</string>\n    <string name=\"star_3\">3 星</string>\n    <string name=\"star_4\">4 星</string>\n    <string name=\"star_5\">5 星</string>\n    <string name=\"search_sp\">頁數：</string>\n    <string name=\"search_sp_to\">到</string>\n    <string name=\"search_sp_err0\">頁數最小值至多為 1000</string>\n    <string name=\"search_sp_err1\">頁數最大值至少為 10</string>\n    <string name=\"search_sp_err2\">頁數範圍差至少為 20</string>\n    <string name=\"search_sp_err3\">頁數範圍比至多為 0.5</string>\n    <string name=\"search_sf\">禁用排除項：</string>\n    <string name=\"search_sfl\">語言</string>\n    <string name=\"search_sfu\">上傳者</string>\n    <string name=\"search_sft\">標籤</string>\n    <string name=\"search_image\">圖片搜索</string>\n    <string name=\"select_image\">選擇圖片</string>\n    <string name=\"select_image_first\">請先選擇圖片</string>\n    <string name=\"keyword_search\">關鍵字搜索</string>\n    <string name=\"image_search\">圖片搜索</string>\n\n    <!-- Gallery Detail Scene -->\n    <string name=\"download_upgradeable\">可升級</string>\n    <string name=\"download_upgrade_existed\">目標畫廊已存在，請刪除後重試</string>\n    <string name=\"download_upgrade_service_failed\">由於系統限制，無法從後台啓動下載服務，請點擊確定開始下載</string>\n    <string name=\"read_from\">從第 %d 頁閲讀</string>\n    <plurals name=\"page_count\">\n        <item quantity=\"other\">%d 頁</item>\n    </plurals>\n    <string name=\"favored_times\">\\u2665 %d</string>\n    <string name=\"more_information\">查看更多信息</string>\n    <string name=\"newer_version_avaliable\">此畫廊有新版本可用</string>\n    <string name=\"newer_version_title\">%1$s, 添加於 %2$s</string>\n    <string name=\"favorited\">已收藏</string>\n    <string name=\"not_favorited\">未收藏</string>\n    <string name=\"share\">分享</string>\n    <string name=\"torrent_count\">種子 (%d)</string>\n    <string name=\"archive\">壓縮包</string>\n    <string name=\"similar_gallery\">相似畫廊</string>\n    <string name=\"no_tags\">暫無標籤</string>\n    <string name=\"no_comments\">暫無評論</string>\n    <string name=\"more_comment\">查看更多評論</string>\n    <string name=\"no_more_comments\">已顯示所有評論</string>\n    <string name=\"no_previews\">沒有預覽</string>\n    <string name=\"more_previews\">查看更多預覽</string>\n    <string name=\"no_more_previews\">已顯示所有預覽</string>\n    <!-- Gallery Actions -->\n    <string name=\"refresh\">刷新</string>\n    <string name=\"action_add_tag\">添加標籤</string>\n    <string name=\"action_add_tag_tip\">添加新標籤，用逗號「,」分割</string>\n    <string name=\"action_clear_image_cache\">清除圖片緩存</string>\n    <string name=\"action_image_cache_cleared\">已清除圖片緩存</string>\n    <string name=\"open_in_other_app\">在其他應用中打開</string>\n    <string name=\"filter_the_uploader\">屏蔽上傳者“%s”？</string>\n    <string name=\"filter_added\">已添加屏蔽項</string>\n    <string name=\"copy_trans\">複製翻譯</string>\n    <string name=\"show_definition\">查看定義</string>\n    <string name=\"filter_the_tag\">屏蔽標籤“%s”？</string>\n    <string name=\"tag_vote_up\">是的，這很正確</string>\n    <string name=\"tag_vote_down\">不，這不是</string>\n    <string name=\"tag_vote_up_cancel\">取消贊成</string>\n    <string name=\"tag_vote_down_cancel\">取消反對</string>\n    <string name=\"tag_vote_successfully\">投票成功</string>\n    <string name=\"vote_failed\">投票失敗</string>\n    <!-- Gallery Info -->\n    <string name=\"gallery_info\">畫廊信息</string>\n    <string name=\"header_key\">鍵</string>\n    <string name=\"header_value\">值</string>\n    <string name=\"key_url\">鏈接</string>\n    <string name=\"key_title\">標題</string>\n    <string name=\"key_title_jpn\">日文標題</string>\n    <string name=\"key_thumb\">縮略圖</string>\n    <string name=\"key_category\">分類</string>\n    <string name=\"key_uploader\">上傳者</string>\n    <string name=\"key_posted\">上傳時間</string>\n    <string name=\"key_parent\">父畫廊</string>\n    <string name=\"key_visible\">可見性</string>\n    <string name=\"key_language\">語言</string>\n    <string name=\"key_pages\">頁數</string>\n    <string name=\"key_size\">大小</string>\n    <string name=\"key_favorite_count\">收藏次數</string>\n    <string name=\"key_favorited\">是否收藏</string>\n    <string name=\"key_favorite_name\">收藏分類</string>\n    <string name=\"key_rating_count\">評價次數</string>\n    <string name=\"key_rating\">評分</string>\n    <string name=\"key_torrents\">種子個數</string>\n    <string name=\"key_torrent_url\">種子</string>\n    <string name=\"copied_to_clipboard\">已複製到剪切板</string>\n    <!-- Gallery Torrents -->\n    <string name=\"torrents\">種子</string>\n    <string name=\"no_torrents\">沒有種子</string>\n    <string name=\"download_torrent_started\">開始下載種子</string>\n    <string name=\"download_torrent_failure\">無法下載種子</string>\n    <!-- Gallery Archives -->\n    <string name=\"no_archives\">沒有壓縮包</string>\n    <string name=\"archive_original\">原始檔案</string>\n    <string name=\"archive_resample\">重採樣檔案</string>\n    <string name=\"archive_free\">免費</string>\n    <string name=\"current_funds\">資金：%1$s GP，%2$d Credits</string>\n    <string name=\"insufficient_funds\">GP 不足</string>\n    <string name=\"download_archive_started\">開始下載壓縮包</string>\n    <string name=\"download_archive_failure\">無法下載壓縮包</string>\n    <string name=\"download_archive_failure_no_hath\">下載壓縮包需要 H@H 客户端</string>\n    <!-- Gallery Rating -->\n    <string name=\"rate\">評分</string>\n    <string name=\"rate_successfully\">評分成功</string>\n    <string name=\"rate_failed\">評分失敗</string>\n    <string name=\"rating10\">根本把持不住</string>\n    <string name=\"rating9\">好極了</string>\n    <string name=\"rating8\">很棒</string>\n    <string name=\"rating7\">不錯</string>\n    <string name=\"rating6\">還行</string>\n    <string name=\"rating5\">一般般</string>\n    <string name=\"rating4\">不行</string>\n    <string name=\"rating3\">糟糕</string>\n    <string name=\"rating2\">瞎眼</string>\n    <string name=\"rating1\">快要窒息了</string>\n    <string name=\"rating0\">…</string>\n    <!-- Gallery Comments -->\n    <string name=\"gallery_comments\">畫廊評論</string>\n    <string name=\"no_one_comments_gallery\">暫時沒有評論</string>\n    <string name=\"comment_successfully\">評論成功</string>\n    <string name=\"comment_failed\">評論失敗</string>\n    <string name=\"comment_user_uploader\">%s （上傳者）</string>\n    <string name=\"last_edited\">上次修改時間：%s</string>\n    <string name=\"click_more_comments\">點擊加載更多評論</string>\n    <string name=\"copy_comment_text\">複製評論文字</string>\n    <string name=\"edit_comment\">修改評論</string>\n    <string name=\"edit_comment_successfully\">評論已修改</string>\n    <string name=\"edit_comment_failed\">評論修改失敗</string>\n    <string name=\"block_commenter\">屏蔽評論者</string>\n    <string name=\"filter_the_commenter\">屏蔽評論者“%s”？</string>\n    <string name=\"vote_up\">深表贊同</string>\n    <string name=\"cancel_vote_up\">不再深表贊同</string>\n    <string name=\"vote_down\">垃圾評論</string>\n    <string name=\"cancel_vote_down\">不是垃圾評論</string>\n    <string name=\"vote_up_successfully\">已深表贊同</string>\n    <string name=\"cancel_vote_up_successfully\">已不再深表贊同</string>\n    <string name=\"vote_down_successfully\">已欽定為垃圾評論</string>\n    <string name=\"cancel_vote_down_successfully\">已不再欽定為垃圾評論</string>\n    <string name=\"check_vote_status\">查看投票情況</string>\n    <string name=\"format_bold\"><b>加粗</b></string>\n    <string name=\"format_italic\"><i>傾斜</i></string>\n    <string name=\"format_strikethrough\"><strike>刪除線</strike></string>\n    <string name=\"format_underline\"><u>下劃線</u></string>\n    <string name=\"format_url\">鏈接</string>\n    <string name=\"format_plain\">純文本</string>\n    <!-- Gallery Previews -->\n    <string name=\"gallery_previews\">畫廊預覽</string>\n\n    <!-- Favorites Scene -->\n    <string name=\"collections\">收藏夾</string>\n    <string name=\"favorites_search_bar_hint\">搜索 %s</string>\n    <string name=\"favorites_title\">%s</string>\n    <string name=\"favorites_title_2\">%1$s - %2$s</string>\n    <string name=\"need_sign_in\">需要登錄</string>\n    <!-- Favorites Collection -->\n    <string name=\"default_favorites_collection\">默認收藏夾</string>\n    <string name=\"cloud_favorites\">雲端收藏</string>\n    <string name=\"let_me_select\">讓我選擇</string>\n    <!-- Favorites Fab Action -->\n    <string name=\"delete_favorites_dialog_title\">刪除收藏</string>\n    <string name=\"delete_favorites_dialog_message\">刪除 %d 項收藏？</string>\n    <string name=\"move_favorites_dialog_title\">移動收藏</string>\n\n    <!-- History Scene -->\n    <string name=\"no_history\">這裏是閲讀歷史</string>\n    <string name=\"clear_all\">全部清除</string>\n    <string name=\"clear_all_history\">清除所有閲讀歷史？</string>\n\n    <!-- Download Scene -->\n    <string name=\"scene_download_title\">下載 - %s</string>\n    <string name=\"no_download_info\">這裏是下載項目</string>\n    <string name=\"download_state_none\">未啓動</string>\n    <string name=\"download_state_wait\">等待中</string>\n    <string name=\"download_state_downloading\">下載中</string>\n    <string name=\"download_state_downloaded\">已下載</string>\n    <string name=\"download_state_failed\">失敗</string>\n    <string name=\"download_state_failed_2\">%d 未完成</string>\n    <string name=\"download_state_finish\">已完成</string>\n    <!-- Download Action Bar -->\n    <string name=\"download_filter\">篩選</string>\n    <string name=\"download_filter_title\">篩選標題</string>\n    <string name=\"download_start_all\">全部開始</string>\n    <string name=\"download_stop_all\">全部停止</string>\n    <string name=\"download_start_all_reversed\">全部開始（倒序）</string>\n    <string name=\"download_sort_by\">排序規則</string>\n    <string name=\"download_sort_added_time_desc\">新增時間（降序）</string>\n    <string name=\"download_sort_added_time_asc\">新增時間（升序）</string>\n    <string name=\"download_sort_title_asc\">畫廊標題（升序）</string>\n    <string name=\"download_sort_title_desc\">畫廊標題（降序）</string>\n    <string name=\"download_sort_author_asc\">畫廊作者（升序）</string>\n    <string name=\"download_sort_author_desc\">畫廊作者（降序）</string>\n    <string name=\"download_sort_name_asc\">畫廊名稱（升序）</string>\n    <string name=\"download_sort_name_desc\">畫廊名稱（降序）</string>\n    <string name=\"download_sort_category_asc\">畫廊分類（升序）</string>\n    <string name=\"download_sort_category_desc\">畫廊分類（降序）</string>\n    <string name=\"download_sort_shuffle\">隨機排序</string>\n    <string name=\"download_reset_reading_progress\">重置閲讀進度</string>\n    <string name=\"reset_reading_progress_message\">重置所有已下載畫廊的閲讀進度？</string>\n    <!-- Download Fab Action -->\n    <string name=\"download_remove_dialog_message_2\">從下載列表移除 %d 項？</string>\n    <!-- Download Labels -->\n    <string name=\"download_labels\">下載標籤</string>\n    <string name=\"download_all\">全部</string>\n    <string name=\"add\">添加</string>\n    <string name=\"new_label_title\">新標籤</string>\n    <string name=\"label_text_is_empty\">標籤文本為空</string>\n    <string name=\"label_text_is_invalid\">“全部”或“默認”是無效標籤</string>\n    <string name=\"label_text_exist\">標籤已存在</string>\n    <string name=\"rename_label\">重命名</string>\n    <string name=\"rename_label_title\">重命名標籤</string>\n    <string name=\"delete_label_title\">刪除標籤</string>\n    <string name=\"delete_label_message\">刪除“%s”？</string>\n    <string name=\"default_download_label\">默認下載標籤</string>\n    <!-- Download Service -->\n    <string name=\"download_service\">下載服務</string>\n    <string name=\"download_service_label\">EhViewer 下載服務</string>\n    <string name=\"download_speed_text_2\">%1$s，剩餘 %2$s</string>\n    <string name=\"stat_download_action_stop_all\">全部停止</string>\n    <string name=\"stat_509_alert_title\">509 警告</string>\n    <string name=\"stat_509_alert_text\">圖片配額已用盡。請停止下載，休息一下。</string>\n    <string name=\"stat_download_done_title\">下載結束</string>\n    <string name=\"stat_download_done_text_succeeded\">%d 項下載成功</string>\n    <string name=\"stat_download_done_text_failed\">%d 項下載失敗</string>\n    <string name=\"stat_download_done_text_mix\">%1$d 項下載成功，%2$d 項下載失敗</string>\n    <string name=\"stat_download_done_line_succeeded\">下載成功：%s</string>\n    <string name=\"stat_download_done_line_failed\">下載失敗：%s</string>\n\n    <!-- Settings -->\n    <string name=\"settings_eh\">EH</string>\n    <string name=\"settings_eh_account_name\">賬戶</string>\n    <string name=\"settings_eh_account_name_tourist\">遊客模式</string>\n    <string name=\"settings_eh_account_refresh_igneous\">刷新 igneous</string>\n    <string name=\"settings_eh_account_sign_out\">退出登錄</string>\n    <string name=\"settings_eh_account_sign_out_tip\">已退出登錄，稍後可重新登錄</string>\n    <string name=\"settings_eh_account_identity_cookies\">身份 Cookie 可用於登錄該賬號。\\n注意數據安全\\n\\n%s</string>\n    <string name=\"settings_eh_account_igneous_expire\">igneous 自動刷新：</string>\n    <string name=\"settings_eh_account_identity_cookies_copy\">複製</string>\n    <string name=\"settings_eh_gallery_site\">畫廊站點</string>\n    <string name=\"settings_eh_image_limits\">圖片配額</string>\n    <string name=\"settings_eh_image_limits_summary\">加載中…</string>\n    <string name=\"settings_eh_image_limits_summary_ip\">基於 IP 的限制：</string>\n    <string name=\"settings_eh_image_limits_summary_ip_ok\">未受限</string>\n    <string name=\"settings_eh_image_limits_summary_ip_restricted\">高解析度圖片受限</string>\n    <string name=\"settings_eh_image_limits_summary_acc\">當前已用：%1$d / %2$d</string>\n    <string name=\"settings_eh_unlock_cost\">您可以花費 %1$d GP 來解鎖 24 小時高解析度配額</string>\n    <string name=\"settings_eh_unlock\">解鎖</string>\n    <string name=\"settings_eh_reset_cost\">重置花費：%1$d GP</string>\n    <string name=\"settings_eh_reset\">重置</string>\n    <string name=\"settings_eh_reset_limits_succeed\">成功重置圖片配額</string>\n    <string name=\"settings_eh_u_config\">E-Hentai 設置</string>\n    <string name=\"settings_eh_u_config_summary\">E-Hentai 網站上的設置</string>\n    <string name=\"settings_eh_my_tags\">我的標籤</string>\n    <string name=\"settings_eh_my_tags_summary\">E-Hentai 網站上的我的標籤</string>\n    <string name=\"settings_eh_black_dark_theme\">純黑深色主題</string>\n    <string name=\"settings_eh_launch_page\">啓動頁</string>\n    <string name=\"settings_eh_list_mode\">列表模式</string>\n    <string name=\"settings_eh_list_mode_detail\">詳情</string>\n    <string name=\"settings_eh_list_mode_thumb\">縮略圖</string>\n    <string name=\"settings_eh_detail_size\">詳情模式下條目寬度</string>\n    <string name=\"settings_eh_list_tile_thumb_size\">詳情模式下縮略圖大小</string>\n    <string name=\"settings_eh_thumb_size\">縮略圖模式下縮略圖大小</string>\n    <string name=\"settings_eh_thumb_show_title\">縮略圖模式下顯示畫廊標題</string>\n    <string name=\"settings_eh_show_jpn_title\">顯示日文標題</string>\n    <string name=\"settings_eh_show_jpn_title_summary\">需同時在 E-Hentai 網站設置中啓用 Japanese Title</string>\n    <string name=\"settings_eh_show_gallery_pages\">顯示畫廊頁數</string>\n    <string name=\"settings_eh_show_gallery_pages_summary\">在畫廊列表中顯示頁數</string>\n    <string name=\"settings_eh_show_gallery_comments\">顯示畫廊評論</string>\n    <string name=\"settings_eh_show_gallery_comments_summary\">在畫廊詳情頁中顯示評論</string>\n    <string name=\"settings_eh_comment_threshold\">評論分數閾值</string>\n    <string name=\"settings_eh_comment_threshold_summary\">隱藏低於或等於此分數的評論（-101 停用）</string>\n    <string name=\"settings_eh_preview_num\">畫廊詳情頁預覽圖最大數量</string>\n    <string name=\"settings_eh_preview_size\">畫廊詳情頁預覽圖大小</string>\n    <string name=\"settings_eh_show_tag_translations\">顯示標籤翻譯</string>\n    <string name=\"settings_eh_show_tag_translations_summary\">顯示翻譯後的標籤而非原始文字（需花費時間來下載數據文件）</string>\n    <string name=\"settings_eh_tag_translations_source\">補充翻譯（由 EhTagTranslation 提供）</string>\n    <string name=\"settings_eh_tag_translations_source_url\">https://github.com/EhTagTranslation/Editor/wiki</string>\n    <string name=\"settings_eh_filter\">屏蔽列表</string>\n    <string name=\"settings_eh_filter_summary\">根據標題、上傳者、標籤、評論者屏蔽畫廊或評論</string>\n    <string name=\"settings_eh_metered_network_warning\">流量計費網絡警告</string>\n    <string name=\"settings_eh_request_news\">啓動時請求新聞頁面</string>\n    <string name=\"settings_eh_hide_hv_events\">隱藏 HV 事件通知</string>\n    <string name=\"settings_read\">閲讀</string>\n    <string name=\"settings_read_screen_rotation\">屏幕方向</string>\n    <string name=\"settings_read_screen_rotation_default\">系統默認</string>\n    <string name=\"settings_read_screen_rotation_portrait\">豎屏</string>\n    <string name=\"settings_read_screen_rotation_landscape\">橫屏</string>\n    <string name=\"settings_read_screen_rotation_sensor\">自動旋轉</string>\n    <string name=\"settings_read_reading_direction\">閲讀方向</string>\n    <string name=\"settings_read_reading_direction_left_to_right\">從左至右</string>\n    <string name=\"settings_read_reading_direction_right_to_Left\">從右至左</string>\n    <string name=\"settings_read_reading_direction_top_to_bottom\">從上至下</string>\n    <string name=\"settings_read_page_scaling\">頁面縮放</string>\n    <string name=\"settings_read_page_scaling_actual_size\">原始尺寸</string>\n    <string name=\"settings_read_page_scaling_fit_to_width\">匹配寬度</string>\n    <string name=\"settings_read_page_scaling_fit_to_height\">匹配長度</string>\n    <string name=\"settings_read_page_scaling_fit_to_screen\">匹配屏幕</string>\n    <string name=\"settings_read_page_scaling_fixed_scale\">固定縮放</string>\n    <string name=\"settings_read_start_position\">開頁位置</string>\n    <string name=\"settings_read_start_position_top_left\">左上角</string>\n    <string name=\"settings_read_start_position_top_right\">右上角</string>\n    <string name=\"settings_read_start_position_bottom_left\">左下角</string>\n    <string name=\"settings_read_start_position_bottom_right\">右下角</string>\n    <string name=\"settings_read_start_position_center\">中心</string>\n    <string name=\"settings_read_theme\">色彩主題</string>\n    <string name=\"settings_read_theme_follow_app\">跟隨應用</string>\n    <string name=\"settings_read_theme_dark\">暗色</string>\n    <string name=\"settings_read_theme_light\">亮色</string>\n    <string name=\"settings_read_keep_screen_on\">屏幕常亮</string>\n    <string name=\"settings_read_show_clock\">顯示時鐘</string>\n    <string name=\"settings_read_show_progress\">顯示進度</string>\n    <string name=\"settings_read_show_battery\">顯示電量</string>\n    <string name=\"settings_read_show_page_interval\">顯示頁面間隔</string>\n    <string name=\"settings_read_turn_page_interval\">自動翻頁間隔（秒）</string>\n    <string name=\"settings_read_volume_page\">使用音量鍵翻頁</string>\n    <string name=\"settings_read_volume_page_interval\">音量鍵翻頁間隔</string>\n    <string name=\"settings_read_reverse_volume\">翻轉音量鍵</string>\n    <string name=\"settings_read_reading_fullscreen\">全屏</string>\n    <string name=\"settings_read_custom_screen_lightness\">自定義屏幕亮度</string>\n    <string name=\"settings_read_screen_lightness\">屏幕亮度</string>\n    <string name=\"settings_download\">下載</string>\n    <string name=\"settings_download_download_location\">下載路徑</string>\n    <string name=\"settings_download_pick_new_location\">選擇新路徑</string>\n    <string name=\"settings_download_reset_location\">恢復默認路徑</string>\n    <string name=\"settings_download_invalid_download_location\">無效的下載路徑</string>\n    <string name=\"settings_download_cant_get_download_location\">無法獲取下載路徑</string>\n    <string name=\"settings_download_media_scan\">允許媒體掃描</string>\n    <string name=\"settings_download_media_scan_summary_on\">請避免他人翻看您的圖庫應用</string>\n    <string name=\"settings_download_media_scan_summary_off\">大多數圖庫應用將不會顯示下載目錄中的圖片</string>\n    <string name=\"settings_download_concurrency\">並發下載數</string>\n    <string name=\"settings_download_concurrency_summary\">最多同時下載 %s 張圖片</string>\n    <string name=\"settings_download_download_delay\">下載延時</string>\n    <string name=\"settings_download_download_delay_summary\">每次下載延時 %s 毫秒</string>\n    <string name=\"settings_download_download_timeout\">下載超時（秒）</string>\n    <string name=\"settings_download_preload_image\">預載圖片</string>\n    <string name=\"settings_download_preload_image_summary\">向後預載 %s 張圖片</string>\n    <string name=\"settings_download_download_origin_image\">加載原圖</string>\n    <string name=\"settings_download_download_origin_image_summary\">%s，警告！可能需要消耗 GP</string>\n    <string name=\"settings_download_download_origin_image_never\">從不啓用</string>\n    <string name=\"settings_download_download_origin_image_force\">總是啓用</string>\n    <string name=\"settings_download_download_origin_image_only\">僅下載時啓用</string>\n    <string name=\"settings_download_task_confirm\">確定要執行操作？</string>\n    <string name=\"settings_download_restore_download_items\">恢復下載項</string>\n    <string name=\"settings_download_restore_download_items_summary\">恢復下載目錄裏的所有下載項</string>\n    <string name=\"settings_download_restore_not_found\">未找到可恢復下載項</string>\n    <string name=\"settings_download_restore_failed\">恢復失敗</string>\n    <string name=\"settings_download_restore_successfully\">成功恢復 %d 項</string>\n    <string name=\"settings_download_clean_redundancy\">清理下載冗餘</string>\n    <string name=\"settings_download_clean_redundancy_summary\">清理下載目錄中不在下載列表裏的圖片文件</string>\n    <string name=\"settings_download_clean_redundancy_no_redundancy\">未發現冗餘</string>\n    <string name=\"settings_download_clean_redundancy_done\">完成冗餘清理，共清理 %d 項</string>\n    <string name=\"settings_privacy\">隱私</string>\n    <string name=\"settings_privacy_pattern_protection_title\">圖案保護</string>\n    <string name=\"settings_privacy_pattern_protection_not_set\">未設置圖案保護</string>\n    <string name=\"settings_privacy_pattern_protection_set\">已設置圖案保護</string>\n    <string name=\"settings_privacy_secure\">不允許屏幕抓取</string>\n    <string name=\"settings_privacy_secure_summary\">啓用後，將不能截取該應用的屏幕截圖。同時，也不會在系統任務切換器中顯示該應用的內容預覽。請重新啓動應用以使此更改生效</string>\n    <string name=\"settings_privacy_clear_search_history\">清除裝置上的搜尋記錄</string>\n    <string name=\"settings_privacy_clear_search_history_summary\">移除您在此裝置上執行過的搜尋查詢的記錄</string>\n    <string name=\"settings_privacy_clear_search_history_cleared\">已清除搜尋記錄</string>\n    <string name=\"settings_advanced\">高級</string>\n    <string name=\"settings_advanced_save_parse_error_body\">解析失敗時保存頁面內容</string>\n    <string name=\"settings_advanced_save_parse_error_body_summary\">頁面內容可能含有隱私敏感信息</string>\n    <string name=\"settings_advanced_save_crash_log\">應用崩潰時保存錯誤日誌</string>\n    <string name=\"settings_advanced_save_crash_log_summary\">錯誤日誌可以幫助開發者修正問題</string>\n    <string name=\"settings_advanced_dump_logcat\">導出日誌</string>\n    <string name=\"settings_advanced_dump_logcat_summary\">保存日誌至外置存儲器</string>\n    <string name=\"settings_advanced_dump_logcat_failed\">導出日誌失敗</string>\n    <string name=\"settings_advanced_dump_logcat_to\">已保存日誌至 %s</string>\n    <string name=\"settings_advanced_read_cache_size\">閲讀緩存大小</string>\n    <string name=\"settings_advanced_app_language_title\">App 界面語言</string>\n    <string name=\"settings_advanced_proxy\">代理</string>\n    <string name=\"settings_advanced_backup_favorite\">備份收藏列表</string>\n    <string name=\"settings_advanced_backup_favorite_summary\">備份雲端收藏列表到本地</string>\n    <string name=\"settings_advanced_backup_favorite_start\">正在備份收藏列表 %s</string>\n    <string name=\"settings_advanced_backup_favorite_nothing\">沒有可以備份的收藏列表</string>\n    <string name=\"settings_advanced_backup_favorite_success\">備份收藏列表成功</string>\n    <string name=\"settings_advanced_backup_favorite_failed\">備份收藏列表失敗</string>\n    <string name=\"settings_advanced_export_data\">導出數據</string>\n    <string name=\"settings_advanced_export_data_summary\">保存數據至外置存儲器，例如下載列表，快速搜索列表</string>\n    <string name=\"settings_advanced_export_data_to\">已導出數據至 %s</string>\n    <string name=\"settings_advanced_export_data_failed\">導出數據失敗</string>\n    <string name=\"settings_advanced_import_data\">導入數據</string>\n    <string name=\"settings_advanced_import_data_summary\">從外置存儲器導入數據</string>\n    <string name=\"settings_advanced_import_data_successfully\">導入數據成功</string>\n    <string name=\"settings_advanced_import_data_cant_read\">無法讀取文件</string>\n    <string name=\"settings_advanced_open_by_default\">默認打開</string>\n    <string name=\"settings_about\">關於</string>\n    <string name=\"settings_about_declaration_summary\">EhViewer 與 E-Hentai.org 無任何聯繫</string>\n    <string name=\"settings_about_author\">作者</string>\n    <string name=\"settings_about_issues\">常見問題</string>\n    <string name=\"settings_about_latest_release\">最新版本</string>\n    <string name=\"settings_about_source\">源碼</string>\n    <string name=\"settings_about_version\">版本號</string>\n    <string name=\"settings_about_build_time\">構建於 %s</string>\n    <!-- UConfig Activity -->\n    <string name=\"u_config\">EHentai 設置</string>\n    <string name=\"apply\">應用</string>\n    <string name=\"apply_tip\">點擊右上角的對勾來保存設置</string>\n    <!-- My Tags Activity -->\n    <string name=\"my_tags\">我的標籤</string>\n    <!-- Filter Activity -->\n    <string name=\"filter\">屏蔽列表</string>\n    <string name=\"tip\">提示</string>\n    <string name=\"filter_tip\">該屏蔽系統會在 EHentai 網站屏蔽系統的基礎上繼續屏蔽畫廊。\\n\\n標題屏蔽項：排除標題含有該關鍵字的畫廊。\\n\\n上傳者屏蔽項：排除該上傳者上傳的畫廊。\\n\\n標籤屏蔽項：排除包含該標籤的畫廊，這會使獲取畫廊列表花費更多時間。\\n\\n命名空間屏蔽項：排除包含該命名空間的畫廊，這會使獲取畫廊列表花費更多時間。\\n\\n評論者屏蔽項：排除該評論者發佈的評論。\\n\\n評論屏蔽項：排除匹配該正則表達式的評論。</string>\n    <string name=\"no_filter\">這裏是屏蔽列表</string>\n    <string name=\"add_filter\">添加屏蔽項</string>\n    <string name=\"filter_title\">標題</string>\n    <string name=\"filter_uploader\">上傳者</string>\n    <string name=\"filter_tag\">標籤</string>\n    <string name=\"filter_tag_namespace\">命名空間</string>\n    <string name=\"filter_commenter\">評論者</string>\n    <string name=\"filter_comment\">評論</string>\n    <string name=\"filter_text\">屏蔽項文本</string>\n    <string name=\"delete_filter\">刪除屏蔽項“%s”？</string>\n    <!-- Set Security -->\n    <string name=\"set_pattern_protection\">設置圖案保護</string>\n    <string name=\"set_pattern_protection_tip\">繪製圖案來設置圖案保護\\n留空來取消圖案保護</string>\n    <string name=\"enable_biometric\">允許使用生物特徵解鎖</string>\n    <string name=\"set\">設置</string>\n    <!-- Languages -->\n    <string name=\"app_language_system\">系統語言（默認）</string>\n    <!-- Proxy -->\n    <string name=\"proxy_direct\">直接連接</string>\n    <string name=\"proxy_system\">系統代理</string>\n    <string name=\"proxy_host_or_ip\">主機或 IP</string>\n    <string name=\"proxy_port\">端口</string>\n    <string name=\"proxy_invalid_port\">無效的端口</string>\n\n    <!-- Errors -->\n    <string name=\"error_bad_status_code\">錯誤狀態碼：%d</string>\n    <string name=\"error_timeout\">超時</string>\n    <string name=\"error_unknown_host\">未知主機</string>\n    <string name=\"error_redirection\">太多重定向</string>\n    <string name=\"error_socket\">網絡錯誤</string>\n    <string name=\"error_unknown\">奇怪的錯誤</string>\n    <string name=\"error_cant_find_activity\">找不到相應的應用</string>\n    <string name=\"error_cannot_parse_the_url\">無法解析鏈接</string>\n    <string name=\"error_decoding_failed\">解碼失敗</string>\n    <string name=\"error_empty\">空</string>\n    <string name=\"error_reading_failed\">讀取失敗</string>\n    <string name=\"error_out_of_range\">越界</string>\n    <string name=\"error_write_failed\">寫入失敗</string>\n    <string name=\"error_parse_error\">解析失敗</string>\n    <string name=\"error_invalid_url\">無效鏈接</string>\n    <string name=\"error_get_ptoken_error\">獲取 pToken 錯誤</string>\n    <string name=\"error_cant_create_temp_file\">無法創建臨時文件</string>\n    <string name=\"error_cant_save_image\">無法保存圖片</string>\n    <string name=\"error_invalid_number\">非法數字</string>\n    <string name=\"error_please_login_first\">請先登錄</string>\n    <string name=\"error_cannot_find_gallery\">找不到畫廊</string>\n    <string name=\"error_something_wrong_happened\">被玩壞了</string>\n    <string name=\"kokomade_tip\">設置-&gt;Eh-&gt;畫廊站點-&gt;E-Hentai</string>\n    <string name=\"no_browser_installed\">請安裝一個瀏覽器。</string>\n    <string name=\"cloudflare_bypass_failed\">未能繞過 Cloudflare 驗證</string>\n    <string name=\"open_in_webview\">在 WebView 中打開鏈接以通過驗證？</string>\n    <string name=\"information_webview_outdated\">請更新 WebView 以獲得更佳的相容性</string>\n\n    <!-- Readable Time -->\n    <string name=\"from_the_future\">來自未來</string>\n    <string name=\"just_now\">剛剛</string>\n    <plurals name=\"some_minutes_ago\">\n        <item quantity=\"other\">%d 分鐘前</item>\n    </plurals>\n    <plurals name=\"some_hours_ago\">\n        <item quantity=\"other\">%d 小時前</item>\n    </plurals>\n    <string name=\"yesterday\">昨天</string>\n    <string name=\"some_days_ago\">%d 天前</string>\n    <plurals name=\"second\">\n        <item quantity=\"other\">秒</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">分鐘</item>\n    </plurals>\n    <plurals name=\"hour\">\n        <item quantity=\"other\">小時</item>\n    </plurals>\n    <plurals name=\"day\">\n        <item quantity=\"other\">天</item>\n    </plurals>\n    <plurals name=\"year\">\n        <item quantity=\"other\">年</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/bools.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2019 Hippo Seven\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<resources>\n\n    <bool name=\"tag_translatable\">true</bool>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<resources>\n    <!-- Login Scene -->\n    <string name=\"app_waring\">本應用程式中的內容均來自網際網路，部分內容可能對您的生理及心理造成難以恢復的傷害。本應用程式的作者不會對由其所造成的任何影響負責。\\n未成年人應在監護人指導下使用本應用程式。\\n繼續使用即代表您同意上述條款。</string>\n    <string name=\"username\">使用者名稱</string>\n    <string name=\"password\">密碼</string>\n    <string name=\"sign_in\">登入</string>\n    <string name=\"register\">註冊</string>\n    <string name=\"sign_in_via_webview\">透過網頁登入</string>\n    <string name=\"sign_in_via_cookies\">透過 Cookie 登入</string>\n    <string name=\"tourist_mode\">訪客模式</string>\n    <string name=\"error_username_cannot_empty\">使用者名稱欄位不能為空</string>\n    <string name=\"error_password_cannot_empty\">密碼欄位不能為空</string>\n    <string name=\"sign_in_failed\">登入失敗</string>\n    <string name=\"sign_in_failed_tip\">若持續出錯，請嘗試“透過網頁登入”。</string>\n    <string name=\"sign_in_failed_tip_2\">若您確認您的 Cookie 為正確，也可以選擇忽略錯誤並繼續，但可能會導致部分問題。</string>\n    <string name=\"ignore\">忽略</string>\n    <string name=\"get_it\">知道了</string>\n    <!-- Cookie Sign In Scene -->\n    <string name=\"cookie_explain\">Cookie 是儲存在瀏覽器裡的一小塊資料。如果您不清楚 Cookie 是什麼或者如何取得 Cookie，最好去查一下。\\n\\n請輸入特定的 Cookie 來登入。</string>\n    <string name=\"from_clipboard\">從剪貼板匯入</string>\n    <string name=\"text_is_empty\">文字是空的</string>\n    <string name=\"from_clipboard_error\">未在剪貼板中發現 Cookies。</string>\n\n    <!-- Select Site Scene -->\n    <string name=\"select_scene\">你想選擇哪個圖庫站台？</string>\n    <string name=\"select_scene_explain\">E-Hentai：對任何人開放\\nExHentai：僅對登入使用者開放</string>\n\n    <!-- Guide -->\n    <string name=\"guide_gallery_left\">翻頁</string>\n    <string name=\"guide_gallery_right\">翻頁</string>\n    <string name=\"guide_gallery_menu\">選單</string>\n    <string name=\"guide_gallery_progress\">進度條</string>\n    <string name=\"guide_gallery_long_click\">長按以開啟頁面選單</string>\n\n    <!-- Main Activity -->\n    <string name=\"metered_network_warning\">正在使用流量計費網路</string>\n    <string name=\"waring\">警告</string>\n    <string name=\"invalid_download_location\">下載路徑似乎沒辦法用。請重新設定下載路徑。</string>\n    <string name=\"press_twice_exit\">再按一次離開</string>\n    <string name=\"clipboard_gallery_url_snack_message\">剪下板裡有圖庫連結</string>\n    <string name=\"clipboard_gallery_url_snack_action\">檢視</string>\n    <string name=\"please_wait\">請稍候…</string>\n    <string name=\"app_link_not_verified_title\">應用連結未驗證</string>\n    <string name=\"app_link_not_verified_message\">對於 Android 12 及更新的版本，您需要手動增加連結到已驗證連結才能在 EhViewer 中打開 E-Hentai 連結。</string>\n    <string name=\"open_settings\">打開設定</string>\n    <string name=\"dont_show_again\">不再顯示</string>\n\n    <!-- Gallery Activity -->\n    <string name=\"archive_need_passwd\">封存文件需要密碼</string>\n    <string name=\"archive_passwd\">請輸入密碼</string>\n    <string name=\"passwd_cannot_be_empty\">密碼不能為空</string>\n    <string name=\"passwd_wrong\">密碼錯誤</string>\n    <string name=\"page_menu_title\">第 %d 頁</string>\n    <string name=\"page_menu_refresh\">@string/refresh</string>\n    <string name=\"page_menu_share\">@string/share</string>\n    <string name=\"page_menu_save\">儲存</string>\n    <string name=\"page_menu_save_to\">儲存到…</string>\n    <string name=\"page_menu_download_original\">下載原圖</string>\n    <string name=\"gallery_menu_title\">選單</string>\n    <string name=\"share_image\">分享圖片</string>\n    <string name=\"start_download_original\">請稍候</string>\n    <string name=\"image_saved\">已將圖片儲存至 %s</string>\n\n    <!-- Navigation Bar -->\n    <string name=\"homepage\">首頁</string>\n    <string name=\"subscription\">訂閱</string>\n    <string name=\"whats_hot\">熱門</string>\n    <string name=\"toplist\">排行</string>\n    <string name=\"favourite\">收藏</string>\n    <string name=\"history\">歷史</string>\n    <string name=\"downloads\">下載</string>\n    <string name=\"settings\">設定</string>\n\n    <!-- Gallery List Scene -->\n    <string name=\"search\">搜尋</string>\n    <string name=\"gallery_list_search_bar_hint_exhentai\">在 ExHentai 中搜尋</string>\n    <string name=\"gallery_list_search_bar_hint_e_hentai\">在 E-Hentai 中搜尋</string>\n    <string name=\"gallery_list_search_bar_open_gallery\">開啟圖庫</string>\n    <string name=\"toplist_yesterday\">過去一天</string>\n    <string name=\"toplist_pastmonth\">過去一月</string>\n    <string name=\"toplist_pastyear\">過去一年</string>\n    <string name=\"toplist_alltime\">從始至終</string>\n    <string name=\"gallery_list_empty_hit\">什麼都沒找到</string>\n    <string name=\"gallery_list_empty_hit_subscription\">請至 設定-&gt;EH-&gt;我的標籤 訂閱標籤</string>\n    <!-- Gallery List Action -->\n    <string name=\"read\">閱讀</string>\n    <string name=\"download\">下載</string>\n    <string name=\"delete_downloads\">刪除下載</string>\n    <string name=\"add_to_favourites\">收藏</string>\n    <string name=\"remove_from_favourites\">移除收藏</string>\n    <string name=\"download_move_dialog_title\">移動</string>\n    <string name=\"default_download_label_name\">預設</string>\n    <string name=\"remember_download_label\">記住下載標籤</string>\n    <string name=\"added_to_download_list\">已新增至下載列表</string>\n    <string name=\"download_remove_dialog_title\">移除下載佇列</string>\n    <string name=\"download_remove_dialog_message\">將 %s 從下載佇列中移除嗎?</string>\n    <string name=\"download_remove_dialog_check_text\">同時刪除圖檔</string>\n    <string name=\"add_favorites_dialog_title\">新增到收藏裡</string>\n    <string name=\"local_favorites\">本機收藏</string>\n    <string name=\"remember_favorite_collection\">記住常用收藏</string>\n    <string name=\"add_favorite_note_dialog_title\">記住常用收藏</string>\n    <string name=\"favorite_note\">收藏備註</string>\n    <string name=\"favorite_note_never_show\">不再顯示此窗口</string>\n    <string name=\"add_to_favorite_success\">已新增至收藏中</string>\n    <string name=\"add_to_favorite_failure\">無法新增至收藏</string>\n    <string name=\"remove_from_favorite_success\">已從收藏中移除</string>\n    <string name=\"remove_from_favorite_failure\">無法從收藏中移除</string>\n    <!-- Gallery List Fab Action -->\n    <string name=\"go_to\">跳到</string>\n    <string name=\"go_to_gid\">GID，留空跳轉末頁</string>\n    <string name=\"go_to_hint\">第 %1$d 頁，共 %2$d 頁</string>\n    <!-- Gallery List Quick Search -->\n    <string name=\"quick_search\">快速搜尋</string>\n    <string name=\"quick_search_tip\">點擊“+”以新增快速搜尋</string>\n    <string name=\"readme\">ReadMe</string>\n    <string name=\"add_quick_search_tip\">圖庫列表的狀態將被儲存為快速搜尋。如果你想儲存搜尋面板的狀態，請先進行搜尋。</string>\n    <string name=\"add_quick_search_dialog_title\">新增快速搜尋</string>\n    <string name=\"save_progress\">同時儲存閱讀進度</string>\n    <string name=\"name_is_empty\">名稱是空的</string>\n    <string name=\"duplicate_name\">已經由重複的名稱了</string>\n    <string name=\"image_search_not_quick_search\">無法將圖片搜尋加入快速搜尋</string>\n    <string name=\"duplicate_quick_search\">已存在相同的快速搜尋，名稱為“%s”。</string>\n    <string name=\"delete\">刪除</string>\n    <string name=\"delete_quick_search_title\">刪除快速搜尋</string>\n    <string name=\"delete_quick_search_message\">刪除“%s”嗎？</string>\n\n    <!-- Gallery Search -->\n    <string name=\"search_normal\">一般搜尋</string>\n    <string name=\"search_normal_search\">一般搜尋</string>\n    <string name=\"search_subscription_search\">訂閱搜尋</string>\n    <string name=\"search_specify_uploader\">指定上傳者</string>\n    <string name=\"search_specify_tag\">指定標籤</string>\n    <string name=\"search_tip\">一般搜尋：就是搜尋。\\n\\n訂閱搜尋：在訂閱中搜尋。\\n\\n指定上傳者：列出該上傳者上傳的圖庫。其他選項會被忽略。\\n\\n指定標籤：列出含有該標籤的圖庫。其他選項會被忽略。</string>\n    <string name=\"search_enable_advance\">啟用進階選項</string>\n    <string name=\"search_advance\">進階選項</string>\n    <string name=\"search_sh\">只顯示已被移除的圖庫</string>\n    <string name=\"search_sto\">只顯示有種子的圖庫</string>\n    <string name=\"search_sr\">最低評分：</string>\n    <string name=\"star_2\">2 星</string>\n    <string name=\"star_3\">3 星</string>\n    <string name=\"star_4\">4 星</string>\n    <string name=\"star_5\">5 星</string>\n    <string name=\"search_sp\">頁數：</string>\n    <string name=\"search_sp_to\">到</string>\n    <string name=\"search_sp_err0\">頁數最小值至多為 1000</string>\n    <string name=\"search_sp_err1\">頁數最大值至少為 10</string>\n    <string name=\"search_sp_err2\">頁數範圍差至少為 20</string>\n    <string name=\"search_sp_err3\">頁數範圍比至多為 0.5</string>\n    <string name=\"search_sf\">停用排除項：</string>\n    <string name=\"search_sfl\">語言</string>\n    <string name=\"search_sfu\">上傳者</string>\n    <string name=\"search_sft\">標籤</string>\n    <string name=\"search_image\">圖片搜尋</string>\n    <string name=\"select_image\">選擇圖片</string>\n    <string name=\"select_image_first\">請先選擇圖片</string>\n    <string name=\"keyword_search\">關鍵字搜尋</string>\n    <string name=\"image_search\">圖片搜尋</string>\n\n    <!-- Gallery Detail Scene -->\n    <string name=\"download_upgradeable\">可升級</string>\n    <string name=\"download_upgrade_existed\">目標圖庫已存在，請刪除後重試</string>\n    <string name=\"download_upgrade_service_failed\">由於系統限制，無法從後臺啟動下載服務，請點選確定開始下載</string>\n    <string name=\"read_from\">從第 %d 頁閱讀</string>\n    <plurals name=\"page_count\">\n        <item quantity=\"other\">%d 頁</item>\n    </plurals>\n    <string name=\"favored_times\">\\u2665 %d</string>\n    <string name=\"more_information\">更多資訊</string>\n    <string name=\"newer_version_avaliable\">此圖庫有新版本可以使用</string>\n    <string name=\"newer_version_title\">%1$s, 增加於 %2$s</string>\n    <string name=\"favorited\">已收藏</string>\n    <string name=\"not_favorited\">未收藏</string>\n    <string name=\"share\">分享</string>\n    <string name=\"torrent_count\">種子 (%d)</string>\n    <string name=\"archive\">壓縮檔</string>\n    <string name=\"similar_gallery\">類似的圖庫</string>\n    <string name=\"no_tags\">還沒有標籤</string>\n    <string name=\"no_comments\">沒有留言</string>\n    <string name=\"more_comment\">更多留言</string>\n    <string name=\"no_more_comments\">已顯示所有的留言了</string>\n    <string name=\"no_previews\">沒有預覽</string>\n    <string name=\"more_previews\">更多預覽</string>\n    <string name=\"no_more_previews\">已顯示所有預覽了</string>\n    <!-- Gallery Actions -->\n    <string name=\"refresh\">重新整理</string>\n    <string name=\"action_add_tag\">增加標籤</string>\n    <string name=\"action_add_tag_tip\">增加新標籤，用逗號「,」分割</string>\n    <string name=\"action_clear_image_cache\">清除圖片快取</string>\n    <string name=\"action_image_cache_cleared\">已清除圖片快取</string>\n    <string name=\"open_in_other_app\">在其他應用程式中開啟</string>\n    <string name=\"filter_the_uploader\">把上傳者“%s”加入濾除列表?</string>\n    <string name=\"filter_added\">已經新增篩選條件</string>\n    <string name=\"copy_trans\">複製翻譯</string>\n    <string name=\"show_definition\">檢視定義</string>\n    <string name=\"filter_the_tag\">隱藏標籤“%s”？</string>\n    <string name=\"tag_vote_up\">是的，這很正確</string>\n    <string name=\"tag_vote_down\">不，這不是</string>\n    <string name=\"tag_vote_up_cancel\">取消贊成</string>\n    <string name=\"tag_vote_down_cancel\">取消反對</string>\n    <string name=\"tag_vote_successfully\">投票成功</string>\n    <string name=\"vote_failed\">推噓失敗</string>\n    <!-- Gallery Info -->\n    <string name=\"gallery_info\">圖庫資訊</string>\n    <string name=\"header_key\">索引鍵</string>\n    <string name=\"header_value\">數值</string>\n    <string name=\"key_url\">網址</string>\n    <string name=\"key_title\">標題</string>\n    <string name=\"key_title_jpn\">日文標題</string>\n    <string name=\"key_thumb\">縮圖</string>\n    <string name=\"key_category\">分類</string>\n    <string name=\"key_uploader\">上傳者</string>\n    <string name=\"key_posted\">上傳時間</string>\n    <string name=\"key_parent\">上層圖庫</string>\n    <string name=\"key_visible\">可見</string>\n    <string name=\"key_language\">語言</string>\n    <string name=\"key_pages\">頁數</string>\n    <string name=\"key_size\">大小</string>\n    <string name=\"key_favorite_count\">收藏次數</string>\n    <string name=\"key_favorited\">是否收藏</string>\n    <string name=\"key_favorite_name\">收藏分類</string>\n    <string name=\"key_rating_count\">評分次數</string>\n    <string name=\"key_rating\">分數</string>\n    <string name=\"key_torrents\">種子數</string>\n    <string name=\"key_torrent_url\">種子連結</string>\n    <string name=\"copied_to_clipboard\">已複製到剪貼簿</string>\n    <!-- Gallery Torrents -->\n    <string name=\"torrents\">種子</string>\n    <string name=\"no_torrents\">沒有種子</string>\n    <string name=\"download_torrent_started\">開始下載種子</string>\n    <string name=\"download_torrent_failure\">無法下載種子</string>\n    <!-- Gallery Archives -->\n    <string name=\"no_archives\">沒有壓縮檔</string>\n    <string name=\"archive_original\">原始檔案</string>\n    <string name=\"archive_resample\">重新採樣檔案</string>\n    <string name=\"archive_free\">免費</string>\n    <string name=\"current_funds\">資金：%1$s GP，%2$d Credits</string>\n    <string name=\"insufficient_funds\">GP 不足</string>\n    <string name=\"download_archive_started\">開始下載壓縮檔</string>\n    <string name=\"download_archive_failure\">下載失敗</string>\n    <string name=\"download_archive_failure_no_hath\">下載壓縮檔需要 H@H 客戶端</string>\n    <!-- Gallery Rating -->\n    <string name=\"rate\">評分</string>\n    <string name=\"rate_successfully\">評分成功</string>\n    <string name=\"rate_failed\">評分失敗</string>\n    <string name=\"rating10\">太神啦!</string>\n    <string name=\"rating9\">根本把持不住</string>\n    <string name=\"rating8\">很棒</string>\n    <string name=\"rating7\">不錯</string>\n    <string name=\"rating6\">還好</string>\n    <string name=\"rating5\">普通</string>\n    <string name=\"rating4\">不行</string>\n    <string name=\"rating3\">超爛</string>\n    <string name=\"rating2\">快瞎了</string>\n    <string name=\"rating1\">快要窒息了</string>\n    <string name=\"rating0\">下去 !</string>\n    <!-- Gallery Comments -->\n    <string name=\"gallery_comments\">圖庫留言</string>\n    <string name=\"no_one_comments_gallery\">目前沒有留言</string>\n    <string name=\"comment_successfully\">留言成功</string>\n    <string name=\"comment_failed\">留言失敗</string>\n    <string name=\"comment_user_uploader\">%s （上傳者）</string>\n    <string name=\"last_edited\">上次修改時間：%s</string>\n    <string name=\"click_more_comments\">點選載入更多留言</string>\n    <string name=\"copy_comment_text\">複製留言文字</string>\n    <string name=\"edit_comment\">修改留言</string>\n    <string name=\"edit_comment_successfully\">留言已被修改</string>\n    <string name=\"edit_comment_failed\">修改留言失敗</string>\n    <string name=\"block_commenter\">隱藏評論者</string>\n    <string name=\"filter_the_commenter\">隱藏評論者“%s”？</string>\n    <string name=\"vote_up\">推</string>\n    <string name=\"cancel_vote_up\">收回推</string>\n    <string name=\"vote_down\">噓</string>\n    <string name=\"cancel_vote_down\">收回噓</string>\n    <string name=\"vote_up_successfully\">發出推了</string>\n    <string name=\"cancel_vote_up_successfully\">已經收回推了</string>\n    <string name=\"vote_down_successfully\">發出噓了</string>\n    <string name=\"cancel_vote_down_successfully\">已經收回噓了</string>\n    <string name=\"check_vote_status\">檢視推噓狀態</string>\n    <string name=\"format_bold\"><b>加粗</b></string>\n    <string name=\"format_italic\"><i>傾斜</i></string>\n    <string name=\"format_strikethrough\"><strike>刪除線</strike></string>\n    <string name=\"format_underline\"><u>下劃線</u></string>\n    <string name=\"format_url\">連結</string>\n    <string name=\"format_plain\">純文字</string>\n    <!-- Gallery Previews -->\n    <string name=\"gallery_previews\">圖庫預覽</string>\n\n    <!-- Favorites Scene -->\n    <string name=\"collections\">收藏夾</string>\n    <string name=\"favorites_search_bar_hint\">搜尋 %s</string>\n    <string name=\"favorites_title\">%s</string>\n    <string name=\"favorites_title_2\">%1$s - %2$s</string>\n    <string name=\"need_sign_in\">需要登入</string>\n    <!-- Favorites Collection -->\n    <string name=\"default_favorites_collection\">預設收藏夾</string>\n    <string name=\"cloud_favorites\">雲端收藏</string>\n    <string name=\"let_me_select\">讓我每次都選擇</string>\n    <!-- Favorites Fab Action -->\n    <string name=\"delete_favorites_dialog_title\">移除收藏</string>\n    <string name=\"delete_favorites_dialog_message\">將 %d 項收藏移除？</string>\n    <string name=\"move_favorites_dialog_title\">移動收藏</string>\n\n    <!-- History Scene -->\n    <string name=\"no_history\">歷史紀錄將會顯示在這裡</string>\n    <string name=\"clear_all\">全部清除</string>\n    <string name=\"clear_all_history\">清除所有歷史紀錄？</string>\n\n    <!-- Download Scene -->\n    <string name=\"scene_download_title\">下載 - %s</string>\n    <string name=\"no_download_info\">下載的物件將會顯示在此</string>\n    <string name=\"download_state_none\">閒置中</string>\n    <string name=\"download_state_wait\">等待中</string>\n    <string name=\"download_state_downloading\">下載中</string>\n    <string name=\"download_state_downloaded\">已下載</string>\n    <string name=\"download_state_failed\">下載失敗</string>\n    <string name=\"download_state_failed_2\">還有 %d 尚未完成</string>\n    <string name=\"download_state_finish\">大功告成</string>\n    <!-- Download Action Bar -->\n    <string name=\"download_filter\">過濾</string>\n    <string name=\"download_filter_title\">過濾標題</string>\n    <string name=\"download_start_all\">全部開始</string>\n    <string name=\"download_stop_all\">全部停止</string>\n    <string name=\"download_start_all_reversed\">全部開始（倒序）</string>\n    <string name=\"download_sort_by\">排序規則</string>\n    <string name=\"download_sort_added_time_desc\">加入時間（遞減）</string>\n    <string name=\"download_sort_added_time_asc\">加入時間（遞增）</string>\n    <string name=\"download_sort_title_asc\">圖庫標題（遞增）</string>\n    <string name=\"download_sort_title_desc\">圖庫標題（遞減）</string>\n    <string name=\"download_sort_author_asc\">圖庫作者（遞增）</string>\n    <string name=\"download_sort_author_desc\">圖庫作者（遞減）</string>\n    <string name=\"download_sort_name_asc\">圖庫名稱（遞增）</string>\n    <string name=\"download_sort_name_desc\">圖庫名稱（遞減）</string>\n    <string name=\"download_sort_category_asc\">圖庫分類（遞增）</string>\n    <string name=\"download_sort_category_desc\">圖庫分類（遞減）</string>\n    <string name=\"download_sort_shuffle\">隨機排序</string>\n    <string name=\"download_reset_reading_progress\">重置閱讀進度</string>\n    <string name=\"reset_reading_progress_message\">重置所有已下載圖庫的閱讀進度？</string>\n    <!-- Download Fab Action -->\n    <string name=\"download_remove_dialog_message_2\">從下載佇列中移除 %d 項任務嗎？</string>\n    <!-- Download Labels -->\n    <string name=\"download_labels\">下載標籤</string>\n    <string name=\"download_all\">全部</string>\n    <string name=\"add\">新增</string>\n    <string name=\"new_label_title\">新標籤</string>\n    <string name=\"label_text_is_empty\">標籤文字是空的</string>\n    <string name=\"label_text_is_invalid\">“預設”是無效的標籤</string>\n    <string name=\"label_text_exist\">標籤已存在</string>\n    <string name=\"rename_label\">重新命名</string>\n    <string name=\"rename_label_title\">重新命名標籤</string>\n    <string name=\"delete_label_title\">刪除標籤</string>\n    <string name=\"delete_label_message\">刪除“%s”？</string>\n    <string name=\"default_download_label\">預設下載標籤</string>\n    <!-- Download Service -->\n    <string name=\"download_service\">下載服務</string>\n    <string name=\"download_service_label\">EhViewer 下載服務</string>\n    <string name=\"download_speed_text_2\">%1$s，剩餘 %2$s</string>\n    <string name=\"stat_download_action_stop_all\">全部停止</string>\n    <string name=\"stat_509_alert_title\">509 警告</string>\n    <string name=\"stat_509_alert_text\">圖片流量已用盡。請停止下載，休息一下。</string>\n    <string name=\"stat_download_done_title\">下載結束</string>\n    <string name=\"stat_download_done_text_succeeded\">有 %d 項已成功下載</string>\n    <string name=\"stat_download_done_text_failed\">有 %d 項下載失敗</string>\n    <string name=\"stat_download_done_text_mix\">%1$d 項成功下載，%2$d 項下載失敗</string>\n    <string name=\"stat_download_done_line_succeeded\"> %s 下載成功</string>\n    <string name=\"stat_download_done_line_failed\"> %s 下載失敗</string>\n\n    <!-- Settings -->\n    <string name=\"settings_eh\">EH</string>\n    <string name=\"settings_eh_account_name\">帳號</string>\n    <string name=\"settings_eh_account_name_tourist\">訪客模式</string>\n    <string name=\"settings_eh_account_refresh_igneous\">重新整理 igneous</string>\n    <string name=\"settings_eh_account_sign_out\">登出</string>\n    <string name=\"settings_eh_account_sign_out_tip\">已登出，稍後可重新登入</string>\n    <string name=\"settings_eh_account_identity_cookies\">身份 Cookie 可用於登入該帳號。\\n請注意資料安全\\n\\n%s</string>\n    <string name=\"settings_eh_account_igneous_expire\">igneous 自動重新整理：</string>\n    <string name=\"settings_eh_account_identity_cookies_copy\">複製</string>\n    <string name=\"settings_eh_gallery_site\">圖庫站台</string>\n    <string name=\"settings_eh_image_limits\">圖片配額</string>\n    <string name=\"settings_eh_image_limits_summary\">載入中…</string>\n    <string name=\"settings_eh_image_limits_summary_ip\">基於 IP 的限制：</string>\n    <string name=\"settings_eh_image_limits_summary_ip_ok\">未受限</string>\n    <string name=\"settings_eh_image_limits_summary_ip_restricted\">高解析度圖片受限</string>\n    <string name=\"settings_eh_image_limits_summary_acc\">當前已用：%1$d / %2$d</string>\n    <string name=\"settings_eh_unlock_cost\">您可以花費 %1$d GP 來解鎖 24 小時高解析度配額</string>\n    <string name=\"settings_eh_unlock\">解鎖</string>\n    <string name=\"settings_eh_reset_cost\">重置花費：%1$d GP</string>\n    <string name=\"settings_eh_reset\">重置</string>\n    <string name=\"settings_eh_reset_limits_succeed\">成功重置圖片配額</string>\n    <string name=\"settings_eh_u_config\">E-Hentai 設定</string>\n    <string name=\"settings_eh_u_config_summary\">E-Hentai 網站上的設定</string>\n    <string name=\"settings_eh_my_tags\">我的標籤</string>\n    <string name=\"settings_eh_my_tags_summary\">E-Hentai 網站上的我的標籤</string>\n    <string name=\"settings_eh_black_dark_theme\">純黑深色主題</string>\n    <string name=\"settings_eh_launch_page\">啟動頁</string>\n    <string name=\"settings_eh_list_mode\">列表模式</string>\n    <string name=\"settings_eh_list_mode_detail\">詳細資料</string>\n    <string name=\"settings_eh_list_mode_thumb\">縮圖</string>\n    <string name=\"settings_eh_detail_size\">詳細資料模式下條目寬度</string>\n    <string name=\"settings_eh_list_tile_thumb_size\">詳細資料模式下縮圖大小</string>\n    <string name=\"settings_eh_thumb_size\">縮圖模式下縮圖大小</string>\n    <string name=\"settings_eh_thumb_show_title\">縮圖模式下顯示畫廊標題</string>\n    <string name=\"settings_eh_show_jpn_title\">顯示日文標題</string>\n    <string name=\"settings_eh_show_jpn_title_summary\">需同時在 E-Hentai 網站設定中啟用 Japanese Title</string>\n    <string name=\"settings_eh_show_gallery_pages\">顯示圖庫頁數</string>\n    <string name=\"settings_eh_show_gallery_pages_summary\">在圖庫列表中顯示頁數</string>\n    <string name=\"settings_eh_show_gallery_comments\">顯示畫廊評論</string>\n    <string name=\"settings_eh_show_gallery_comments_summary\">在畫廊詳情頁中顯示評論</string>\n    <string name=\"settings_eh_comment_threshold\">評論分數閾值</string>\n    <string name=\"settings_eh_comment_threshold_summary\">隱藏低於或等於此分數的評論（-101 停用）</string>\n    <string name=\"settings_eh_preview_num\">畫廊詳情頁預覽圖最大數量</string>\n    <string name=\"settings_eh_preview_size\">畫廊詳情頁預覽圖大小</string>\n    <string name=\"settings_eh_show_tag_translations\">顯示標籤翻譯</string>\n    <string name=\"settings_eh_show_tag_translations_summary\">顯示翻譯後的標籤而非原始文字（需花費時間來下載資料檔案）</string>\n    <string name=\"settings_eh_tag_translations_source\">補充翻譯（由 EhTagTranslation 提供）</string>\n    <string name=\"settings_eh_tag_translations_source_url\">https://github.com/EhTagTranslation/Editor/wiki</string>\n    <string name=\"settings_eh_filter\">隱藏列表</string>\n    <string name=\"settings_eh_filter_summary\">根據標題、上傳者、標籤、評論者隱藏畫廊或評論</string>\n    <string name=\"settings_eh_metered_network_warning\">流量計費網路警告</string>\n    <string name=\"settings_eh_request_news\">啟動時請求新聞頁面</string>\n    <string name=\"settings_eh_hide_hv_events\">隱藏 HV 事件通知</string>\n    <string name=\"settings_read\">閱讀</string>\n    <string name=\"settings_read_screen_rotation\">螢幕方向</string>\n    <string name=\"settings_read_screen_rotation_default\">系統預設</string>\n    <string name=\"settings_read_screen_rotation_portrait\">豎屏</string>\n    <string name=\"settings_read_screen_rotation_landscape\">橫屏</string>\n    <string name=\"settings_read_screen_rotation_sensor\">自動旋轉</string>\n    <string name=\"settings_read_reading_direction\">閱讀方向</string>\n    <string name=\"settings_read_reading_direction_left_to_right\">從左至右</string>\n    <string name=\"settings_read_reading_direction_right_to_Left\">從右至左</string>\n    <string name=\"settings_read_reading_direction_top_to_bottom\">從上至下</string>\n    <string name=\"settings_read_page_scaling\">頁面縮放</string>\n    <string name=\"settings_read_page_scaling_actual_size\">原始尺寸</string>\n    <string name=\"settings_read_page_scaling_fit_to_width\">匹配寬度</string>\n    <string name=\"settings_read_page_scaling_fit_to_height\">匹配長度</string>\n    <string name=\"settings_read_page_scaling_fit_to_screen\">匹配螢幕</string>\n    <string name=\"settings_read_page_scaling_fixed_scale\">固定縮放</string>\n    <string name=\"settings_read_start_position\">開頁位置</string>\n    <string name=\"settings_read_start_position_top_left\">左上角</string>\n    <string name=\"settings_read_start_position_top_right\">右上角</string>\n    <string name=\"settings_read_start_position_bottom_left\">左下角</string>\n    <string name=\"settings_read_start_position_bottom_right\">右下角</string>\n    <string name=\"settings_read_start_position_center\">中心</string>\n    <string name=\"settings_read_theme\">色彩主題</string>\n    <string name=\"settings_read_theme_follow_app\">跟隨應用</string>\n    <string name=\"settings_read_theme_dark\">暗色</string>\n    <string name=\"settings_read_theme_light\">亮色</string>\n    <string name=\"settings_read_keep_screen_on\">螢幕常亮</string>\n    <string name=\"settings_read_show_clock\">顯示時鐘</string>\n    <string name=\"settings_read_show_progress\">顯示進度</string>\n    <string name=\"settings_read_show_battery\">顯示電量</string>\n    <string name=\"settings_read_show_page_interval\">顯示頁面間隔</string>\n    <string name=\"settings_read_turn_page_interval\">自動翻頁間隔（秒）</string>\n    <string name=\"settings_read_volume_page\">使用音量鍵翻頁</string>\n    <string name=\"settings_read_volume_page_interval\">音量鍵翻頁間隔</string>\n    <string name=\"settings_read_reverse_volume\">翻轉音量鍵</string>\n    <string name=\"settings_read_reading_fullscreen\">全屏</string>\n    <string name=\"settings_read_custom_screen_lightness\">自定義螢幕亮度</string>\n    <string name=\"settings_read_screen_lightness\">螢幕亮度</string>\n    <string name=\"settings_download\">下載</string>\n    <string name=\"settings_download_download_location\">下載路徑</string>\n    <string name=\"settings_download_pick_new_location\">選擇新路徑</string>\n    <string name=\"settings_download_reset_location\">恢復預設路徑</string>\n    <string name=\"settings_download_invalid_download_location\">無效的下載路徑</string>\n    <string name=\"settings_download_cant_get_download_location\">無法取得下載路徑</string>\n    <string name=\"settings_download_media_scan\">允許其他程式進行媒體掃描</string>\n    <string name=\"settings_download_media_scan_summary_on\">請別讓別人看到你的相簿</string>\n    <string name=\"settings_download_media_scan_summary_off\">大部分相簿軟體將會忽略顯示下載路徑中的影像</string>\n    <string name=\"settings_download_concurrency\">同時多重下載</string>\n    <string name=\"settings_download_concurrency_summary\">最多同時下載 %s 張圖片</string>\n    <string name=\"settings_download_download_delay\">下載延時</string>\n    <string name=\"settings_download_download_delay_summary\">每次下載延時 %s 毫秒</string>\n    <string name=\"settings_download_download_timeout\">下載超時（秒）</string>\n    <string name=\"settings_download_preload_image\">預載圖片</string>\n    <string name=\"settings_download_preload_image_summary\">向後預載 %s 張圖片</string>\n    <string name=\"settings_download_download_origin_image\">載入原圖</string>\n    <string name=\"settings_download_download_origin_image_summary\">%s，警告！可能需要消耗 GP</string>\n    <string name=\"settings_download_download_origin_image_never\">從不啟用</string>\n    <string name=\"settings_download_download_origin_image_force\">總是啟用</string>\n    <string name=\"settings_download_download_origin_image_only\">僅下載時啟用</string>\n    <string name=\"settings_download_task_confirm\">確定要執行操作？</string>\n    <string name=\"settings_download_restore_download_items\">回復下載的檔案</string>\n    <string name=\"settings_download_restore_download_items_summary\">回復下載路徑中的所有下載的檔案</string>\n    <string name=\"settings_download_restore_not_found\">未尋獲可回復的已下載檔案</string>\n    <string name=\"settings_download_restore_failed\">回復失敗</string>\n    <string name=\"settings_download_restore_successfully\">成功回復 %d 項</string>\n    <string name=\"settings_download_clean_redundancy\">清除冗餘的檔案</string>\n    <string name=\"settings_download_clean_redundancy_summary\">清除已下載但不在下載列表中的圖檔</string>\n    <string name=\"settings_download_clean_redundancy_no_redundancy\">未發現冗餘檔案</string>\n    <string name=\"settings_download_clean_redundancy_done\">完成冗餘檔案清理，共清理了 %d 個檔案</string>\n    <string name=\"settings_privacy\">隱私</string>\n    <string name=\"settings_privacy_pattern_protection_title\">圖案保護</string>\n    <string name=\"settings_privacy_pattern_protection_not_set\">未設定圖案保護</string>\n    <string name=\"settings_privacy_pattern_protection_set\">已設定圖案保護</string>\n    <string name=\"settings_privacy_secure\">不允許螢幕擷取</string>\n    <string name=\"settings_privacy_secure_summary\">啟用後，將無法對本程式進行螢幕擷取，同時，將不會在系統任務切換器中顯示該程式的預覽內容，重新啟動本程式以套用此變更</string>\n    <string name=\"settings_privacy_clear_search_history\">清除裝置上的搜尋記錄</string>\n    <string name=\"settings_privacy_clear_search_history_summary\">移除曾在此裝置上進行過的搜尋</string>\n    <string name=\"settings_privacy_clear_search_history_cleared\">已清除搜尋記錄</string>\n    <string name=\"settings_advanced\">進階</string>\n    <string name=\"settings_advanced_save_parse_error_body\">解析失敗時儲存網頁的內容</string>\n    <string name=\"settings_advanced_save_parse_error_body_summary\">網頁內容可能含有敏感的隱私資料</string>\n    <string name=\"settings_advanced_save_crash_log\">應用程式崩潰時儲存錯誤日誌</string>\n    <string name=\"settings_advanced_save_crash_log_summary\">錯誤日誌可以幫助開發者修正問題</string>\n    <string name=\"settings_advanced_dump_logcat\">傾印 log</string>\n    <string name=\"settings_advanced_dump_logcat_summary\">儲存 log 紀錄至外接儲存裝置中</string>\n    <string name=\"settings_advanced_dump_logcat_failed\">無法傾印 log 紀錄</string>\n    <string name=\"settings_advanced_dump_logcat_to\">已儲存 log 紀錄至 %s</string>\n    <string name=\"settings_advanced_read_cache_size\">閱讀快取大小</string>\n    <string name=\"settings_advanced_app_language_title\">App 介面語言</string>\n    <string name=\"settings_advanced_proxy\">代理</string>\n    <string name=\"settings_advanced_backup_favorite\">備份收藏列表</string>\n    <string name=\"settings_advanced_backup_favorite_summary\">備份雲端收藏列表到本機</string>\n    <string name=\"settings_advanced_backup_favorite_start\">正在備份收藏列表 %s</string>\n    <string name=\"settings_advanced_backup_favorite_nothing\">沒有可以備份的收藏列表</string>\n    <string name=\"settings_advanced_backup_favorite_success\">備份收藏列表成功</string>\n    <string name=\"settings_advanced_backup_favorite_failed\">備份收藏列表失敗</string>\n    <string name=\"settings_advanced_export_data\">匯出檔案</string>\n    <string name=\"settings_advanced_export_data_summary\">儲存資料至外接儲存設備，例如下載列表，快速搜尋列表</string>\n    <string name=\"settings_advanced_export_data_to\">已匯出資料至 %s</string>\n    <string name=\"settings_advanced_export_data_failed\">無法匯出資料</string>\n    <string name=\"settings_advanced_import_data\">匯入檔案</string>\n    <string name=\"settings_advanced_import_data_summary\">從之前儲存的檔案匯入</string>\n    <string name=\"settings_advanced_import_data_successfully\">成功將資料匯入</string>\n    <string name=\"settings_advanced_import_data_cant_read\">無法讀取檔案</string>\n    <string name=\"settings_advanced_open_by_default\">預設打開</string>\n    <string name=\"settings_about\">關於</string>\n    <string name=\"settings_about_declaration_summary\">EhViewer 與 E-Hentai.org 無任何聯繫</string>\n    <string name=\"settings_about_author\">作者</string>\n    <string name=\"settings_about_issues\">常見問題</string>\n    <string name=\"settings_about_latest_release\">最新版本</string>\n    <string name=\"settings_about_source\">原始碼</string>\n    <string name=\"settings_about_version\">版本號碼</string>\n    <string name=\"settings_about_build_time\">構建於 %s</string>\n    <!-- UConfig Activity -->\n    <string name=\"u_config\">EHentai 設定</string>\n    <string name=\"apply\">套用</string>\n    <string name=\"apply_tip\">點選右上角的勾勾以儲存設定</string>\n    <!-- My Tags Activity -->\n    <string name=\"my_tags\">我的標籤</string>\n    <!-- Filter Activity -->\n    <string name=\"filter\">隱藏列表</string>\n    <string name=\"tip\">提示</string>\n    <string name=\"filter_tip\">該隱藏系統會在 EHentai 網站隱藏系統的基礎上繼續隱藏圖庫。\\n\\n標題隱藏項：排除標題含有該關鍵字的圖庫。\\n\\n上傳者隱藏項：排除該上傳者上傳的圖庫。\\n\\n標籤隱藏項：排除包含該標籤的圖庫，這會使獲取圖庫列表花費更多時間。\\n\\n命名空間隱藏項：排除包含該命名空間的圖庫，這會使獲取圖庫列表花費更多時間。\\n\\n評論者隱藏項：排除該評論者釋出的評論。\\n\\n評論隱藏項：排除匹配該正則表示式的評論。</string>\n    <string name=\"no_filter\">這裡是隱藏列表</string>\n    <string name=\"add_filter\">新增隱藏項</string>\n    <string name=\"filter_title\">標題</string>\n    <string name=\"filter_uploader\">上傳者</string>\n    <string name=\"filter_tag\">標籤</string>\n    <string name=\"filter_tag_namespace\">命名空間</string>\n    <string name=\"filter_commenter\">評論者</string>\n    <string name=\"filter_comment\">評論</string>\n    <string name=\"filter_text\">隱藏項文字</string>\n    <string name=\"delete_filter\">刪除隱藏項“%s”？</string>\n    <!-- Set Security -->\n    <string name=\"set_pattern_protection\">設定圖案保護</string>\n    <string name=\"set_pattern_protection_tip\">繪製圖案來設定圖案保護\\n留空來取消圖案保護</string>\n    <string name=\"enable_biometric\">允許使用生物特徵解鎖</string>\n    <string name=\"set\">設定</string>\n    <!-- Languages -->\n    <string name=\"app_language_system\">系統語言（預設）</string>\n    <!-- Proxy -->\n    <string name=\"proxy_direct\">直接連線</string>\n    <string name=\"proxy_system\">系統代理</string>\n    <string name=\"proxy_host_or_ip\">主機或 IP</string>\n    <string name=\"proxy_port\">通訊埠</string>\n    <string name=\"proxy_invalid_port\">無效的通訊埠</string>\n\n    <!-- Errors -->\n    <string name=\"error_bad_status_code\">錯誤狀態碼：%d</string>\n    <string name=\"error_timeout\">逾時</string>\n    <string name=\"error_unknown_host\">未知的主機</string>\n    <string name=\"error_redirection\">重新導向迴圈</string>\n    <string name=\"error_socket\">網路錯誤</string>\n    <string name=\"error_unknown\">奇怪的錯誤</string>\n    <string name=\"error_cant_find_activity\">找不到相對應的應用程式</string>\n    <string name=\"error_cannot_parse_the_url\">無法解析連結</string>\n    <string name=\"error_decoding_failed\">解碼失敗</string>\n    <string name=\"error_empty\">空</string>\n    <string name=\"error_reading_failed\">讀取失敗</string>\n    <string name=\"error_out_of_range\">超出範圍</string>\n    <string name=\"error_write_failed\">寫入失敗</string>\n    <string name=\"error_parse_error\">解析失敗</string>\n    <string name=\"error_invalid_url\">不正確的連結</string>\n    <string name=\"error_get_ptoken_error\">取得 pToken 時發生錯誤</string>\n    <string name=\"error_cant_create_temp_file\">無法建立暫存檔案</string>\n    <string name=\"error_cant_save_image\">無法儲存圖片</string>\n    <string name=\"error_invalid_number\">無效的數字</string>\n    <string name=\"error_please_login_first\">請先登入</string>\n    <string name=\"error_cannot_find_gallery\">找不到圖庫</string>\n    <string name=\"error_something_wrong_happened\">被玩壞了</string>\n    <string name=\"kokomade_tip\">設定-&gt;Eh-&gt;圖庫站台-&gt;E-Hentai</string>\n    <string name=\"no_browser_installed\">請安裝一個瀏覽器。</string>\n    <string name=\"cloudflare_bypass_failed\">未能繞過 Cloudflare 驗證</string>\n    <string name=\"open_in_webview\">在 WebView 中開啟網址以完成驗證？</string>\n    <string name=\"information_webview_outdated\">請更新 WebView 以獲得更佳的相容性</string>\n\n    <!-- Readable Time -->\n    <string name=\"from_the_future\">來自未來</string>\n    <string name=\"just_now\">剛剛</string>\n    <plurals name=\"some_minutes_ago\">\n        <item quantity=\"other\">%d 分鐘前</item>\n    </plurals>\n    <plurals name=\"some_hours_ago\">\n        <item quantity=\"other\">%d 小時前</item>\n    </plurals>\n    <string name=\"yesterday\">昨天</string>\n    <string name=\"some_days_ago\">%d 天前</string>\n    <plurals name=\"second\">\n        <item quantity=\"other\">秒</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">分鐘</item>\n    </plurals>\n    <plurals name=\"hour\">\n        <item quantity=\"other\">小時</item>\n    </plurals>\n    <plurals name=\"day\">\n        <item quantity=\"other\">天</item>\n    </plurals>\n    <plurals name=\"year\">\n        <item quantity=\"other\">年</item>\n    </plurals>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/about_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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=\"declaration\"\n        android:summary=\"@string/settings_about_declaration_summary\"\n        android:title=\"@string/settings_about_declaration\"\n        app:iconSpaceReserved=\"false\" />\n\n    <Preference\n        android:key=\"author\"\n        android:title=\"@string/settings_about_author\"\n        app:iconSpaceReserved=\"false\" />\n\n    <com.hippo.preference.UrlPreference\n        android:key=\"telegram\"\n        android:title=\"@string/settings_about_telegram\"\n        app:enableCopying=\"true\"\n        app:iconSpaceReserved=\"false\"\n        app:url=\"https://t.me/+AOIyOaC5iaFmYWVl\" />\n\n    <com.hippo.preference.UrlPreference\n        android:key=\"issues\"\n        android:title=\"@string/settings_about_issues\"\n        app:enableCopying=\"true\"\n        app:iconSpaceReserved=\"false\"\n        app:url=\"https://github.com/EhViewer-NekoInverter/EhViewer/issues/18\" />\n\n    <com.hippo.preference.UrlPreference\n        android:key=\"latest_release\"\n        android:title=\"@string/settings_about_latest_release\"\n        app:enableCopying=\"true\"\n        app:iconSpaceReserved=\"false\"\n        app:url=\"https://github.com/EhViewer-NekoInverter/EhViewer/releases\" />\n\n    <com.hippo.preference.UrlPreference\n        android:key=\"source\"\n        android:title=\"@string/settings_about_source\"\n        app:enableCopying=\"true\"\n        app:iconSpaceReserved=\"false\"\n        app:url=\"https://github.com/EhViewer-NekoInverter/EhViewer\" />\n\n    <com.hippo.ehviewer.preference.VersionPreference\n        android:key=\"version\"\n        app:iconSpaceReserved=\"false\" />\n\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/advanced_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"save_parse_error_body\"\n        android:summary=\"@string/settings_advanced_save_parse_error_body_summary\"\n        android:title=\"@string/settings_advanced_save_parse_error_body\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"save_crash_log\"\n        android:summary=\"@string/settings_advanced_save_crash_log_summary\"\n        android:title=\"@string/settings_advanced_save_crash_log\"\n        app:iconSpaceReserved=\"false\" />\n\n    <Preference\n        android:key=\"dump_logcat\"\n        android:summary=\"@string/settings_advanced_dump_logcat_summary\"\n        android:title=\"@string/settings_advanced_dump_logcat\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"320\"\n        android:key=\"read_cache_size\"\n        android:title=\"@string/settings_advanced_read_cache_size\"\n        app:entries=\"@array/read_cache_size_entries\"\n        app:entryValues=\"@array/read_cache_size_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"system\"\n        android:key=\"app_language\"\n        android:title=\"@string/settings_advanced_app_language_title\"\n        app:entries=\"@array/app_language_entries\"\n        app:entryValues=\"@array/app_language_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <com.hippo.ehviewer.preference.ProxyPreference\n        android:key=\"proxy\"\n        android:title=\"@string/settings_advanced_proxy\"\n        app:iconSpaceReserved=\"false\" />\n\n    <com.hippo.ehviewer.preference.UserAgentPreference\n        android:key=\"user_agent\"\n        android:title=\"@string/settings_advanced_user_agent_title\"\n        app:iconSpaceReserved=\"false\"\n        app:negativeButtonText=\"@android:string/cancel\"\n        app:positiveButtonText=\"@android:string/ok\" />\n\n    <Preference\n        android:key=\"backup_favorite\"\n        android:summary=\"@string/settings_advanced_backup_favorite_summary\"\n        android:title=\"@string/settings_advanced_backup_favorite\"\n        app:iconSpaceReserved=\"false\" />\n\n    <Preference\n        android:key=\"export_data\"\n        android:summary=\"@string/settings_advanced_export_data_summary\"\n        android:title=\"@string/settings_advanced_export_data\"\n        app:iconSpaceReserved=\"false\" />\n\n    <Preference\n        android:key=\"import_data\"\n        android:summary=\"@string/settings_advanced_import_data_summary\"\n        android:title=\"@string/settings_advanced_import_data\"\n        app:iconSpaceReserved=\"false\" />\n\n    <Preference\n        android:key=\"open_by_default\"\n        android:title=\"@string/settings_advanced_open_by_default\"\n        app:iconSpaceReserved=\"false\" />\n\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/backup_scheme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<full-backup-content>\n\n    <include\n        domain=\"database\"\n        path=\"eh.db\" />\n    <include\n        domain=\"database\"\n        path=\"eh.db-shm\" />\n    <include\n        domain=\"database\"\n        path=\"eh.db-wal\" />\n    <include\n        domain=\"database\"\n        path=\"hosts.db\" />\n    <include\n        domain=\"database\"\n        path=\"search_database.db\" />\n\n</full-backup-content>\n"
  },
  {
    "path": "app/src/main/res/xml/data_extraction_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<data-extraction-rules>\n    <cloud-backup>\n\n    </cloud-backup>\n</data-extraction-rules>"
  },
  {
    "path": "app/src/main/res/xml/download_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<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=\"download_location\"\n        android:title=\"@string/settings_download_download_location\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"media_scan\"\n        android:title=\"@string/settings_download_media_scan\"\n        app:iconSpaceReserved=\"false\"\n        app:summaryOff=\"@string/settings_download_media_scan_summary_off\"\n        app:summaryOn=\"@string/settings_download_media_scan_summary_on\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"3\"\n        android:key=\"download_thread\"\n        android:title=\"@string/settings_download_concurrency\"\n        app:entries=\"@array/multi_thread_download_entries\"\n        app:entryValues=\"@array/multi_thread_download_entry_values\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"1000\"\n        android:key=\"download_delay_2\"\n        android:title=\"@string/settings_download_download_delay\"\n        app:entries=\"@array/download_delay_entries\"\n        app:entryValues=\"@array/download_delay_entry_values\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"60\"\n        android:key=\"download_timeout\"\n        android:max=\"120\"\n        android:title=\"@string/settings_download_download_timeout\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"20\"\n        app:showSeekBarValue=\"true\"\n        app:singleLineTitle=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"5\"\n        android:key=\"preload_image\"\n        android:title=\"@string/settings_download_preload_image\"\n        app:entries=\"@array/preload_image_entries\"\n        app:entryValues=\"@array/preload_image_entry_values\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"0\"\n        android:key=\"download_origin_image_\"\n        android:title=\"@string/settings_download_download_origin_image\"\n        app:entries=\"@array/download_origin_image_entries\"\n        app:entryValues=\"@array/download_origin_image_entry_values\"\n        app:iconSpaceReserved=\"false\" />\n\n    <com.hippo.ehviewer.preference.RestoreDownloadPreference\n        android:key=\"restore_download_items\"\n        android:summary=\"@string/settings_download_restore_download_items_summary\"\n        android:title=\"@string/settings_download_restore_download_items\"\n        app:iconSpaceReserved=\"false\" />\n\n    <com.hippo.ehviewer.preference.CleanRedundancyPreference\n        android:key=\"clean_redundancy\"\n        android:summary=\"@string/settings_download_clean_redundancy_summary\"\n        android:title=\"@string/settings_download_clean_redundancy\"\n        app:iconSpaceReserved=\"false\" />\n\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/eh_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <com.hippo.ehviewer.preference.AccountPreference\n        android:key=\"account\"\n        android:summary=\"@string/settings_eh_account_name_tourist\"\n        android:title=\"@string/settings_eh_account_name\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"1\"\n        android:key=\"gallery_site\"\n        android:title=\"@string/settings_eh_gallery_site\"\n        app:entries=\"@array/gallery_site_entries\"\n        app:entryValues=\"@array/gallery_site_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <com.hippo.ehviewer.preference.ImageLimitsPreference\n        android:key=\"image_limits\"\n        android:summary=\"@string/settings_eh_image_limits_summary\"\n        android:title=\"@string/settings_eh_image_limits\"\n        app:iconSpaceReserved=\"false\"\n        app:negativeButtonText=\"@android:string/cancel\"\n        app:positiveButtonText=\"@string/settings_eh_reset\" />\n\n    <Preference\n        android:key=\"uconfig\"\n        android:summary=\"@string/settings_eh_u_config_summary\"\n        android:title=\"@string/settings_eh_u_config\"\n        app:iconSpaceReserved=\"false\" />\n\n    <Preference\n        android:key=\"mytags\"\n        android:summary=\"@string/settings_eh_my_tags_summary\"\n        android:title=\"@string/settings_eh_my_tags\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"-1\"\n        android:entries=\"@array/night_mode_entries\"\n        android:entryValues=\"@array/night_mode_values\"\n        android:key=\"theme\"\n        android:title=\"@string/dark_theme\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:key=\"black_dark_theme\"\n        android:title=\"@string/settings_eh_black_dark_theme\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"0\"\n        android:key=\"launch_page\"\n        android:title=\"@string/settings_eh_launch_page\"\n        app:entries=\"@array/launch_page_entries\"\n        app:entryValues=\"@array/launch_page_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"0\"\n        android:key=\"list_mode\"\n        android:title=\"@string/settings_eh_list_mode\"\n        app:entries=\"@array/list_mode_entries\"\n        app:entryValues=\"@array/list_mode_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"8\"\n        android:key=\"detail_size_\"\n        android:max=\"12\"\n        android:title=\"@string/settings_eh_detail_size\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"6\"\n        app:showSeekBarValue=\"true\"\n        app:singleLineTitle=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"40\"\n        android:key=\"list_cover_size\"\n        android:max=\"60\"\n        android:title=\"@string/settings_eh_list_tile_thumb_size\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"35\"\n        app:showSeekBarValue=\"true\"\n        app:singleLineTitle=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"4\"\n        android:key=\"cover_size\"\n        android:max=\"10\"\n        android:title=\"@string/settings_eh_thumb_size\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"2\"\n        app:showSeekBarValue=\"true\"\n        app:singleLineTitle=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"thumb_show_title\"\n        android:title=\"@string/settings_eh_thumb_show_title\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"show_jpn_title\"\n        android:summary=\"@string/settings_eh_show_jpn_title_summary\"\n        android:title=\"@string/settings_eh_show_jpn_title\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"show_gallery_pages\"\n        android:summary=\"@string/settings_eh_show_gallery_pages_summary\"\n        android:title=\"@string/settings_eh_show_gallery_pages\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"show_gallery_comments\"\n        android:summary=\"@string/settings_eh_show_gallery_comments_summary\"\n        android:title=\"@string/settings_eh_show_gallery_comments\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"-101\"\n        android:key=\"comment_threshold\"\n        android:max=\"100\"\n        android:summary=\"@string/settings_eh_comment_threshold_summary\"\n        android:title=\"@string/settings_eh_comment_threshold\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"-101\"\n        app:showSeekBarValue=\"true\"\n        app:singleLineTitle=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"60\"\n        android:key=\"preview_num\"\n        android:max=\"200\"\n        android:title=\"@string/settings_eh_preview_num\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"0\"\n        app:showSeekBarValue=\"true\"\n        app:singleLineTitle=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"3\"\n        android:key=\"preview_size\"\n        android:max=\"10\"\n        android:title=\"@string/settings_eh_preview_size\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"1\"\n        app:showSeekBarValue=\"true\"\n        app:singleLineTitle=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"show_tag_translations\"\n        android:summary=\"@string/settings_eh_show_tag_translations_summary\"\n        android:title=\"@string/settings_eh_show_tag_translations\"\n        app:iconSpaceReserved=\"false\" />\n\n    <com.hippo.preference.UrlPreference\n        android:key=\"tag_translations_source\"\n        android:title=\"@string/settings_eh_tag_translations_source\"\n        app:iconSpaceReserved=\"false\"\n        app:url=\"@string/settings_eh_tag_translations_source_url\" />\n\n    <Preference\n        android:key=\"filter\"\n        android:summary=\"@string/settings_eh_filter_summary\"\n        android:title=\"@string/settings_eh_filter\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"cellular_network_warning\"\n        android:title=\"@string/settings_eh_metered_network_warning\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"request_news\"\n        android:title=\"@string/settings_eh_request_news\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"hide_hv_events\"\n        android:title=\"@string/settings_eh_hide_hv_events\"\n        app:iconSpaceReserved=\"false\" />\n\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/filepaths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<path>\n\n    <external-cache-path\n        name=\"temp\"\n        path=\".\" />\n\n    <external-cache-path\n        name=\"copy\"\n        path=\".\" />\n</path>\n"
  },
  {
    "path": "app/src/main/res/xml/locale_config.xml",
    "content": "<locale-config xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <locale android:name=\"en-US\"/>\n    <locale android:name=\"ja\"/>\n    <locale android:name=\"zh-CN\"/>\n    <locale android:name=\"zh-HK\"/>\n    <locale android:name=\"zh-TW\"/>\n</locale-config>\n"
  },
  {
    "path": "app/src/main/res/xml/privacy_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\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=\"security\"\n        android:title=\"@string/settings_privacy_pattern_protection_title\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"enable_secure\"\n        android:title=\"@string/settings_privacy_secure\"\n        app:iconSpaceReserved=\"false\"\n        app:summaryOff=\"@string/settings_privacy_secure_summary\"\n        app:summaryOn=\"@string/settings_privacy_secure_summary\" />\n\n    <com.hippo.ehviewer.preference.ClearSearchHistoryPreference\n        android:key=\"clear_search_history\"\n        android:summary=\"@string/settings_privacy_clear_search_history_summary\"\n        android:title=\"@string/settings_privacy_clear_search_history\"\n        app:iconSpaceReserved=\"false\" />\n\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/read_settings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"0\"\n        android:key=\"screen_rotation\"\n        android:title=\"@string/settings_read_screen_rotation\"\n        app:entries=\"@array/screen_rotation_entries\"\n        app:entryValues=\"@array/screen_rotation_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"1\"\n        android:key=\"reading_direction\"\n        android:title=\"@string/settings_read_reading_direction\"\n        app:entries=\"@array/reading_direction_entries\"\n        app:entryValues=\"@array/reading_direction_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"3\"\n        android:key=\"page_scaling\"\n        android:title=\"@string/settings_read_page_scaling\"\n        app:entries=\"@array/page_scaling_entries\"\n        app:entryValues=\"@array/page_scaling_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"1\"\n        android:key=\"start_position\"\n        android:title=\"@string/settings_read_start_position\"\n        app:entries=\"@array/start_position_entries\"\n        app:entryValues=\"@array/start_position_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <rikka.preference.SimpleMenuPreference\n        android:defaultValue=\"1\"\n        android:key=\"read_theme\"\n        android:title=\"@string/settings_read_theme\"\n        app:entries=\"@array/read_theme_entries\"\n        app:entryValues=\"@array/read_theme_entry_values\"\n        app:useSimpleSummaryProvider=\"true\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"keep_screen_on\"\n        android:title=\"@string/settings_read_keep_screen_on\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"gallery_show_clock\"\n        android:title=\"@string/settings_read_show_clock\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"gallery_show_progress\"\n        android:title=\"@string/settings_read_show_progress\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"gallery_show_battery\"\n        android:title=\"@string/settings_read_show_battery\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"gallery_show_page_interval\"\n        android:title=\"@string/settings_read_show_page_interval\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"5\"\n        android:key=\"turn_page_interval\"\n        android:max=\"15\"\n        android:title=\"@string/settings_read_turn_page_interval\"\n        app:iconSpaceReserved=\"false\"\n        app:min=\"1\"\n        app:showSeekBarValue=\"true\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"volume_page\"\n        android:title=\"@string/settings_read_volume_page\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"1\"\n        android:dependency=\"volume_page\"\n        android:key=\"volume_page_interval\"\n        android:max=\"14\"\n        android:title=\"@string/settings_read_volume_page_interval\"\n        app:iconSpaceReserved=\"false\"\n        app:showSeekBarValue=\"true\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:dependency=\"volume_page\"\n        android:key=\"reserve_volume_page\"\n        android:title=\"@string/settings_read_reverse_volume\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"true\"\n        android:key=\"reading_fullscreen\"\n        android:title=\"@string/settings_read_reading_fullscreen\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SwitchPreferenceCompat\n        android:defaultValue=\"false\"\n        android:key=\"custom_screen_lightness\"\n        android:title=\"@string/settings_read_custom_screen_lightness\"\n        app:iconSpaceReserved=\"false\" />\n\n    <SeekBarPreference\n        android:defaultValue=\"50\"\n        android:dependency=\"custom_screen_lightness\"\n        android:key=\"screen_lightness\"\n        android:title=\"@string/settings_read_screen_lightness\"\n        android:max=\"200\"\n        app:iconSpaceReserved=\"false\"\n        app:showSeekBarValue=\"true\" />\n\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml/settings_headers.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2016 Hippo Seven\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<PreferenceScreen xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <Preference\n        android:key=\"eh\"\n        android:icon=\"@drawable/v_sad_panda_primary_x24\"\n        android:title=\"@string/settings_eh\" />\n\n    <Preference\n        android:key=\"read\"\n        android:icon=\"@drawable/v_book_open_primary_x24\"\n        android:title=\"@string/settings_read\" />\n\n    <Preference\n        android:key=\"download\"\n        android:icon=\"@drawable/v_download_primary_x24\"\n        android:title=\"@string/settings_download\" />\n\n    <Preference\n        android:key=\"privacy\"\n        android:icon=\"@drawable/v_sec_primary_x24\"\n        android:title=\"@string/settings_privacy\" />\n\n    <Preference\n        android:key=\"advanced\"\n        android:icon=\"@drawable/v_adb_primary_x24\"\n        android:title=\"@string/settings_advanced\" />\n\n    <Preference\n        android:key=\"about\"\n        android:icon=\"@drawable/v_info_primary_x24\"\n        android:title=\"@string/settings_about\" />\n</PreferenceScreen>\n"
  },
  {
    "path": "app/src/main/res/xml-v25/shortcuts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n  ~ Copyright 2022 Moedog\n  ~\n  ~ This file is part of EhViewer\n  ~\n  ~ EhViewer is free software: you can redistribute it and/or modify it\n  ~ under the terms of the GNU General Public License as published\n  ~ by the Free Software Foundation, either version 3 of the License, or\n  ~ (at your option) any later version.\n  ~\n  ~ EhViewer is distributed in the hope that it will be useful, but WITHOUT\n  ~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or\n  ~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\n  ~\n  ~ You should have received a copy of the GNU General Public License along with EhViewer.\n  ~ If not, see <https://www.gnu.org/licenses/>.\n  -->\n\n<shortcuts xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <shortcut\n        android:enabled=\"true\"\n        android:icon=\"@drawable/ic_shortcut_start\"\n        android:shortcutDisabledMessage=\"@string/download_start_all\"\n        android:shortcutId=\"start_all\"\n        android:shortcutLongLabel=\"@string/download_start_all\"\n        android:shortcutShortLabel=\"@string/download_start_all\">\n        <intent\n            android:action=\"start_all\"\n            android:targetClass=\"com.hippo.ehviewer.shortcuts.ShortcutsActivity\"\n            android:targetPackage=\"org.moedog.ehviewer\" />\n        <categories android:name=\"android.shortcut.conversation\" />\n    </shortcut>\n\n    <shortcut\n        android:enabled=\"true\"\n        android:icon=\"@drawable/ic_shortcut_stop\"\n        android:shortcutDisabledMessage=\"@string/download_stop_all\"\n        android:shortcutId=\"stop_all\"\n        android:shortcutLongLabel=\"@string/download_stop_all\"\n        android:shortcutShortLabel=\"@string/download_stop_all\">\n        <intent\n            android:action=\"stop_all\"\n            android:targetClass=\"com.hippo.ehviewer.shortcuts.ShortcutsActivity\"\n            android:targetPackage=\"org.moedog.ehviewer\" />\n        <categories android:name=\"android.shortcut.conversation\" />\n    </shortcut>\n\n</shortcuts>"
  },
  {
    "path": "build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.kotlin.android) apply false\n    alias(libs.plugins.kotlin.parcelize) apply false\n    alias(libs.plugins.kotlin.serialization) apply false\n    alias(libs.plugins.ksp) apply false\n    alias(libs.plugins.spotless) apply false\n}\n"
  },
  {
    "path": "docs/CHANGELOG/zh-cn.md",
    "content": "# 变更日志\n\n## [1.8.13] - 2026-03-19\n\n### 改进\n\n* Cookie 登录允许忽略 Cloudflare 错误，但会导致无法获取用户名和头像\n\n### Bug 修复\n\n* 修复清除图片缓存后可能导致图片加载卡死\n* 修复部分设备 UA 版本号错误\n\n## [1.8.12] - 2025-12-26\n\n### 改进\n\n* 优化 Cookie 登录失败提示\n* 添加 WebView 更新提示\n\n### Bug 修复\n\n* 修复部分老旧设备过 Cloudflare 盾\n\n## [1.8.11] - 2025-12-15\n\n### 重大变更\n\n* 为减少更新画廊时出现的意外情况，现在更新时不会再自动删除旧画廊，**请在新画廊下载完成之后手动删除旧画廊**\n* 同时更新画廊功能不再仅限于 MPV 用户（仅限软件更新后下载的画廊）\n\n### 改进\n\n* 退出登录无需再重启 APP\n* 过滤 cookie 中的非法字符\n* 在搜索时自动移除换行符\n* 优化收藏备注显示样式\n* 支持在设置中一键清空搜索历史\n* 同步网页更改 页数范围比至多为 0.5\n* 同步网页更改 添加 location 命名空间\n* 内置 ISRG Root X1 证书\n* 更新日语翻译\n\n### Bug 修复\n\n* 修复过 Cloudflare 盾\n* 修复翻页速度错误\n* 修复导出的数据文件扩展名错误\n* 修复搜索时候选项跳动\n* 修复里站种子文件下载\n\n## [1.8.10] - 2025-05-11\n\n### 重大变更\n\n* 合并 Marshmallow 变种\n* 由于缩略图服务器已全部支持 HTTP/2，移除强制使用 E-Hentai 缩略图服务器功能\n\n### 改进\n\n* 支持调整音量键翻页速度\n* 恢复下载项时支持从 ComicInfo.xml 创建丢失的 .ehviewer 文件\n\n### Bug 修复\n\n* 修复动图播放时撕裂\n* 修复下载页面缩略图加载卡顿 [#81](https://github.com/EhViewer-NekoInverter/EhViewer/issues/81)\n* 修复从下载页面进入详情页时缩略图闪烁\n\n## [1.8.9] - 2024-12-13\n\n### 重大变更\n\n* 迁移到 Coil 3\n* 由于 E 站变更，移除搜索封面功能\n* 由于 E 站变更，cookie 登录移除 igneous 字段\n\n### 改进\n\n* 更新日语翻译\n* 不再修改 GIF 原文件\n* 账号密码登录也进行账户封禁检查\n* 优化压缩包读取，添加缺失的 .heic 支持\n\n### Bug 修复\n\n* 修复 16 bit 图片渲染\n* 修复 GIF 重写未符合浏览器行为\n* 修复部分情况下缩略图链接被错误替换\n* 修复部分情况下 SnackBar 被遮挡\n* 修复保存单张原图时文件扩展名错误\n* 修复列表模式下评分数与上传者重叠 [#77](https://github.com/EhViewer-NekoInverter/EhViewer/issues/77)\n* 修复归档下载未申请存储权限 (API < 29)\n* [Marshmallow] 修复 UTF-8 文件名压缩包读取\n* [Marshmallow] 修复低版本设备阅读时闪退\n\n## [1.8.8] - 2024-11-01\n\n### 重大变更\n\n* 重新提供对 Android 6-8 设备的有限支持\n* 适配 E 站缩略图和 WebP 等相关变更\n\n### 改进\n\n* 限制里站缩略图并发数，防止在快速滚动时出现大规模的加载失败\n* 解析画廊列表的错误信息\n* 修改标签样式（半透明=0-9MP，斜体=10-99MP，普通=100+MP）并支持在投票后自动刷新标签\n* 支持一键刷新 cookie，并显示其过期时间\n* 显示临时配额的过期时间\n* 为下载页面添加更多排序方式，并记住上次选择的排序方式\n* 记住上次选择的排行榜\n* 为恢复下载项、清理下载冗余功能添加确认框\n\n### Bug 修复\n\n* 修复图片预载顺序错乱的问题\n* 修复画廊详情页无法通过点击错误刷新的问题\n* 修复我的标签页面无法切换标签集的问题\n* 修复强制使用 E-Hentai 缩略图服务器功能，并不再强制使用 E-Hentai API 服务器\n* 修复某些情况下 H@H 图片校验错误\n* 修复小缩略图的淡入效果\n\n## [1.8.7] - 2024-09-12\n\n### 重大变更\n\n* 适配 E 站新配额系统（不显示 IP 配额、支持临时配额）\n\n### 改进\n\n* 增加图片校验，避免加载被篡改的 H@H 图片\n* 游客用户访问设置页时不再尝试获取配额信息\n* 支持自定义 User Agent，如有必要请自行更改为桌面端\n* 移除内置 hosts\n* A13 及以上系统也显示复制通知\n* 支持下载单页原图\n* 支持清除指定画廊的缓存\n* E 站不再强制人机验证，故加回传统登录页面\n* 解析 Cookie 封禁错误\n* 优化 Cookie 粘贴，无 igneous 也可识别\n\n### Bug 修复\n\n* 修复种子数量解析\n* 修复从画廊预览页进入阅读器时的进度错误\n* 修复进入网页设置后所有 Cookie 有效期被转为永久的问题\n\n## [1.8.6] - 2024-05-14\n\n### 重大变更\n\n* 使用更先进的元数据序列化机制以兼容彩 E 的下载数据，此版本及以后的下载数据将无法被旧版本应用读取\n\n### 改进\n\n* 支持升级已下载的画廊 [⚠需要 Hath Perk: Multi-Page Viewer]\n* 在登录时初始化搜藏夹名称 [#59](https://github.com/EhViewer-NekoInverter/EhViewer/issues/59)\n* 禁止在 cookie 界面截图以防止不经意的信息泄露\n* 支持通过 ComicInfo.xml 恢复下载项\n\n### Bug 修复\n\n* 修复图片搜索功能 [由于 E 站限制，现在仅能搜索完全匹配图片]\n* 修复图片下载未遵循先进先出原则\n\n## [1.8.5] - 2024-03-23\n\n### 重大变更\n\n* 由于登录强制需要人机验证，移除传统登录页面\n\n### 改进\n\n* 支持阅读时自动翻页\n\n### Bug 修复\n\n* 修复部分情况下缩略图动画闪退\n\n## [1.8.4] - 2024-03-07\n\n### 改进\n\n* 支持直接清除 igneous 而无需重新登录\n\n### Bug 修复\n\n* 修复种子解析失败问题\n\n## [1.8.3] - 2024-03-03\n\n### 改进\n\n* 归档下载支持调用第三方下载器\n\n### Bug 修复\n\n* 修复数据过多时滚动条消失的问题 [#38](https://github.com/EhViewer-NekoInverter/EhViewer/issues/38)\n\n## [1.8.2.1] - 2024-03-03\n\n### 改进\n\n* 支持手动调整下载项顺序\n* 支持从本地读取下载项封面图\n* 支持在详情页长按收藏按钮更改收藏分类\n* 从 H@H 下载原图失败后自动尝试从源服务器下载\n* 增加阅读器加载动画\n\n### Bug 修复\n\n* 修复某些情况下无法调整下载标签顺序 [#56](https://github.com/EhViewer-NekoInverter/EhViewer/issues/56)\n\n## [1.8.2] - 2023-11-01\n\n### 改进\n\n* 鉴于中文评论区涌入大量网络低能，贴心地增加了按分数屏蔽评论的功能\n\n### Bug 修复\n\n* 修复原图下载\n* 修复 Android 9 打开评论区白屏 [#55](https://github.com/EhViewer-NekoInverter/EhViewer/issues/55)\n\n## [1.8.1] - 2023-10-28\n\n### 重大变更\n\n* 修复从 API 获取图片地址时解析失败的问题\n* 本次网站更新后，移动端的自动分辨率变成了 780px，而不是原来的 1280px，如果觉得图片有点糊请自行在网站设置中调整分辨率\n\n### 改进\n\n* 重构下载延迟以缓解 IP 封禁\n* 优化图片下载重试策略，减少配额消耗\n* 实现下载超时，可在 设置-下载 中调整\n\n### Bug 修复\n\n* 修复状态栏文字/图标颜色错误\n\n## [1.8.0] - 2023-09-09\n\n### 改进\n\n* 移除代理设置、hosts 设置\n\n### Bug 修复\n\n* 修复里站配额获取\n* 修复种子列表解析\n\n## [1.7.30] - 2023-08-06\n\n### 重大变更\n\n* 移除域名前置，直连用户请考虑换用其他软件\n* 适配网站变更，优化原图下载 (自 2023-07-07 起，发布时间超过一年的画廊下载原图需要消耗 GP)\n\n### 改进\n\n* 支持使用 WebView 进行 Cloudflare 验证，缓解 403 问题\n\n### Bug 修复\n\n* 修复 1.7.29.4 版本跳页闪退\n* 修复压缩包下载和配额重置\n* 修复下载时缩略图闪烁\n\n## [1.7.29.4] - 2023-05-16\n\n### 重大变更\n\n* 适配 exhentai 缩略图域名变更\n\n### 改进\n\n* 本地标签屏蔽列表支持显示翻译\n\n## [1.7.29.3] - 2023-04-04\n\n### 改进\n\n* 优化允许媒体扫描\n* 优化标签翻译数据库加载与更新逻辑\n* 优化标签建议，不再限制建议数量\n\n### Bug 修复\n\n* 修复重新下载时无法获取原文件 [#35](https://github.com/EhViewer-NekoInverter/EhViewer/issues/35)\n* 修复移动本地收藏至云端后未删除本地收藏\n* 修复某些情况下缩略图闪烁\n\n## [1.7.29.2] - 2023-03-25\n\n### 改进\n\n* 优化侧边栏夜间模式切换逻辑，优先设为跟随系统\n* 使用 room 重构 cookie 数据库\n\n### Bug 修复\n\n* 修复空压缩包不显示错误信息\n* 修复从上至下阅读模式下的翻页回弹问题 [seven332/EhViewer#653](https://github.com/seven332/EhViewer/issues/653)\n* 修复已下载画廊的图片无法长按保存\n\n## [1.7.29.1] - 2023-03-22\n\n### 重大变更\n\n* 将缩略图和缓存组件更换为 Coil 实现，所有缓存图片需要重新加载\n\n### 改进\n\n* 将 “退出登录” 与 “身份 Cookie” 合并为 “账户”\n* 协程化 Spider Queen\n* 优化压缩包读取\n\n### Bug 修复\n\n* 修复未退出阅读器时无法打开压缩包\n* 修复快速搜索项目过多时切换页面崩溃 [#33](https://github.com/EhViewer-NekoInverter/EhViewer/issues/33)\n* 修复已下载画廊的图片无法长按保存\n\n## [1.7.29] - 2023-03-12\n\n### 重大变更\n\n* 移除数据库版本验证，兼容的数据都可以直接导入\n* 支持查看与重置图片配额，支持在压缩包下载页面显示账户余额\n* 重构登录逻辑，防止 IP 污染，捐赠用户不会再遇到白屏 [Ehviewer-Overhauled/Ehviewer#873](https://github.com/Ehviewer-Overhauled/EhViewer/issues/873)\n\n### 改进\n\n* 支持设置缩略图缓存大小\n* 支持在启用域名前置时绕过 VPN\n* 支持在压缩包下载页面显示账户余额\n* 评论编辑器增强\n* 重构标签数据库，优化标签建议顺序：完全匹配->起始匹配->包含匹配\n* 重构 EhEngine\n* 移除新用户导引\n\n### Bug 修复\n\n* 修复快速切换页面导致的崩溃\n* 修复历史记录中的收藏状态显示，仅更新后的记录有效\n* 修复 509 误报\n* 修复无主画廊上传者评论显示、评论上次修改时间显示\n* 修复评论删除线显示\n\n## [1.7.28.7] - 2023-02-16\n\n### 改进\n\n* 使用半透明表示弱权重标签，使用黄色/灰色表示已赞/已踩标签\n\n### Bug 修复\n\n* 修复阅读器页码出现黑色背景\n* 修复 GIF 渲染时未清除上一帧内容\n\n## [1.7.28.6] - 2023-02-05\n\n### 改进\n\n* 归档文件名称排序不再忽略前置零\n* 添加了表站 Sad Panda 与 503 错误提示\n\n### Bug 修复\n\n* 修复上一版本变更导致的缩略图重载\n* 修复某些非中文情况下标签翻译默认打开\n* 修复余额不足时不显示归档文件下载\n\n## [1.7.28.5] - 2023-01-26\n\n### 改进\n\n* 移除无人维护的语言文件\n* 提升画廊详情页缩略图渲染效率，在多缩略图情况下有效改善卡顿\n* 画廊详情页加载更多缩略图时从当前阅览的下一张开始\n\n### Bug 修复\n\n* 修正长方形头像的显示问题\n* 修复偶尔有缩略图不加载的问题\n\n## [1.7.28.4] - 2023-01-15\n\n### 改进\n\n* 支持置顶下载项目，搭配标题筛选可快速按系列整理下载内容\n* 下载界面添加更多排序方式\n* 下载界面添加“全部”下载标签\n* 添加开始下载种子文件的提示\n* 归档文件下载界面隐藏不可用的下载选项\n* 支持在画廊详情界面长按收藏按钮编辑备注\n* 从上至下阅读模式下双击不会再缩小到 0.5\n* 下载时始终优先从缓存读取 GIF 动图，减少配额损耗\n\n### Bug 修复\n\n* 修复了几处奔溃\n* 修复 Extended 模式下不显示收藏备注\n* 修复某些情况下 GIF 播放速度异常\n* 修复直接下载的图片文件扩展名错误\n\n## [1.7.28.3] - 2023-01-09\n\n### 改进\n\n* 支持仅在下载时下载原图\n* 支持显示和添加收藏备注\n\n### Bug 修复\n\n* 修复某些情况下进入下载页面闪退\n\n## [1.7.28.2] - 2023-01-07\n\n### 改进\n\n* 为种子下载页面添加更多信息\n* 支持不通过 H@H 下载压缩包\n* 更改排行榜顺序，优先显示昨日排行\n\n### Bug 修复\n\n* 修复某些情况下主页无法加载\n* 修复某些情况下对下载目录进行轮询阻塞 UI 的问题\n\n## [1.7.28.1] - 2023-01-01\n\n### 改进\n\n* 下载页面添加标题筛选与排序功能\n* 为 H@H 下载添加文件大小及下载资费显示\n\n### Bug 修复\n\n* 修复上一版本跳转到末页功能无效\n\n## [1.7.28] - 2022-12-30\n\n### 改进\n\n* 压缩包支持按图片文件名顺序阅读 [Ehviewer-Overhauled/Ehviewer#427](https://github.com/Ehviewer-Overhauled/EhViewer/issues/427)\n* 阅读界面拖动进度条实时跳转\n* 优化从上至下阅读模式下的阅读进度显示，支持识别最后一页 [Ehviewer-Overhauled/Ehviewer#362](https://github.com/Ehviewer-Overhauled/EhViewer/issues/362)\n* 从上至下阅读模式允许将图片缩小到 0.5\n* 所有语言均支持标签建议，标签建议中完全匹配的标签优先显示\n* 缩略图模式支持显示画廊标题\n* 支持 GID 跳页与跳转到末页\n* 快速搜索支持保存 GID 位置信息\n\n### Bug 修复\n\n* 修复部分情况下的闪退问题\n* 修复本地收藏搜索\n* 修复加载上一页后打开当前页画廊错误 [Ehviewer-Overhauled/Ehviewer#201](https://github.com/Ehviewer-Overhauled/EhViewer/issues/201)\n* 重构 getPageData，修复页面加载时打开画廊再返回后卡住 [Ehviewer-Overhauled/Ehviewer#210](https://github.com/Ehviewer-Overhauled/EhViewer/issues/210)\n\n## [1.7.28.0-b2] - 2022-12-11\n\n### 改进\n\n* 添加评论用户屏蔽和评论内容正则屏蔽功能\n* 搜索历史长按直接删除，无需二次确认，与原版表现一致\n* 使用 room paging 重构数据库和历史页面，历史记录现在不再有数量限制\n* 将 跳过登录 重命名为 游客模式，防止被跳广告软件跳过登录\n* 在画廊详情页删除下载时支持同时删除文件\n* 适配 Android 13 的语言选择器\n\n### Bug 修复\n\n* 修复图片搜索选择图片后不显示预览的问题\n* 修复无主画廊上传者显示，添加私密画廊分类\n\n## [1.7.28.0-b1] - 2022-12-03\n\n### 重大变更\n\n* 从此版本开始，最低要求 Android 9\n* 将归档组件从 p7zip 迁移到 libarchive 实现\n    * 更多归档格式支持 (.gz .xz .tar .cbz .cbr)\n    * 支持打开加密的压缩包 (7z 暂不支持 [Ehviewer-Overhauled/Ehviewer#114](https://github.com/Ehviewer-Overhauled/EhViewer/issues/114))\n    * 流式读取 (会影响图片顺序 [Ehviewer-Overhauled/Ehviewer#427](https://github.com/Ehviewer-Overhauled/EhViewer/issues/427))\n* 将图像解码组件更换为 ImageDecoder Java API 实现\n    * 更优秀的性能\n\n### 改进\n\n* 在画廊详情页面添加返回按钮\n* 扩大 缩略图模式下缩略图/详情页面预览图 大小调整范围\n\n### Bug 修复\n\n* 修复列表页面标签解析\n\n## [1.7.27-final] - 2022-11-29\n\n### 重大变更\n\n* 从下一版本开始最低要求 Android 9\n\n### 改进\n\n* 修改深色模式下 detail button 颜色\n* 最低支持到 Android 6\n\n### Bug 修复\n\n* 修复部分情况下的时区错乱\n\n## [1.7.27.8] - 2022-11-24\n\n### 改进\n\n* 收藏页面添加全选功能，方便整理收藏夹\n* 改善对安卓 10 以下系统的兼容性\n* 添加 IP 被墙标记，暂时性改善表站直连\n\n### Bug 修复\n\n* 修复标签和上传者页面的跳页功能\n\n## [1.7.27.7] - 2022-11-19\n\n### 改进\n\n* 支持备份在线收藏至本地\n* 支持隐藏画廊详情页评论区\n* 支持隐藏 HV 事件通知\n* 支持设置列表模式下缩略图大小\n* 支持设置画廊详情页预览图最大数量 (首先需要在网页设置中增加预览行数)，在低端设备上降低此值可缓解卡顿\n* 限制日期选择器的选择范围\n* 当预览只有一页时隐藏预览界面跳页按钮\n* 移除“修正缩略图链接”\n* 提升 targetSdkVersion 至 33\n\n## [1.7.27.6] - 2022-11-16\n\n### 重大变更\n\n* 适配 E 站全新的搜索系统，请阅读 [更新原帖](https://forums.e-hentai.org/index.php?showtopic=261743) 了解详细信息\n\n### 改进\n\n* 移除了墨水屏模式\n* 移除了统计相关代码\n* 更改了包名和签名\n"
  },
  {
    "path": "docs/README/zh-cn.md",
    "content": "<p align=\"right\">\n  <a href=\"/README.md\">\n  English\n  </a>\n  <span> | </span>\n  <strong>简体中文</strong>\n</p>\n\n<h1 align=\"center\">\n  <img src=\"https://github.com/EhViewer-NekoInverter/Arts/blob/main/launcher_icon-web.webp\" width=\"150\" alt=\"EhViewer\">\n  <br>EhViewer<br>\n</h1>\n\n<p align=\"center\">\n  <a href=\"https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml\">\n    <img src=\"https://img.shields.io/github/actions/workflow/status/EhViewer-NekoInverter/EhViewer/ci.yml?style=flat-square\" alt=\"Github Actions\">\n  </a>\n  <a href=\"/LICENSE\">\n    <img src=\"https://img.shields.io/github/license/EhViewer-NekoInverter/EhViewer?style=flat-square\" alt=\"LICENSE\">\n  </a>\n  <a href=\"https://github.com/EhViewer-NekoInverter/Ehviewer/releases\">\n    <img src=\"https://img.shields.io/github/v/release/EhViewer-NekoInverter/Ehviewer?style=flat-square&include_prereleases\" alt=\"Releases\">\n  </a>\n  <a href=\"https://github.com/EhViewer-NekoInverter/EhViewer/issues\">\n    <img src=\"https://img.shields.io/github/issues/EhViewer-NekoInverter/EhViewer?style=flat-square\" alt=\"Issues\">\n  </a>\n</p>\n\n<div align=\"center\">\n  <h3>\n    <a href=\"#描述\">\n    描述\n    </a>\n    <span> | </span>\n    <a href=\"#下载\">\n    下载\n    </a>\n    <span> | </span>\n    <a href=\"#截图\">\n    截图\n    </a>\n    <span> | </span>\n    <a href=\"#感谢\">\n    感谢\n    </a>\n    <span> | </span>\n    <a href=\"#许可证\">\n    许可证\n    </a>\n  </h3>\n</div>\n\n# 描述\n\n沿用 Material Design 2 经典风格的 EhViewer 分支\n\n本分支为自用性质，不接受功能请求，软件常见使用问题请参阅 [Q&A](https://github.com/EhViewer-NekoInverter/EhViewer/issues/18)\n\n如果您可以接受 Material Design 3，建议使用 [EhViewer-Overhauled](https://github.com/FooIbar/EhViewer)\n\n# 下载\n\n| Android 版本 | 备注         |\n|------------|------------|\n| 6.0-8.1    | 不支持动画 WebP |\n| 9.0+       | 完整支持       |\n\n- 请前往 [Github Releases](https://github.com/EhViewer-NekoInverter/EhViewer/releases) 下载发行版\n\n- 如果发行版存在未修复的问题，前往 [Github Actions](https://github.com/EhViewer-NekoInverter/EhViewer/actions/workflows/ci.yml) 下载 CI 版，需要登录 Github 账号\n\n# 截图\n\n![screenshot-01](https://github.com/EhViewer-NekoInverter/Arts/blob/main/screenshot-01.webp)\n![screenshot-02](https://github.com/EhViewer-NekoInverter/Arts/blob/main/screenshot-02.webp)\n\n# 感谢\n\n本项目受到了诸多开源项目的帮助\n\n- [AOSP & AndroidX](https://source.android.com/)\n- [Kotlin & KotlinX](https://kotlinlang.org/)\n- [Coil](https://coil-kt.github.io/coil/)\n- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)\n- [Jsoup](https://jsoup.org/)\n- [Ktor](https://ktor.io/)\n- [Libarchive](http://www.libarchive.org/)\n- [MDC-Android](https://github.com/material-components/material-components-android)\n- [OkHttp](https://square.github.io/okhttp/)\n- [RikkaX](https://github.com/RikkaApps/RikkaX)\n\n标签翻译数据\n\n- [EhTagTranslation](https://github.com/EhTagTranslation/Database)\n\n翻译人员\n\n- ja: [Re*Index. (ot_inc)](https://github.com/reindex-ot)\n\n# 许可证\n\n    Copyright 2014-2019 Hippo Seven\n    Copyright 2020-2022 NekoInverter\n    Copyright 2022-2025 Moedog\n\n    EhViewer is free software:\n    you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,\n    either version 3 of the License, or (at your option) any later version.\n\n    EhViewer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n    without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n    See the GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along with EhViewer.\n    If not, see <https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"8.13.2\"\nandroidx-activity = \"1.13.0\"\nandroidx-appcompat = \"1.7.1\"\nandroidx-biometric = \"1.4.0-alpha02\"\nandroidx-browser = \"1.9.0\"\nandroidx-collection = \"1.6.0\"\nandroidx-coordinatorlayout = \"1.3.0\"\nandroidx-core = \"1.18.0\"\nandroidx-fragment = \"1.8.9\"\nandroidx-lifecycle = \"2.10.0\"\nandroidx-paging = \"3.4.2\"\nandroidx-preference = \"1.2.1\"\nandroidx-recyclerview = \"1.4.0\"\nandroidx-room = \"2.8.4\"\nandroidx-swiperefreshlayout = \"1.2.0\"\nandroidx-webkit = \"1.15.0\"\ncoil = \"3.4.0\"\ndrawer = \"1.0.3\"\ngoogle-android-material = \"1.13.0\"\njsoup = \"1.21.2\"\nkotlin = \"2.3.20\"\nkotlinx-coroutines = \"1.10.2\"\nkotlinx-datetime = \"0.7.1\"\nkotlinx-serialization = \"1.10.0\"\nksp = \"2.3.4\"\nktor-utils = \"3.4.1\"\nokhttp3 = \"5.3.2\"\nokio = \"3.17.0\"\n#noinspection NewerVersionAvailable\nrikkax-material = { strictly = \"1.6.6\" }\nspotless = \"8.3.0\"\n\n[libraries]\nandroidx-activity = { module = \"androidx.activity:activity-ktx\", version.ref = \"androidx-activity\" }\nandroidx-appcompat = { module = \"androidx.appcompat:appcompat\", version.ref = \"androidx-appcompat\" }\nandroidx-biometric = { module = \"androidx.biometric:biometric-ktx\", version.ref = \"androidx-biometric\" }\nandroidx-browser = { module = \"androidx.browser:browser\", version.ref = \"androidx-browser\" }\nandroidx-collection = { module = \"androidx.collection:collection-ktx\", version.ref = \"androidx-collection\" }\nandroidx-coordinatorlayout = { module = \"androidx.coordinatorlayout:coordinatorlayout\", version.ref = \"androidx-coordinatorlayout\" }\nandroidx-core = { module = \"androidx.core:core-ktx\", version.ref = \"androidx-core\" }\nandroidx-fragment = { module = \"androidx.fragment:fragment-ktx\", version.ref = \"androidx-fragment\" }\nandroidx-lifecycle-process = { module = \"androidx.lifecycle:lifecycle-process\", version.ref = \"androidx-lifecycle\" }\nandroidx-paging-runtime = { module = \"androidx.paging:paging-runtime-ktx\", version.ref = \"androidx-paging\" }\nandroidx-preference = { module = \"androidx.preference:preference-ktx\", version.ref = \"androidx-preference\" }\nandroidx-recyclerview = { module = \"androidx.recyclerview:recyclerview\", version.ref = \"androidx-recyclerview\" }\nandroidx-room-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"androidx-room\" }\nandroidx-room-paging = { module = \"androidx.room:room-paging\", version.ref = \"androidx-room\" }\nandroidx-swiperefreshlayout = { module = \"androidx.swiperefreshlayout:swiperefreshlayout\", version.ref = \"androidx-swiperefreshlayout\" }\nandroidx-webkit = { module = \"androidx.webkit:webkit\", version.ref = \"androidx-webkit\" }\n\ncoil = { module = \"io.coil-kt.coil3:coil\" }\ncoil-bom = { module = \"io.coil-kt.coil3:coil-bom\", version.ref = \"coil\" }\ncoil-gif = { module = \"io.coil-kt.coil3:coil-gif\" }\ncoil-network = { module = \"io.coil-kt.coil3:coil-network-okhttp\" }\n\ndrawer = { module = \"com.drakeet.drawer:drawer\", version.ref = \"drawer\" }\njsoup = { module = \"org.jsoup:jsoup\", version.ref = \"jsoup\" }\nkotlinx-coroutines-android = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-android\", version.ref = \"kotlinx-coroutines\" }\nkotlinx-datetime = { module = \"org.jetbrains.kotlinx:kotlinx-datetime\", version.ref = \"kotlinx-datetime\" }\nkotlinx-serialization-cbor = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-cbor\", version.ref = \"kotlinx-serialization\" }\nktlint = \"com.pinterest.ktlint:ktlint-cli:1.8.0\"\nktor-utils = { module = \"io.ktor:ktor-utils\", version.ref = \"ktor-utils\" }\nmaterial = { module = \"com.google.android.material:material\", version.ref = \"google-android-material\" }\n\ndesugar = \"com.android.tools:desugar_jdk_libs:2.1.5\"\n\nokhttp-bom = { module = \"com.squareup.okhttp3:okhttp-bom\", version.ref = \"okhttp3\" }\nokhttp-coroutines = { module = \"com.squareup.okhttp3:okhttp-coroutines\" }\nokhttp-tls = { module = \"com.squareup.okhttp3:okhttp-tls\" }\nokio-jvm = { module = \"com.squareup.okio:okio-jvm\", version.ref = \"okio\" }\n\nrikkax-core = \"dev.rikka.rikkax.core:core-ktx:1.4.1\"\nrikkax-insets = \"dev.rikka.rikkax.insets:insets:1.3.0\"\nrikkax-layoutinflater = \"dev.rikka.rikkax.layoutinflater:layoutinflater:1.3.0\"\nrikkax-material = { module = \"dev.rikka.rikkax.material:material\", version.ref = \"rikkax-material\" }\nrikkax-simplemenu-preference = \"dev.rikka.rikkax.preference:simplemenu-preference:1.0.3\"\n\n[bundles]\ncoil = [\"coil\", \"coil-gif\", \"coil-network\"]\nrikkax = [\"rikkax-core\", \"rikkax-insets\", \"rikkax-layoutinflater\", \"rikkax-material\", \"rikkax-simplemenu-preference\"]\n\n[plugins]\nandroid-application = { id = \"com.android.application\", version.ref = \"agp\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlin-parcelize = { id = \"org.jetbrains.kotlin.plugin.parcelize\", version.ref = \"kotlin\" }\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\nksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nspotless = { id = \"com.diffplug.spotless\", version.ref = \"spotless\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.14-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://wwwCMakeLists.txt\n#main.c.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: -Xmx10248m -XX:MaxPermSize=256m\norg.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 -XX:+UseParallelGC\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\norg.gradle.parallel=true\norg.gradle.caching=true\norg.gradle.configuration-cache=true\nandroid.useAndroidX=true\nandroid.enableAppCompileTimeRClass=true\nandroid.r8.integratedResourceShrinking=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\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#      https://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# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\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    if ! command -v java >/dev/null 2>&1\n    then\n        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.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\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='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n    }\n}\n\n@Suppress(\"UnstableApiUsage\")\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\nplugins {\n    id(\"com.android.settings\") version \"8.13.2\"\n}\n\nandroid {\n    compileSdk = 36\n    minSdk = 23\n    targetSdk = 36\n    ndkVersion = \"29.0.14206865\"\n    buildToolsVersion = \"36.0.0\"\n}\n\nenableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\nrootProject.name = \"EhViewer\"\ninclude(\":app\")\n"
  }
]